Left 1 type of messaging contract - code triggers an output

we're removing 2 additional types of messaging contracts
we're removing any leftovers of rabbit and kafka support - users will need to provide their own message receiver and a message sender
This commit is contained in:
Marcin Grzejszczak
2022-11-15 16:00:48 +01:00
parent fa51d6a076
commit 1999066c5a
117 changed files with 105 additions and 7813 deletions

View File

@@ -2,7 +2,7 @@
set -e
WRAPPER_VERSION="7.4.1"
WRAPPER_VERSION="7.5.1"
GRADLE_BIN_DIR="gradle-${WRAPPER_VERSION}-bin"
GRADLE_WRAPPER_DIR="${HOME}/.gradle/wrapper/dists/${GRADLE_BIN_DIR}"
CURRENT_DIR="$( pwd )"

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -120,9 +120,6 @@ public abstract class ContractTestsBase {
AmqpMetadata amqpMetadata = AmqpMetadata.fromMetadata(contract.metadata);
if (isMessagingType("rabbit") && hasDeclaredOutputQueue(amqpMetadata) || isMessagingType("kafka")) {
log.info("First will try to receive a message to setup the connection with the broker");
if (contract.input != null && StringUtils.hasText(contract.input.messageFrom)) {
setupConnection(contract.input.messageFrom, contract);
}
if (contract.outputMessage != null && StringUtils.hasText(contract.outputMessage.sentTo)){
setupConnection(contract.outputMessage.sentTo, contract);
}

View File

@@ -30,6 +30,8 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.contract.verifier.converter.YamlContract;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.messaging.amqp.AmqpMetadata;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessageMetadata;
@@ -68,8 +70,8 @@ public class MessagingAutoConfig {
@Bean
public ContractVerifierMessaging<Message> contractVerifierMessaging(
MessageVerifier<Message> exchange) {
return new ContractVerifierCamelHelper(exchange);
MessageVerifierSender<Message> sender, MessageVerifierReceiver<Message> receiver) {
return new ContractVerifierCamelHelper(sender, receiver);
}
@Bean
@@ -185,8 +187,8 @@ public class MessagingAutoConfig {
class ContractVerifierCamelHelper extends ContractVerifierMessaging<Message> {
ContractVerifierCamelHelper(MessageVerifier<Message> exchange) {
super(exchange);
ContractVerifierCamelHelper(MessageVerifierSender<Message> sender, MessageVerifierReceiver<Message> receiver) {
super(sender, receiver);
}
@Override
@@ -197,4 +199,4 @@ class ContractVerifierCamelHelper extends ContractVerifierMessaging<Message> {
return new ContractVerifierMessage(receive.getBody(), receive.getHeaders());
}
}
}

View File

@@ -13,7 +13,6 @@ The DSL for messaging looks a little bit different than the one that focuses on
following sections explain the differences:
* <<contract-dsl-output-triggered-method>>
* <<contract-dsl-output-triggered-message>>
* <<contract-dsl-consumer-producer>>
* <<contract-dsl-messaging-common>>
@@ -42,31 +41,6 @@ In the previous example case, the output message is sent to `output` if a method
test that calls that method to trigger the message. On the consumer side, you can use
`some_label` to trigger the message.
[[contract-dsl-output-triggered-message]]
==== Output Triggered by a Message
The output message can be triggered by receiving a message, as shown in the following
example:
====
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy
----
include::{tests_path}/samples-messaging-integration/src/test/groovy/com/example/IntegrationMessagingApplicationSpec.groovy[tags=message_trigger,indent=0]
----
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
.YAML
----
include::{verifier_core_path}/src/test/resources/yml/contract_message_input_message.yml[indent=0]
----
====
In the preceding example, the output message is sent to `output` if a proper message is
received on the `input` destination. On the message publisher's side, the engine
generates a test that sends the input message to the defined destination. On the
consumer side, you can either send a message to the input destination or use a label
(`some_label` in the example) to trigger the message.
[[contract-dsl-consumer-producer]]
==== Consumer/Producer
@@ -75,16 +49,9 @@ IMPORTANT: This section is valid only for the Groovy DSL.
In HTTP, you have a notion of `client`/`stub and `server`/`test` notation. You can also
use those paradigms in messaging. In addition, Spring Cloud Contract Verifier also
provides the `consumer` and `producer` methods, as presented in the following example
provides the `consumer` and `producer` methods
(note that you can use either `$` or `value` methods to provide `consumer` and `producer`
parts):
====
[source,groovy]
----
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=consumer_producer]
----
====
parts).
[[contract-dsl-messaging-common]]
==== Common
@@ -97,14 +64,12 @@ in the generated test.
[[features-messaging-integrations]]
=== Integrations
You can use one of the following four integration configurations:
You can use one of the following integration configurations:
* Apache Camel
* Spring Integration
* Spring Cloud Stream
* Spring AMQP
* Spring JMS (requires embedded broker)
* Spring Kafka (requires embedded broker)
* Spring JMS
Since we use Spring Boot, if you have added one of these libraries to the classpath, all
the messaging configuration is automatically set up.
@@ -143,8 +108,8 @@ testImplementation(group: 'org.springframework.cloud', name: 'spring-cloud-strea
==== Manual Integration Testing
The main interface used by the tests is
`org.springframework.cloud.contract.verifier.messaging.MessageVerifier`.
It defines how to send and receive messages. You can create your own implementation to
`org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender` and `org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver`.
It defines how to send and receive messages. If you need both sender and receiver capabilities you can use `org.springframework.cloud.contract.verifier.messaging.MessageVerifier` interface. You can create your own implementation to
achieve the same goal.
In a test, you can inject a `ContractVerifierMessageExchange` to send and receive
@@ -176,22 +141,12 @@ Having the `input` or `outputMessage` sections in your DSL results in creation o
on the publisher's side. By default, JUnit 4 tests are created. However, there is also a
possibility to create JUnit 5, TestNG, or Spock tests.
There are three main scenarios that we should take into consideration:
* Scenario 1: There is no input message that produces an output message. The output
message is triggered by a component inside the application (for example, a scheduler).
* Scenario 2: The input message triggers an output message.
* Scenario 3: The input message is consumed, and there is no output message.
IMPORTANT: The destination passed to `messageFrom` or `sentTo` can have different
meanings for different messaging implementations. For Stream and Integration, it is
first resolved as a `destination` of a channel. Then, if there is no such `destination`,
it is resolved as a channel name. For Camel, that's a certain component (for example,
`jms`).
[[features-messaging-scenario1]]
==== Scenario 1: No Input Message
Consider the following contract:
=====
@@ -225,76 +180,6 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract
----
====
[[features-messaging-scenario2]]
==== Scenario 2: Output Triggered by Input
Consider the following contract:
=====
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy
----
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_message_dsl]
----
[source,yml,indent=0,subs="verbatim,attributes",role="secondary"]
.YAML
----
include::{verifier_core_path}/src/test/resources/yml/contract_message_scenario2.yml[indent=0]
----
=====
For the preceding contract, the following test would be created:
====
[source,java,indent=0,subs="verbatim,attributes",role="primary"]
.JUnit
----
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_message_junit]
----
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
.Spock
----
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_message_spock]
----
====
[[features-messaging-scenario3]]
==== Scenario 3: No Output Message
Consider the following contract:
====
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy
----
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_no_output_dsl]
----
[source,yml,indent=0,subs="verbatim,attributes",role="secondary"]
.YAML
----
include::{verifier_core_path}/src/test/resources/yml/contract_message_scenario3.yml[indent=0]
----
====
For the preceding contract, the following test would be created:
====
[source,java,indent=0,subs="verbatim,attributes",role="primary"]
.JUnit
----
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_no_output_junit]
----
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
.Spock
----
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_no_output_spock]
----
====
[[features-messaging-consumer]]
=== Consumer Stub Generation
@@ -418,7 +303,7 @@ Assume that we have the following Maven repository with deployed stubs for the
----
====
Further assume that the stubs contain the following structure:
Further, assume that the stubs contain the following structure:
====
[source,bash,indent=0]
@@ -427,107 +312,33 @@ Further assume that the stubs contain the following structure:
│   └── MANIFEST.MF
└── repository
├── accurest
│   ── bookDeleted.groovy
│   ├── bookReturned1.groovy
│   └── bookReturned2.groovy
│   ── bookReturned1.groovy
└── mappings
----
====
Now consider the following contracts (we number them 1 and 2):
Now consider the following contract:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=sample_dsl,indent=0]
----
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0]
include::{tests_path}/samples-messaging-camel/src/test/groovy/com/example/CamelMessagingApplicationSpec.groovy[tags=sample_dsl,indent=0]
----
====
These examples lend themselves to three scenarios:
. <<features-messaging-stub-runner-camel-scenario1>>
. <<features-messaging-stub-runner-camel-scenario2>>
. <<features-messaging-stub-runner-camel-scenario3>>
[[features-messaging-stub-runner-camel-scenario1]]
===== Scenario 1 (No Input Message)
To trigger a message from the `return_book_1` label, we use the `StubTrigger` interface, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_trigger,indent=0]
stubFinder.trigger("return_book_1")
----
====
Next, we want to listen to the output of the message sent to `{output_name}`:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0]
----
====
The received message would then pass the following assertions:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_trigger_message,indent=0]
----
====
[[features-messaging-stub-runner-camel-scenario2]]
===== Scenario 2 (Output Triggered by Input)
Since the route is set for you, you can send a message to the `{output_name}` destination.
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_send,indent=0]
----
====
Next, we want to listen to the output of the message sent to `{output_name}`, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_receive,indent=0]
----
====
The received message would pass the following assertions:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_receive_message,indent=0]
----
====
[[features-messaging-stub-runner-camel-scenario3]]
===== Scenario 3 (Input with No Output)
Since the route is set for you, you can send a message to the `{output_name}` destination, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=trigger_no_output,indent=0]
----
That will send out a message to the destination described in the output message of the contract.
:input_name: input
:output_name: output
====
[[features-messaging-stub-runner-integration]]
=== Consumer Side Messaging with Spring Integration
@@ -581,25 +392,18 @@ Further assume the stubs contain the following structure:
│   └── MANIFEST.MF
└── repository
├── accurest
│   ── bookDeleted.groovy
│   ├── bookReturned1.groovy
│   └── bookReturned2.groovy
│   ── bookReturned1.groovy
└── mappings
----
====
Consider the following contracts (numbered 1 and 2):
Consider the following contract:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=sample_dsl,indent=0]
----
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0]
----
====
Now consider the following Spring Integration Route:
@@ -611,15 +415,6 @@ include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/res
----
====
These examples lend themselves to three scenarios:
. <<features-messaging-stub-runner-integration-scenario1>>
. <<features-messaging-stub-runner-integration-scenario2>>
. <<features-messaging-stub-runner-integration-scenario3>>
[[features-messaging-stub-runner-integration-scenario1]]
===== Scenario 1 (No Input Message)
To trigger a message from the `return_book_1` label, use the `StubTrigger` interface, as
follows:
@@ -630,66 +425,7 @@ include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/gro
----
====
The following listing shows how to listen to the output of the message sent to `{output_name}`:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0]
----
====
The received message would pass the following assertions:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_trigger_message,indent=0]
----
====
[[features-messaging-stub-runner-integration-scenario2]]
===== Scenario 2 (Output Triggered by Input)
Since the route is set for you, you can send a message to the `{output_name}`
destination, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_send,indent=0]
----
====
The following listing shows how to listen to the output of the message sent to `{output_name}`:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_receive,indent=0]
----
====
The received message passes the following assertions:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_receive_message,indent=0]
----
====
[[features-messaging-stub-runner-integration-scenario3]]
===== Scenario 3 (Input with No Output)
Since the route is set for you, you can send a message to the `{input_name}` destination, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=trigger_no_output,indent=0]
----
====
That will send out a message to the destination described in the output message of the contract.
[[features-messaging-stub-runner-stream]]
=== Consumer Side Messaging With Spring Cloud Stream
@@ -773,24 +509,26 @@ Further assume the stubs contain the following structure:
│   └── MANIFEST.MF
└── repository
├── accurest
│   ── bookDeleted.groovy
│   ├── bookReturned1.groovy
│   └── bookReturned2.groovy
│   ── bookReturned1.groovy
└── mappings
----
====
Consider the following contracts (numbered 1 and 2):
Consider the following contract:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=sample_dsl,indent=0]
----
====
[source,groovy]
Now consider the following Spring Cloud Stream function configuration:
====
[source,yaml]
----
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0]
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=setup,indent=0]
----
====
@@ -803,15 +541,6 @@ include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/resource
----
====
These examples lend themselves to three scenarios:
* <<features-messaging-stub-runner-stream-scenario1>>
* <<features-messaging-stub-runner-stream-scenario2>>
* <<features-messaging-stub-runner-stream-scenario3>>
[[features-messaging-stub-runner-stream-scenario1]]
===== Scenario 1 (No Input Message)
To trigger a message from the `return_book_1` label, use the `StubTrigger` interface as
follows:
@@ -822,220 +551,8 @@ include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/o
----
====
The following example shows how to listen to the output of the message sent to a channel whose `destination` is
`returnBook`:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0]
----
====
The received message passes the following assertions:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_trigger_message,indent=0]
----
====
[[features-messaging-stub-runner-stream-scenario2]]
===== Scenario 2 (Output Triggered by Input)
Since the route is set for you, you can send a message to the `bookStorage`
`destination`, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_send,indent=0]
----
====
The following example shows how to listen to the output of the message sent to `returnBook`:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_receive,indent=0]
----
====
The received message passes the following assertions:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_receive_message,indent=0]
----
====
[[features-messaging-stub-runner-stream-scenario3]]
===== Scenario 3 (Input with No Output)
Since the route is set for you, you can send a message to the `{output_name}`
destination, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=trigger_no_output,indent=0]
----
====
[[features-messaging-stub-runner-amqp]]
=== Consumer Side Messaging With Spring AMQP
Spring Cloud Contract Stub Runner's messaging module provides an easy way to
integrate with Spring AMQP's Rabbit Template. For the provided artifacts, it
automatically downloads the stubs and registers the required routes.
The integration tries to work standalone (that is, without interaction with a running
RabbitMQ message broker). It expects a `RabbitTemplate` on the application context and
uses it as a Spring Boot test named `@SpyBean`. As a result, it can use the Mockito spy
functionality to verify and inspect messages sent by the application.
On the message consumer side, the stub runner considers all `@RabbitListener`-annotated
endpoints and all `SimpleMessageListenerContainer` objects on the application context.
As messages are usually sent to exchanges in AMQP, the message contract contains the
exchange name as the destination. Message listeners on the other side are bound to
queues. Bindings connect an exchange to a queue. If message contracts are triggered, the
Spring AMQP stub runner integration looks for bindings on the application context that
matches this exchange. Then it collects the queues from the Spring exchanges and tries to
find message listeners bound to these queues. The message is triggered for all matching
message listeners.
If you need to work with routing keys, you can pass them by using the `amqp_receivedRoutingKey`
messaging header.
[[features-messaging-stub-runner-amqp-adding]]
==== Adding the Runner to the Project
You can have both Spring AMQP and Spring Cloud Contract Stub Runner on the classpath and
set the property `stubrunner.amqp.enabled=true`. Remember to annotate your test class
with `@AutoConfigureStubRunner`.
IMPORTANT: If you already have Stream and Integration on the classpath, you need
to explicitly disable them by setting the `stubrunner.stream.enabled=false` and
`stubrunner.integration.enabled=false` properties.
[[features-messaging-stub-runner-amqp-example]]
==== Examples
Assume that you have the following Maven repository with a deployed stubs for the
`spring-cloud-contract-amqp-test` application:
====
[source,bash,indent=0]
----
└── .m2
└── repository
└── com
└── example
└── spring-cloud-contract-amqp-test
├── 0.4.0-SNAPSHOT
│   ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT.pom
│   ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT-stubs.jar
│   └── maven-metadata-local.xml
└── maven-metadata-local.xml
----
====
Further assume that the stubs contain the following structure:
====
[source,bash,indent=0]
----
├── META-INF
│   └── MANIFEST.MF
└── contracts
└── shouldProduceValidPersonData.groovy
----
====
Then consider the following contract:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-amqp/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpStubRunnerSpec.groovy[tags=amqp_contract,indent=0]
----
====
Now consider the following Spring configuration:
====
[source,yaml]
----
include::{tests_path}/spring-cloud-contract-stub-runner-amqp/src/test/resources/application.yml[]
----
====
[[features-messaging-stub-runner-amqp-triggering]]
===== Triggering the Message
To trigger a message using the contract in the preceding section, use the `StubTrigger` interface, as
follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-amqp/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpStubRunnerSpec.groovy[tags=client_trigger,indent=0]
----
====
The message has a destination of `contract-test.exchange`, so the Spring AMQP stub runner
integration looks for bindings related to this exchange, as the following example shows:
====
[source,java]
----
include::{tests_path}/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpMessagingApplication.java[tags=amqp_binding,indent=0]
----
====
The binding definition binds the queue called `test.queue`. As a result, the following listener
definition is matched and invoked with the contract message:
====
[source,java]
----
include::{tests_path}/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpMessagingApplication.java[tags=amqp_listener,indent=0]
----
====
Also, the following annotated listener matches and is invoked:
====
[source,java]
----
include::{tests_path}/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/MessageSubscriberRabbitListener.java[tags=amqp_annotated_listener,indent=0]
----
====
NOTE: The message is directly handed over to the `onMessage` method of the
`MessageListener` associated with the matching `SimpleMessageListenerContainer`.
[[features-messaging-stub-runner-amqp-configuration]]
===== Spring AMQP Test Configuration
To avoid Spring AMQP trying to connect to a running broker during our tests, we
configure a mock `ConnectionFactory`.
To disable the mocked `ConnectionFactory`, set the following property:
`stubrunner.amqp.mockConnection=false`, as follows:
====
[source,yaml]
----
stubrunner:
amqp:
mockConnection: false
----
====
That will send out a message to the destination described in the output message of the contract.
[[features-messaging-stub-runner-jms]]
=== Consumer Side Messaging With Spring JMS
@@ -1043,7 +560,7 @@ stubrunner:
Spring Cloud Contract Stub Runner's messaging module provides an easy way to
integrate with Spring JMS.
The integration assumes that you have a running instance of a JMS broker (such as an `activemq` embedded broker).
The integration assumes that you have a running instance of a JMS broker.
[[features-messaging-stub-runner-jms-adding]]
==== Adding the Runner to the Project
@@ -1063,9 +580,7 @@ Assume that the stub structure looks as follows:
[source,bash,indent=0]
----
├── stubs
── bookDeleted.groovy
├── bookReturned1.groovy
└── bookReturned2.groovy
── bookReturned1.groovy
----
====
@@ -1088,23 +603,15 @@ spring:
----
====
Now consider the following contracts (we number them 1 and 2):
Now consider the following contract:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=sample_dsl,indent=0]
----
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0]
----
====
[[features-messaging-stub-runner-jms-scenario1]]
===== Scenario 1 (No Input Message)
To trigger a message from the `return_book_1` label, we use the `StubTrigger` interface, as follows:
====
@@ -1114,213 +621,4 @@ include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/
----
====
Next, we want to listen to the output of the message sent to `{output_name}`:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0]
----
====
The received message would then pass the following assertions:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_trigger_message,indent=0]
----
====
[[features-messaging-stub-runner-jms-scenario2]]
===== Scenario 2 (Output Triggered by Input)
Since the route is set for you, you can send a message to the `{output_name}` destination.
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_send,indent=0]
----
====
Next, we want to listen to the output of the message sent to `{output_name}`, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_receive,indent=0]
----
====
The received message would pass the following assertions:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_receive_message,indent=0]
----
====
[[features-messaging-stub-runner-jms-scenario3]]
===== Scenario 3 (Input with No Output)
Since the route is set for you, you can send a message to the `{output_name}` destination, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=trigger_no_output,indent=0]
----
====
[[features-messaging-stub-runner-kafka]]
=== Consumer Side Messaging With Spring Kafka
Spring Cloud Contract Stub Runner's messaging module provides an easy way to
integrate with Spring Kafka.
The integration assumes that you have a running instance of an embedded Kafka broker (through the `spring-kafka-test` dependency).
[[features-messaging-stub-runner-kafka-adding]]
==== Adding the Runner to the Project
You need to have Spring Kafka, Spring Kafka Test (to run the `@EmbeddedBroker`), and Spring Cloud Contract Stub Runner on the classpath. Remember to annotate your test class
with `@AutoConfigureStubRunner`.
With Kafka integration, in order to poll for a single message, we need to register a consumer upon Spring context startup. That may lead to a situation that, when you are on the consumer side, Stub Runner can register an additional consumer for the same group ID and topic. That could lead to a situation that only one of the components would actually poll for the message. Since, on the consumer side, you have both the Spring Cloud Contract Stub Runner and Spring Cloud Contract Verifier classpath, we need to be able to switch off such behavior. That is done automatically through the `stubrunner.kafka.initializer.enabled` flag, which disables the Contact Verifier consumer registration. If your application is both the consumer and the producer of a Kafka message, you might need to manually toggle that property to `false` in the base class of your generated tests.
If you have multiple `KafkaTemplate` beans, you can provide your own bean of `Supplier<KafkaTemplate>` type that returns the `KafkaTemplate` of your chosing.
:input_name: input
:output_name: output
[[features-messaging-stub-runner-kafka-example]]
==== Examples
Assume that the stub structure looks as follows:
====
[source,bash,indent=0]
----
├── stubs
├── bookDeleted.groovy
├── bookReturned1.groovy
└── bookReturned2.groovy
----
====
Further assume the following test configuration (notice the `spring.kafka.bootstrap-servers` pointing to the embedded broker's IP via `${spring.embedded.kafka.brokers}`):
====
[source,yml,indent=0]
----
stubrunner:
repository-root: stubs:classpath:/stubs/
ids: my:stubs
stubs-mode: remote
spring:
kafka:
bootstrap-servers: ${spring.embedded.kafka.brokers}
producer:
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
properties:
"spring.json.trusted.packages": "*"
consumer:
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
properties:
"spring.json.trusted.packages": "*"
group-id: groupId
----
====
NOTE: If your application uses non-integer record keys you will need to set the `spring.kafka.producer.key-serializer`
and `spring.kafka.consumer.key-deserializer` properties accordingly because the Kafka de/serialization expects non-null
record keys to be of integer type.
Now consider the following contracts (we number them 1 and 2):
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=sample_dsl,indent=0]
----
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0]
----
====
[[features-messaging-stub-runner-kafka-scenario1]]
===== Scenario 1 (No Input Message)
To trigger a message from the `return_book_1` label, we use the `StubTrigger` interface, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=client_trigger,indent=0]
----
====
Next, we want to listen to the output of the message sent to `{output_name}`:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0]
----
====
The received message would then pass the following assertions:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=client_trigger_message,indent=0]
----
====
[[features-messaging-stub-runner-kafka-scenario2]]
===== Scenario 2 (Output Triggered by Input)
Since the route is set for you, you can send a message to the `{output_name}` destination.
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=client_send,indent=0]
----
====
Next, we want to listen to the output of the message sent to `{output_name}`, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=client_receive,indent=0]
----
====
The received message would pass the following assertions:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=client_receive_message,indent=0]
----
====
[[features-messaging-stub-runner-kafka-scenario3]]
===== Scenario 3 (Input with No Output)
Since the route is set for you, you can send a message to the `{output_name}` destination, as follows:
====
[source,groovy]
----
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=trigger_no_output,indent=0]
----
====
That will send out a message to the destination described in the output message of the contract.

View File

@@ -446,13 +446,13 @@ The following example shows a Camel messaging contract:
[source,groovy,indent=0,role="primary"]
.groovy
----
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_no_output_dsl]
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_method_dsl]
----
[source,yaml,indent=0,role="secondary"]
.yaml
----
include::{verifier_core_path}/src/test/resources/yml/contract_message_scenario3.yml[indent=0]
include::{verifier_core_path}/src/test/resources/yml/contract_message_scenario1.yml[indent=0]
----
====

View File

@@ -21,11 +21,7 @@ import org.springframework.cloud.contract.spec.Contract
Contract.make {
label("positive")
input {
messageFrom("bytes_input")
messageBody(fileAsBytes("input.pdf"))
messageHeaders {
messagingContentType(applicationOctetStream())
}
triggeredBy("hashCode()")
}
outputMessage {
sentTo("bytes_output")

View File

@@ -1,25 +0,0 @@
/*
* 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.spec.internal;
class ClientInput extends Input {
ClientInput(Input request) {
super(request);
}
}

View File

@@ -17,14 +17,8 @@
package org.springframework.cloud.contract.spec.internal;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import groovy.lang.Closure;
import groovy.lang.DelegatesTo;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Represents an input for messaging. The input can be a message or some action inside the
* application.
@@ -35,49 +29,12 @@ import org.apache.commons.logging.LogFactory;
*/
public class Input extends Common implements RegexCreatingProperty<ClientDslProperty> {
private static final Log log = LogFactory.getLog(Input.class);
private ClientPatternValueDslProperty property = new ClientPatternValueDslProperty();
private DslProperty<String> messageFrom;
private ExecutionProperty triggeredBy;
private Headers messageHeaders = new Headers();
private BodyType messageBody;
private ExecutionProperty assertThat;
private BodyMatchers bodyMatchers;
public Input() {
}
public Input(Input input) {
this.messageFrom = input.getMessageFrom();
this.messageHeaders = input.getMessageHeaders();
this.messageBody = input.getMessageBody();
}
/**
* Name of a destination from which message would come to trigger action in the
* system.
* @param messageFrom message destination
*/
public void messageFrom(String messageFrom) {
this.messageFrom = new DslProperty<>(messageFrom);
}
/**
* Name of a destination from which message would come to trigger action in the
* system.
* @param messageFrom message destination
*/
public void messageFrom(DslProperty messageFrom) {
this.messageFrom = messageFrom;
}
/**
* Function that needs to be executed to trigger action in the system.
* @param triggeredBy method name that triggers the message
@@ -86,11 +43,6 @@ public class Input extends Common implements RegexCreatingProperty<ClientDslProp
this.triggeredBy = new ExecutionProperty(triggeredBy);
}
public BodyType messageBody(Object bodyAsValue) {
this.messageBody = new BodyType(bodyAsValue);
return this.messageBody;
}
public DslProperty value(ClientDslProperty client) {
Object dynamicValue = client.getClientValue();
Object concreteValue = client.getServerValue();
@@ -129,14 +81,6 @@ public class Input extends Common implements RegexCreatingProperty<ClientDslProp
this.property = property;
}
public DslProperty<String> getMessageFrom() {
return messageFrom;
}
public void setMessageFrom(DslProperty<String> messageFrom) {
this.messageFrom = messageFrom;
}
public ExecutionProperty getTriggeredBy() {
return triggeredBy;
}
@@ -145,22 +89,6 @@ public class Input extends Common implements RegexCreatingProperty<ClientDslProp
this.triggeredBy = triggeredBy;
}
public Headers getMessageHeaders() {
return messageHeaders;
}
public void setMessageHeaders(Headers messageHeaders) {
this.messageHeaders = messageHeaders;
}
public BodyType getMessageBody() {
return messageBody;
}
public void setMessageBody(BodyType messageBody) {
this.messageBody = messageBody;
}
public ExecutionProperty getAssertThat() {
return assertThat;
}
@@ -169,14 +97,6 @@ public class Input extends Common implements RegexCreatingProperty<ClientDslProp
this.assertThat = assertThat;
}
public BodyMatchers getBodyMatchers() {
return bodyMatchers;
}
public void setBodyMatchers(BodyMatchers bodyMatchers) {
this.bodyMatchers = bodyMatchers;
}
@Override
public ClientDslProperty anyAlphaUnicode() {
return property.anyAlphaUnicode();
@@ -282,44 +202,6 @@ public class Input extends Common implements RegexCreatingProperty<ClientDslProp
return property.anyOf(values);
}
/**
* The message headers part of the contract.
* @param consumer function to manipulate the message headers
*/
public void messageHeaders(Consumer<Headers> consumer) {
this.messageHeaders = new Headers();
consumer.accept(this.messageHeaders);
}
/**
* The stub matchers part of the contract.
* @param consumer function to manipulate the message headers
*/
public void bodyMatchers(Consumer<BodyMatchers> consumer) {
this.bodyMatchers = new BodyMatchers();
consumer.accept(this.bodyMatchers);
}
/**
* The message headers part of the contract.
* @param consumer function to manipulate the message headers
*/
public void messageHeaders(@DelegatesTo(Headers.class) Closure consumer) {
this.messageHeaders = new Headers();
consumer.setDelegate(this.messageHeaders);
consumer.call();
}
/**
* The stub matchers part of the contract.
* @param consumer function to manipulate the message headers
*/
public void bodyMatchers(@DelegatesTo(BodyMatchers.class) Closure consumer) {
this.bodyMatchers = new BodyMatchers();
consumer.setDelegate(this.bodyMatchers);
consumer.call();
}
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -329,34 +211,18 @@ public class Input extends Common implements RegexCreatingProperty<ClientDslProp
return false;
}
Input input = (Input) o;
return Objects.equals(messageFrom, input.messageFrom) && Objects.equals(triggeredBy, input.triggeredBy)
&& Objects.equals(messageHeaders, input.messageHeaders)
&& Objects.equals(messageBody, input.messageBody) && Objects.equals(assertThat, input.assertThat)
&& Objects.equals(bodyMatchers, input.bodyMatchers);
return Objects.equals(triggeredBy, input.triggeredBy) && Objects.equals(assertThat, input.assertThat);
}
@Override
public int hashCode() {
return Objects.hash(messageFrom, triggeredBy, messageHeaders, messageBody, assertThat, bodyMatchers);
return Objects.hash(triggeredBy, assertThat);
}
@Override
public String toString() {
return "Input{\n\tmessageFrom=" + messageFrom + ", \n\ttriggeredBy=" + triggeredBy + ", \n\tmessageHeaders="
+ messageHeaders + ", \n\tmessageBody=" + messageBody + ", \n\tassertThat=" + assertThat
+ ", \n\tbodyMatchers=" + bodyMatchers + "} \n\t" + super.toString();
}
public static class BodyType extends DslProperty {
public BodyType(Object clientValue, Object serverValue) {
super(clientValue, serverValue);
}
public BodyType(Object singleValue) {
super(singleValue);
}
return "Input{\n\t" + ", \n\ttriggeredBy=" + triggeredBy + ", \n\tassertThat=" + assertThat + "} \n\t"
+ super.toString();
}
private class ClientPatternValueDslProperty extends PatternValueDslProperty<ClientDslProperty> {

View File

@@ -1,25 +0,0 @@
/*
* 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.spec.internal;
class ServerInput extends Input {
ServerInput(Input request) {
super(request);
}
}

View File

@@ -31,53 +31,16 @@ class InputDsl : CommonDsl() {
private val delegate = Input()
/**
* Name of a destination from which message would come to trigger action in the
* system.
*/
var messageFrom: DslProperty<String>? = null
/**
* Function that needs to be executed to trigger action in the system.
*/
var triggeredBy: String? = null
/**
* The message headers part of the contract.
*/
var headers: Headers? = null
/**
* The contents of the incoming message.
*/
var messageBody: Input.BodyType? = null
/**
* Function that needs to be executed after the message has been received/processed by the system.
*/
var assertThat: String? = null
/**
* The body matchers part of the contract.
*/
var bodyMatchers: BodyMatchers? = null
fun messageFrom(messageFrom: String) = messageFrom.toDslProperty()
fun headers(headers: HeadersDsl.() -> Unit) {
this.headers = HeadersDsl().apply(headers).get()
}
fun messageBody(vararg pairs: Pair<String, Any>) = Input.BodyType(pairs.toMap())
fun messageBody(pair: Pair<String, Any>) = Input.BodyType(mapOf(pair))
fun messageBody(value: String) = Input.BodyType(value)
fun bodyMatchers(configurer: BodyMatchersDsl.() -> Unit) {
this.bodyMatchers = BodyMatchersDsl().apply(configurer).get()
}
/* HELPER VARIABLES */
val anyAlphaUnicode: ClientDslProperty
@@ -170,12 +133,8 @@ class InputDsl : CommonDsl() {
internal fun get(): Input {
val input = Input()
messageFrom?.also { input.messageFrom = messageFrom }
triggeredBy?.also { input.triggeredBy(triggeredBy) }
headers?.also { input.messageHeaders = headers }
messageBody?.also { input.messageBody = messageBody }
assertThat?.also { input.assertThat(assertThat) }
bodyMatchers?.also { input.bodyMatchers = bodyMatchers }
return input
}
}

View File

@@ -142,90 +142,6 @@ class ContractTests {
}
}
@Test
fun `should work for messaging`() {
val contract = contract {
input {
messageFrom = messageFrom("input")
messageBody = messageBody("foo" to "bar")
headers {
header("foo", "bar")
header("X-Custom-Header", value(consumer(regex("^.*2134.*\$")), producer("121345")))
}
}
outputMessage {
sentTo = sentTo("output")
body = body("foo" to "bar", "foo2" to "bar2")
headers {
header("foo2", "bar")
header("X-Custom-Header", value(consumer("121345"), producer(regex("^.*2134.*\$"))))
}
}
}
assertDoesNotThrow {
Contract.assertContract(contract)
}.also {
val input = contract.input
assertThat(input.messageFrom.clientValue).isEqualTo("input")
assertThat(input.messageFrom.serverValue).isEqualTo("input")
assertThat(input.messageBody.clientValue).isEqualTo(mapOf("foo" to "bar"))
assertThat(input.messageBody.serverValue).isEqualTo(mapOf("foo" to "bar"))
val headers = input.messageHeaders.entries
assertThat(headers).hasSize(2)
assertThat(headers.elementAt(0).name).isEqualTo("foo")
assertThat(headers.elementAt(0).clientValue).isEqualTo("bar")
assertThat(headers.elementAt(0).serverValue).isEqualTo("bar")
assertThat(headers.elementAt(1).name).isEqualTo("X-Custom-Header")
assertThat(headers.elementAt(1).clientValue).isInstanceOf(RegexProperty::class.java)
assertThat((headers.elementAt(1).clientValue as RegexProperty).pattern()).isEqualTo("^.*2134.*\$")
assertThat(headers.elementAt(1).serverValue).isEqualTo("121345")
}.also {
val output = contract.outputMessage
assertThat(output.sentTo.clientValue).isEqualTo("output")
assertThat(output.sentTo.serverValue).isEqualTo("output")
assertThat(output.body.clientValue).isEqualTo(mapOf("foo" to "bar",
"foo2" to "bar2"
))
assertThat(output.body.serverValue).isEqualTo(mapOf("foo" to "bar",
"foo2" to "bar2"
))
val headers = output.headers.entries
assertThat(headers).hasSize(2)
assertThat(headers.elementAt(0).name).isEqualTo("foo2")
assertThat(headers.elementAt(0).clientValue).isEqualTo("bar")
assertThat(headers.elementAt(0).serverValue).isEqualTo("bar")
assertThat(headers.elementAt(1).name).isEqualTo("X-Custom-Header")
assertThat(headers.elementAt(1).clientValue).isEqualTo("121345")
assertThat(headers.elementAt(1).serverValue).isInstanceOf(RegexProperty::class.java)
assertThat((headers.elementAt(1).serverValue as RegexProperty).pattern()).isEqualTo("^.*2134.*\$")
}
}
@Test
fun `should work for messaging with pattern properties`() {
val contract = contract {
input {
messageFrom("input")
messageBody("foo" to anyNonBlankString)
headers {
header("foo", anyNumber)
}
}
outputMessage {
sentTo("output")
body("foo2" to anyNonEmptyString)
headers {
header("foo2", anyIpAddress)
}
}
}
assertDoesNotThrow {
Contract.assertContract(contract)
}
}
@Test
fun `should set a description`() {
val contract =
@@ -888,4 +804,4 @@ then:
}
}
}
}

View File

@@ -101,65 +101,6 @@ class ContractSpec extends Specification {
ex.message.contains("Status is missing for HTTP contract")
}
def 'should work for messaging'() {
when:
Contract.make {
input {
messageFrom('input')
messageBody([
foo: 'bar'
])
messageHeaders {
header([
foo: 'bar'
])
}
}
outputMessage {
sentTo('output')
body([
foo2: 'bar'
])
headers {
header([
foo2: 'bar'
])
}
}
}
then:
noExceptionThrown()
}
def 'should work for messaging with pattern properties'() {
when:
Contract.make {
input {
messageFrom('input')
messageBody([
foo: anyNonBlankString()
])
messageHeaders {
header([
foo: anyNumber()
])
}
}
outputMessage {
sentTo('output')
body([
foo2: anyNonEmptyString()
])
headers {
header([
foo2: anyIpAddress()
])
}
}
}
then:
noExceptionThrown()
}
def 'should set a description'() {
given:

View File

@@ -57,7 +57,7 @@ class StubRunnerExecutor implements StubFinder {
private final AvailablePortScanner portScanner;
private final MessageVerifierSender<?> contractVerifierMessaging;
private final MessageVerifierSender<?> messageVerifierSender;
private final List<HttpServerStub> serverStubs;
@@ -65,10 +65,10 @@ class StubRunnerExecutor implements StubFinder {
private final YamlContractConverter yamlContractConverter = new YamlContractConverter();
StubRunnerExecutor(AvailablePortScanner portScanner, MessageVerifierSender<?> contractVerifierMessaging,
StubRunnerExecutor(AvailablePortScanner portScanner, MessageVerifierSender<?> messageVerifierSender,
List<HttpServerStub> serverStubs) {
this.portScanner = portScanner;
this.contractVerifierMessaging = contractVerifierMessaging;
this.messageVerifierSender = messageVerifierSender;
this.serverStubs = serverStubs;
}
@@ -249,7 +249,7 @@ class StubRunnerExecutor implements StubFinder {
YamlContract contract = yamlContracts.get(0);
setMessageType(contract, ContractVerifierMessageMetadata.MessageType.OUTPUT);
// TODO: Json is harcoded here
this.contractVerifierMessaging.send(
this.messageVerifierSender.send(
JsonOutput
.toJson(BodyExtractor.extractClientValueFromBody(body == null ? null : body.getClientValue())),
headers == null ? null : headers.asStubSideMap(), outputMessage.getSentTo().getClientValue(), contract);

View File

@@ -1,50 +0,0 @@
/*
* 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;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.annotation.Configuration;
/**
* Supports
* {@link org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner} by
* loading in AutoConfigurations related to Stream and Integration only if the relevant
* jars are in classpath.
*
* @author Biju Kunjummen
*/
@Configuration(proxyBeanMethods = false)
public class StubRunnerStreamsIntegrationAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(TestChannelBinderConfiguration.class)
@ImportAutoConfiguration(classes = TestChannelBinderConfiguration.class)
static class StreamsRelatedAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(IntegrationAutoConfiguration.class)
@ImportAutoConfiguration(classes = { IntegrationAutoConfiguration.class })
static class IntegrationRelatedAutoConfiguration {
}
}

View File

@@ -1,101 +0,0 @@
/*
* 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 java.util.Collection;
import java.util.List;
import java.util.Map;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.RoutesBuilder;
import org.apache.camel.builder.RouteBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Camel configuration that iterates over the downloaded Groovy DSLs and registers a route
* for each DSL.
*
* @author Marcin Grzejszczak
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RoutesBuilder.class)
@ConditionalOnProperty(name = "stubrunner.camel.enabled", havingValue = "true", matchIfMissing = true)
public class StubRunnerCamelConfiguration {
static final String STUBRUNNER_DESTINATION_URL_HEADER_NAME = "STUBRUNNER_DESTINATION_URL";
@Bean
public RoutesBuilder myRouter(final BatchStubRunner batchStubRunner) {
return new RouteBuilder() {
@Override
public void configure() throws Exception {
Map<StubConfiguration, Collection<Contract>> contracts = batchStubRunner.getContracts();
for (Map.Entry<StubConfiguration, Collection<Contract>> entry : contracts.entrySet()) {
Collection<Contract> value = entry.getValue();
MultiValueMap<String, Contract> map = new LinkedMultiValueMap<>();
for (Contract dsl : value) {
if (dsl == null) {
continue;
}
if (dsl.getInput() != null && dsl.getInput().getMessageFrom() != null
&& StringUtils.hasText(dsl.getInput().getMessageFrom().getClientValue())) {
String from = dsl.getInput().getMessageFrom().getClientValue();
map.add(from, dsl);
}
}
for (Map.Entry<String, List<Contract>> entries : map.entrySet()) {
from(entries.getKey()).filter(new StubRunnerCamelPredicate(entries.getValue()))
.process(new StubRunnerCamelProcessor()).dynamicRouter(
header(StubRunnerCamelConfiguration.STUBRUNNER_DESTINATION_URL_HEADER_NAME));
}
}
}
};
}
@Bean
DummyProcessor dummyStubRunnerProcessor() {
return new DummyProcessor();
}
private static class DummyProcessor implements Processor {
private static final Log log = LogFactory.getLog(DummyProcessor.class);
@Override
public void process(Exchange exchange) {
if (log.isTraceEnabled()) {
log.trace("Got exchange [" + exchange + "]");
}
}
}
}

View File

@@ -1,35 +0,0 @@
/*
* 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 org.springframework.cloud.contract.spec.Contract;
/**
* @author Marcin Grzejszczak
*/
class StubRunnerCamelPayload {
final Object payload;
final Contract contract;
StubRunnerCamelPayload(Contract contract) {
this.contract = contract;
this.payload = null;
}
}

View File

@@ -1,226 +0,0 @@
/*
* 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 java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.toomuchcoding.jsonassert.JsonAssertion;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.FromFileProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.spec.internal.RegexProperty;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.util.ContentType;
import org.springframework.cloud.contract.verifier.util.ContentUtils;
import org.springframework.cloud.contract.verifier.util.JsonPaths;
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
import org.springframework.cloud.contract.verifier.util.MapConverter;
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
/**
* Passes through a message that matches the one defined in the DSL.
*
* @author Marcin Grzejszczak
*/
class StubRunnerCamelPredicate implements Predicate {
private static final Log log = LogFactory.getLog(StubRunnerCamelPredicate.class);
private final List<Contract> groovyDsls;
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
StubRunnerCamelPredicate(List<Contract> groovyDsls) {
this.groovyDsls = groovyDsls;
}
@Override
public boolean matches(Exchange exchange) {
Contract contract = getContract(exchange.getMessage());
if (log.isDebugEnabled()) {
log.debug("For exchange [" + exchange + "] found contract [" + contract + "]");
}
if (contract == null) {
return false;
}
exchange.getIn().setBody(new StubRunnerCamelPayload(contract));
return true;
}
private Contract getContract(Message message) {
for (Contract groovyDsl : this.groovyDsls) {
Contract contract = matchContract(message, groovyDsl);
if (contract != null) {
return contract;
}
}
return null;
}
private Contract matchContract(Message message, Contract groovyDsl) {
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
if (!unmatchedHeaders.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
}
return null;
}
Object inputMessage = message.getBody();
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
if (dslBody instanceof FromFileProperty) {
if (log.isDebugEnabled()) {
log.debug("Will compare file content");
}
FromFileProperty property = (FromFileProperty) dslBody;
if (property.isString()) {
// continue processing as if body was pure string
dslBody = property.asString();
}
else if (!(inputMessage instanceof byte[])) {
if (log.isDebugEnabled()) {
log.debug("Contract provided byte comparison, but the input message is of type ["
+ inputMessage.getClass() + "]. Can't compare the two.");
}
return null;
}
else {
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
if (log.isDebugEnabled() && !matches) {
log.debug("Contract provided byte comparison, but the byte arrays don't match");
}
return matches ? groovyDsl : null;
}
}
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
return groovyDsl;
}
return null;
}
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
boolean matches;
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
if (type == ContentType.JSON) {
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
}
else if (dslBody instanceof RegexProperty && inputMessage instanceof String) {
Pattern pattern = ((RegexProperty) dslBody).getPattern();
matches = pattern.matcher((String) inputMessage).matches();
bodyUnmatchedLog(dslBody, matches, pattern);
}
else {
matches = dslBody.equals(inputMessage);
bodyUnmatchedLog(dslBody, matches, inputMessage);
}
return matches;
}
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
if (log.isDebugEnabled() && !matches) {
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
+ "]");
}
}
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
Object dslBody) {
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
JsonPaths jsonPaths = JsonToJsonPathsConverter
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
DocumentContext parsedJson;
try {
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
}
catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot serialize to JSON", e);
}
List<String> unmatchedJsonPath = new ArrayList<>();
boolean matches = true;
for (MethodBufferingJsonVerifiable path : jsonPaths) {
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
}
if (matchers != null && matchers.hasMatchers()) {
for (BodyMatcher matcher : matchers.matchers()) {
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
}
}
if (!unmatchedJsonPath.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
}
}
return matches;
}
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
try {
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
return true;
}
catch (Exception e) {
unmatchedJsonPath.add(e.getLocalizedMessage());
return false;
}
}
private List<String> headersMatch(Message message, Contract groovyDsl) {
List<String> unmatchedHeaders = new ArrayList<>();
Map<String, Object> headers = message.getHeaders();
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
String name = it.getName();
Object value = it.getClientValue();
Object valueInHeader = headers.get(name);
boolean matches;
if (value instanceof RegexProperty) {
Pattern pattern = ((RegexProperty) value).getPattern();
matches = pattern.matcher(valueInHeader.toString()).matches();
}
else {
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
}
if (!matches) {
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
}
}
return unmatchedHeaders;
}
private String unmatchedText(Object expectedValue) {
return expectedValue instanceof RegexProperty
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
: "be equal to [" + expectedValue + "]";
}
}

View File

@@ -1,88 +0,0 @@
/*
* 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 org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.Processor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
/**
* 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 static final Log log = LogFactory.getLog(StubRunnerCamelProcessor.class);
private static final String DUMMY_BEAN_URL = "bean:dummyStubRunnerProcessor";
@Override
public void process(Exchange exchange) {
Message input = exchange.getIn();
StubRunnerCamelPayload body = input.getBody(StubRunnerCamelPayload.class);
Contract groovyDsl = body.contract;
setStubRunnerDestinationHeader(exchange, body);
if (groovyDsl.getInput().getMessageHeaders() != null) {
for (Header entry : groovyDsl.getInput().getMessageHeaders().getEntries()) {
input.removeHeader(entry.getName());
}
}
if (groovyDsl.getOutputMessage() == null) {
if (log.isDebugEnabled()) {
log.debug("No output message provided, will not modify the body");
}
return;
}
input.setBody(outputBody(groovyDsl));
if (groovyDsl.getOutputMessage().getHeaders() != null) {
for (Header entry : groovyDsl.getOutputMessage().getHeaders().getEntries()) {
input.setHeader(entry.getName(), entry.getClientValue());
}
}
}
private Object outputBody(Contract groovyDsl) {
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
if (outputBody instanceof FromFileProperty) {
FromFileProperty property = (FromFileProperty) outputBody;
return property.asBytes();
}
return BodyExtractor.extractStubValueFrom(outputBody);
}
private void setStubRunnerDestinationHeader(Exchange exchange, StubRunnerCamelPayload body) {
boolean outputPart = body.contract.getOutputMessage() != null;
String url = DUMMY_BEAN_URL;
if (outputPart && body.contract.getOutputMessage().getSentTo() != null) {
url = body.contract.getOutputMessage().getSentTo().getClientValue();
}
exchange.getIn().setHeader(StubRunnerCamelConfiguration.STUBRUNNER_DESTINATION_URL_HEADER_NAME, url);
if (log.isDebugEnabled()) {
log.debug("Set stub runner destination header to [" + url + "]");
}
}
}

View File

@@ -1,109 +0,0 @@
/*
* 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.integration;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Consumer;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.Lifecycle;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.FilterEndpointSpec;
import org.springframework.integration.dsl.IntegrationFlowBuilder;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.messaging.Message;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Spring Integration configuration that iterates over the downloaded Groovy DSLs and
* registers a flow for each DSL.
*
* @author Marcin Grzejszczak
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(IntegrationFlowBuilder.class)
@ConditionalOnProperty(name = "stubrunner.integration.enabled", havingValue = "true", matchIfMissing = true)
public class StubRunnerIntegrationConfiguration {
@Bean
@ConditionalOnMissingBean(name = "stubFlowRegistrar")
public FlowRegistrar stubFlowRegistrar(AutowireCapableBeanFactory beanFactory, BatchStubRunner batchStubRunner) {
Map<StubConfiguration, Collection<Contract>> contracts = batchStubRunner.getContracts();
IntegrationFlowBuilder dummyBuilder = IntegrationFlows.from(DummyMessageHandler.CHANNEL_NAME)
.handle(new DummyMessageHandler(), "handle");
beanFactory.initializeBean(dummyBuilder.get(), DummyMessageHandler.CHANNEL_NAME + ".flow");
for (Entry<StubConfiguration, Collection<Contract>> entry : contracts.entrySet()) {
StubConfiguration key = entry.getKey();
Collection<Contract> value = entry.getValue();
String name = key.getGroupId() + "_" + key.getArtifactId();
MultiValueMap<String, Contract> map = new LinkedMultiValueMap<>();
for (Contract dsl : value) {
if (dsl == null) {
continue;
}
if (dsl.getInput() != null && dsl.getInput().getMessageFrom() != null
&& StringUtils.hasText(dsl.getInput().getMessageFrom().getClientValue())) {
String from = dsl.getInput().getMessageFrom().getClientValue();
map.add(from, dsl);
}
}
for (Entry<String, List<Contract>> entries : map.entrySet()) {
final String flowName = name + "_" + entries.getKey() + "_" + entries.getValue().hashCode();
IntegrationFlowBuilder builder = IntegrationFlows.from(entries.getKey())
.filter(new StubRunnerIntegrationMessageSelector(entries.getValue()),
new Consumer<FilterEndpointSpec>() {
@Override
public void accept(FilterEndpointSpec e) {
e.id(flowName + ".filter");
}
})
.transform(new StubRunnerIntegrationTransformer(entries.getValue()))
.route(new StubRunnerIntegrationRouter(entries.getValue(), beanFactory));
beanFactory.initializeBean(builder.get(), flowName);
beanFactory.getBean(flowName + ".filter", Lifecycle.class).start();
}
}
return new FlowRegistrar();
}
static class DummyMessageHandler {
static String CHANNEL_NAME = "stub_runner_dummy_channel";
public void handle(Message<?> message) {
}
}
static class FlowRegistrar {
}
}

View File

@@ -1,241 +0,0 @@
/*
* 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.integration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.toomuchcoding.jsonassert.JsonAssertion;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.FromFileProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.spec.internal.RegexProperty;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.util.ContentType;
import org.springframework.cloud.contract.verifier.util.ContentUtils;
import org.springframework.cloud.contract.verifier.util.JsonPaths;
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
import org.springframework.cloud.contract.verifier.util.MapConverter;
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
import org.springframework.integration.core.MessageSelector;
import org.springframework.messaging.Message;
/**
* Passes through a message that matches the one defined in the DSL.
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
class StubRunnerIntegrationMessageSelector implements MessageSelector {
private static final Map<Message, Contract> CACHE = Collections.synchronizedMap(new WeakHashMap<>());
private static final Log log = LogFactory.getLog(StubRunnerIntegrationMessageSelector.class);
private final List<Contract> groovyDsls;
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
StubRunnerIntegrationMessageSelector(Contract groovyDsl) {
this(Collections.singletonList(groovyDsl));
}
StubRunnerIntegrationMessageSelector(List<Contract> groovyDsls) {
this.groovyDsls = groovyDsls;
}
@Override
public boolean accept(Message<?> message) {
return matchingContract(message) != null;
}
Contract matchingContract(Message<?> message) {
if (CACHE.containsKey(message)) {
return CACHE.get(message);
}
Contract contract = getContract(message);
if (contract != null) {
CACHE.put(message, contract);
}
return contract;
}
void updateCache(Message<?> message, Contract contract) {
CACHE.put(message, contract);
}
private Contract getContract(Message<?> message) {
for (Contract groovyDsl : this.groovyDsls) {
Contract contract = matchContract(message, groovyDsl);
if (contract != null) {
return contract;
}
}
return null;
}
private Contract matchContract(Message<?> message, Contract groovyDsl) {
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
if (!unmatchedHeaders.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
}
return null;
}
Object inputMessage = message.getPayload();
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
if (dslBody instanceof FromFileProperty) {
if (log.isDebugEnabled()) {
log.debug("Will compare file content");
}
FromFileProperty property = (FromFileProperty) dslBody;
if (property.isString()) {
// continue processing as if body was pure string
dslBody = property.asString();
}
else if (!(inputMessage instanceof byte[])) {
if (log.isDebugEnabled()) {
log.debug("Contract provided byte comparison, but the input message is of type ["
+ inputMessage.getClass() + "]. Can't compare the two.");
}
return null;
}
else {
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
if (log.isDebugEnabled() && !matches) {
log.debug("Contract provided byte comparison, but the byte arrays don't match");
}
return matches ? groovyDsl : null;
}
}
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
return groovyDsl;
}
return null;
}
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
boolean matches;
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
if (type == ContentType.JSON) {
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
}
else if (dslBody instanceof RegexProperty && inputMessage instanceof String) {
Pattern pattern = ((RegexProperty) dslBody).getPattern();
matches = pattern.matcher((String) inputMessage).matches();
bodyUnmatchedLog(dslBody, matches, pattern);
}
else {
matches = dslBody.equals(inputMessage);
bodyUnmatchedLog(dslBody, matches, inputMessage);
}
return matches;
}
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
if (log.isDebugEnabled() && !matches) {
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
+ "]");
}
}
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
Object dslBody) {
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
JsonPaths jsonPaths = JsonToJsonPathsConverter
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
DocumentContext parsedJson;
try {
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
}
catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot serialize to JSON", e);
}
List<String> unmatchedJsonPath = new ArrayList<>();
boolean matches = true;
for (MethodBufferingJsonVerifiable path : jsonPaths) {
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
}
if (matchers != null && matchers.hasMatchers()) {
for (BodyMatcher matcher : matchers.matchers()) {
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
}
}
if (!unmatchedJsonPath.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
}
}
return matches;
}
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
try {
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
return true;
}
catch (Exception e) {
unmatchedJsonPath.add(e.getLocalizedMessage());
return false;
}
}
private List<String> headersMatch(Message<?> message, Contract groovyDsl) {
List<String> unmatchedHeaders = new ArrayList<>();
Map<String, Object> headers = message.getHeaders();
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
String name = it.getName();
Object value = it.getClientValue();
Object valueInHeader = headers.get(name);
boolean matches;
if (value instanceof RegexProperty || value instanceof Pattern) {
Pattern pattern = new RegexProperty(value).getPattern();
matches = pattern.matcher(valueInHeader.toString()).matches();
}
else {
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
}
if (!matches) {
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
}
}
return unmatchedHeaders;
}
private String unmatchedText(Object expectedValue) {
return expectedValue instanceof RegexProperty
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
: "be equal to [" + expectedValue + "]";
}
}

View File

@@ -1,54 +0,0 @@
/*
* 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.integration;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.integration.router.AbstractMessageRouter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
/**
* @author Marcin Grzejszczak
*/
class StubRunnerIntegrationRouter extends AbstractMessageRouter {
private final StubRunnerIntegrationMessageSelector selector;
private final BeanFactory beanFactory;
StubRunnerIntegrationRouter(List<Contract> groovyDsls, BeanFactory beanFactory) {
this.selector = new StubRunnerIntegrationMessageSelector(groovyDsls);
this.beanFactory = beanFactory;
}
@Override
protected Collection<MessageChannel> determineTargetChannels(Message<?> message) {
Contract dsl = this.selector.matchingContract(message);
if (dsl != null && dsl.getOutputMessage() != null && dsl.getOutputMessage().getSentTo() != null) {
String channelName = dsl.getOutputMessage().getSentTo().getClientValue();
return Collections.singleton((MessageChannel) this.beanFactory.getBean(channelName));
}
return Collections.singleton((MessageChannel) this.beanFactory
.getBean(StubRunnerIntegrationConfiguration.DummyMessageHandler.CHANNEL_NAME));
}
}

View File

@@ -1,73 +0,0 @@
/*
* 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.integration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
/**
* Sends forward a message defined in the DSL.
*
* @author Marcin Grzejszczak
*/
class StubRunnerIntegrationTransformer {
private final StubRunnerIntegrationMessageSelector selector;
StubRunnerIntegrationTransformer(Contract groovyDsl) {
this(Collections.singletonList(groovyDsl));
}
StubRunnerIntegrationTransformer(List<Contract> groovyDsls) {
this.selector = new StubRunnerIntegrationMessageSelector(groovyDsls);
}
public Message<?> transform(Message<?> source) {
Contract groovyDsl = matchingContract(source);
if (groovyDsl == null || groovyDsl.getOutputMessage() == null) {
return source;
}
Object outputBody = outputBody(groovyDsl);
Map<String, Object> headers = groovyDsl.getOutputMessage().getHeaders().asStubSideMap();
MessageHeaders messageHeaders = new MessageHeaders(headers);
Message<Object> message = MessageBuilder.createMessage(outputBody, messageHeaders);
this.selector.updateCache(message, groovyDsl);
return message;
}
private Object outputBody(Contract groovyDsl) {
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
if (outputBody instanceof FromFileProperty) {
FromFileProperty property = (FromFileProperty) outputBody;
return property.asBytes();
}
return BodyExtractor.extractStubValueFrom(outputBody);
}
Contract matchingContract(Message<?> source) {
return this.selector.matchingContract(source);
}
}

View File

@@ -1,84 +0,0 @@
/*
* 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.jms;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.ObjectMessage;
import jakarta.jms.StreamMessage;
import jakarta.jms.TextMessage;
final class StubRunnerJmsAccessor {
private StubRunnerJmsAccessor() {
throw new IllegalStateException("Can't instantiate an utility class");
}
static Object getBody(Message message) {
try {
return getPayload(message);
}
catch (JMSException ex) {
throw new IllegalStateException(ex);
}
}
static Map<String, Object> getHeaders(Message message) {
try {
return headers(message);
}
catch (JMSException ex) {
throw new IllegalStateException(ex);
}
}
private static Map<String, Object> headers(Message message) throws JMSException {
Map<String, Object> headers = new HashMap<>();
if (message == null) {
return headers;
}
Enumeration enumeration = message.getPropertyNames();
while (enumeration.hasMoreElements()) {
Object element = enumeration.nextElement();
String asString = element.toString();
Object property = message.getObjectProperty(asString);
headers.put(asString, property);
}
return headers;
}
private static Object getPayload(Message message) throws JMSException {
if (message == null) {
return null;
}
else if (message instanceof TextMessage) {
return ((TextMessage) message).getText();
}
else if (message instanceof StreamMessage) {
return ((StreamMessage) message).readObject();
}
else if (message instanceof ObjectMessage) {
return ((ObjectMessage) message).getObject();
}
return message.getBody(Object.class);
}
}

View File

@@ -1,119 +0,0 @@
/*
* 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.jms;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import jakarta.jms.ConnectionFactory;
import jakarta.jms.MessageListener;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.cloud.contract.verifier.util.MapConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.listener.DefaultMessageListenerContainer;
import org.springframework.jms.listener.MessageListenerContainer;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Spring Integration configuration that iterates over the downloaded Groovy DSLs and
* registers a flow for each DSL.
*
* @author Marcin Grzejszczak
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(JmsTemplate.class)
@ConditionalOnProperty(name = "stubrunner.jms.enabled", havingValue = "true", matchIfMissing = true)
public class StubRunnerJmsConfiguration {
@Bean
@ConditionalOnMissingBean(name = "stubFlowRegistrar")
public FlowRegistrar stubFlowRegistrar(ConfigurableListableBeanFactory beanFactory,
BatchStubRunner batchStubRunner) {
Map<StubConfiguration, Collection<Contract>> contracts = batchStubRunner.getContracts();
for (Entry<StubConfiguration, Collection<Contract>> entry : contracts.entrySet()) {
StubConfiguration key = entry.getKey();
Collection<Contract> value = entry.getValue();
String name = key.getGroupId() + "_" + key.getArtifactId();
MultiValueMap<String, Contract> map = new LinkedMultiValueMap<>();
for (Contract dsl : value) {
if (dsl == null) {
continue;
}
if (dsl.getInput() != null && dsl.getInput().getMessageFrom() != null
&& StringUtils.hasText(dsl.getInput().getMessageFrom().getClientValue())) {
String from = dsl.getInput().getMessageFrom().getClientValue();
map.add(from, dsl);
}
}
for (Entry<String, List<Contract>> entries : map.entrySet()) {
List<Contract> matchingContracts = entries.getValue();
final String flowName = name + "_" + entries.getKey() + "_" + Math.abs(matchingContracts.hashCode());
// listener
StubRunnerJmsRouter router = new StubRunnerJmsRouter(matchingContracts, beanFactory);
StubRunnerJmsRouter listener = (StubRunnerJmsRouter) beanFactory.initializeBean(router, flowName);
beanFactory.registerSingleton(flowName, listener);
registerContainers(beanFactory, matchingContracts, flowName, listener);
}
}
return new FlowRegistrar();
}
private void registerContainers(ConfigurableListableBeanFactory beanFactory, List<Contract> matchingContracts,
String flowName, StubRunnerJmsRouter listener) {
// listener's container
ConnectionFactory connectionFactory = beanFactory.getBean(ConnectionFactory.class);
for (Contract matchingContract : matchingContracts) {
if (matchingContract.getInput() == null) {
continue;
}
String destination = MapConverter.getStubSideValuesForNonBody(matchingContract.getInput().getMessageFrom())
.toString();
MessageListenerContainer container = listenerContainer(destination, connectionFactory, listener);
String containerName = flowName + ".container";
Object initializedContainer = beanFactory.initializeBean(container, containerName);
beanFactory.registerSingleton(containerName, initializedContainer);
}
}
private MessageListenerContainer listenerContainer(String queueName, ConnectionFactory connectionFactory,
MessageListener listener) {
DefaultMessageListenerContainer container = new DefaultMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setDestinationName(queueName);
container.setMessageListener(listener);
return container;
}
static class FlowRegistrar {
}
}

View File

@@ -1,231 +0,0 @@
/*
* 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.jms;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.toomuchcoding.jsonassert.JsonAssertion;
import jakarta.jms.Message;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.FromFileProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.spec.internal.RegexProperty;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.util.ContentType;
import org.springframework.cloud.contract.verifier.util.ContentUtils;
import org.springframework.cloud.contract.verifier.util.JsonPaths;
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
import org.springframework.cloud.contract.verifier.util.MapConverter;
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
/**
* Passes through a message that matches the one defined in the DSL.
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
class StubRunnerJmsMessageSelector {
private static final Map<Message, Contract> CACHE = Collections.synchronizedMap(new WeakHashMap<>());
private static final Log log = LogFactory.getLog(StubRunnerJmsMessageSelector.class);
private final List<Contract> groovyDsls;
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
StubRunnerJmsMessageSelector(List<Contract> groovyDsls) {
this.groovyDsls = groovyDsls;
}
Contract matchingContract(Message message) {
if (CACHE.containsKey(message)) {
return CACHE.get(message);
}
Contract contract = getContract(message);
if (contract != null) {
CACHE.put(message, contract);
}
return contract;
}
void updateCache(Message message, Contract contract) {
CACHE.put(message, contract);
}
private Contract getContract(Message message) {
for (Contract groovyDsl : this.groovyDsls) {
Contract contract = matchContract(message, groovyDsl);
if (contract != null) {
return contract;
}
}
return null;
}
private Contract matchContract(Message message, Contract groovyDsl) {
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
if (!unmatchedHeaders.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
}
return null;
}
Object inputMessage = StubRunnerJmsAccessor.getBody(message);
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
if (dslBody instanceof FromFileProperty) {
if (log.isDebugEnabled()) {
log.debug("Will compare file content");
}
FromFileProperty property = (FromFileProperty) dslBody;
if (property.isString()) {
// continue processing as if body was pure string
dslBody = property.asString();
}
else if (!(inputMessage instanceof byte[])) {
if (log.isDebugEnabled()) {
log.debug("Contract provided byte comparison, but the input message is of type ["
+ inputMessage.getClass() + "]. Can't compare the two.");
}
return null;
}
else {
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
if (log.isDebugEnabled() && !matches) {
log.debug("Contract provided byte comparison, but the byte arrays don't match");
}
return matches ? groovyDsl : null;
}
}
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
return groovyDsl;
}
return null;
}
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
boolean matches;
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
if (type == ContentType.JSON) {
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
}
else if (dslBody instanceof RegexProperty && inputMessage instanceof String) {
Pattern pattern = ((RegexProperty) dslBody).getPattern();
matches = pattern.matcher((String) inputMessage).matches();
bodyUnmatchedLog(dslBody, matches, pattern);
}
else {
matches = dslBody.equals(inputMessage);
bodyUnmatchedLog(dslBody, matches, inputMessage);
}
return matches;
}
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
if (log.isDebugEnabled() && !matches) {
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
+ "]");
}
}
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
Object dslBody) {
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
JsonPaths jsonPaths = JsonToJsonPathsConverter
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
DocumentContext parsedJson;
try {
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
}
catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot serialize to JSON", e);
}
List<String> unmatchedJsonPath = new ArrayList<>();
boolean matches = true;
for (MethodBufferingJsonVerifiable path : jsonPaths) {
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
}
if (matchers != null && matchers.hasMatchers()) {
for (BodyMatcher matcher : matchers.matchers()) {
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
}
}
if (!unmatchedJsonPath.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
}
}
return matches;
}
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
try {
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
return true;
}
catch (Exception e) {
unmatchedJsonPath.add(e.getLocalizedMessage());
return false;
}
}
private List<String> headersMatch(Message message, Contract groovyDsl) {
List<String> unmatchedHeaders = new ArrayList<>();
Map<String, Object> headers = StubRunnerJmsAccessor.getHeaders(message);
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
String name = it.getName();
Object value = it.getClientValue();
Object valueInHeader = headers.get(name);
boolean matches;
if (value instanceof RegexProperty) {
Pattern pattern = ((RegexProperty) value).getPattern();
matches = pattern.matcher(valueInHeader.toString()).matches();
}
else {
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
}
if (!matches) {
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
}
}
return unmatchedHeaders;
}
private String unmatchedText(Object expectedValue) {
return expectedValue instanceof RegexProperty
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
: "be equal to [" + expectedValue + "]";
}
}

View File

@@ -1,76 +0,0 @@
/*
* 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.jms;
import java.util.List;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.MessageListener;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessagePostProcessor;
/**
* @author Marcin Grzejszczak
*/
class StubRunnerJmsRouter implements MessageListener {
private final StubRunnerJmsMessageSelector selector;
private final BeanFactory beanFactory;
private final List<Contract> contracts;
private JmsTemplate jmsTemplate;
StubRunnerJmsRouter(List<Contract> groovyDsls, BeanFactory beanFactory) {
this.selector = new StubRunnerJmsMessageSelector(groovyDsls);
this.beanFactory = beanFactory;
this.contracts = groovyDsls;
}
@Override
public void onMessage(jakarta.jms.Message message) {
Contract dsl = this.selector.matchingContract(message);
if (dsl != null && dsl.getOutputMessage() != null && dsl.getOutputMessage().getSentTo() != null) {
String destination = dsl.getOutputMessage().getSentTo().getClientValue();
jmsTemplate().send(destination,
session -> new StubRunnerJmsTransformer(this.contracts).transform(session, dsl));
}
}
private JmsTemplate jmsTemplate() {
if (this.jmsTemplate == null) {
this.jmsTemplate = this.beanFactory.getBean(JmsTemplate.class);
}
return this.jmsTemplate;
}
}
class ReplyToProcessor implements MessagePostProcessor {
@Override
public jakarta.jms.Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("requiresReply", "no");
return message;
}
}

View File

@@ -1,108 +0,0 @@
/*
* 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.jms;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import jakarta.jms.BytesMessage;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.Session;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
/**
* Sends forward a message defined in the DSL.
*
* @author Marcin Grzejszczak
*/
class StubRunnerJmsTransformer {
private final StubRunnerJmsMessageSelector selector;
StubRunnerJmsTransformer(List<Contract> groovyDsls) {
this.selector = new StubRunnerJmsMessageSelector(groovyDsls);
}
public Message transform(Session session, Contract groovyDsl) {
Object outputBody = outputBody(groovyDsl);
Map<String, Object> headers = groovyDsl.getOutputMessage().getHeaders().asStubSideMap();
Message newMessage = createMessage(session, outputBody);
setHeaders(newMessage, headers);
this.selector.updateCache(newMessage, groovyDsl);
return newMessage;
}
private Object outputBody(Contract groovyDsl) {
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
if (outputBody instanceof FromFileProperty) {
FromFileProperty property = (FromFileProperty) outputBody;
return property.asBytes();
}
return BodyExtractor.extractStubValueFrom(outputBody);
}
Contract matchingContract(Message source) {
return this.selector.matchingContract(source);
}
private Message createMessage(Session session, Object payload) {
try {
if (payload instanceof String) {
return session.createTextMessage((String) payload);
}
else if (payload instanceof byte[]) {
BytesMessage bytesMessage = session.createBytesMessage();
bytesMessage.writeBytes((byte[]) payload);
return bytesMessage;
}
else if (payload instanceof Serializable) {
return session.createObjectMessage((Serializable) payload);
}
return session.createMessage();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
private void setHeaders(Message message, Map<String, Object> headers) {
for (Map.Entry<String, Object> entry : headers.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
try {
if (value instanceof String) {
message.setStringProperty(key, (String) value);
}
else if (value instanceof Boolean) {
message.setBooleanProperty(key, (Boolean) value);
}
else {
message.setObjectProperty(key, value);
}
}
catch (JMSException ex) {
throw new IllegalStateException(ex);
}
}
}
}

View File

@@ -1,133 +0,0 @@
/*
* 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.kafka;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.cloud.contract.verifier.util.MapConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.listener.ContainerProperties;
import org.springframework.kafka.listener.GenericMessageListener;
import org.springframework.kafka.listener.KafkaMessageListenerContainer;
import org.springframework.kafka.test.EmbeddedKafkaBroker;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Spring Integration configuration that iterates over the downloaded Groovy DSLs and
* registers a flow for each DSL.
*
* @author Marcin Grzejszczak
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ KafkaTemplate.class, EmbeddedKafkaBroker.class })
@ConditionalOnProperty(name = "stubrunner.kafka.enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnBean(EmbeddedKafkaBroker.class)
public class StubRunnerKafkaConfiguration {
private static final Log log = LogFactory.getLog(StubRunnerKafkaConfiguration.class);
@Bean
@ConditionalOnMissingBean(name = "stubFlowRegistrar")
public FlowRegistrar stubFlowRegistrar(ConfigurableListableBeanFactory beanFactory,
BatchStubRunner batchStubRunner) {
Map<StubConfiguration, Collection<Contract>> contracts = batchStubRunner.getContracts();
for (Entry<StubConfiguration, Collection<Contract>> entry : contracts.entrySet()) {
StubConfiguration key = entry.getKey();
Collection<Contract> value = entry.getValue();
String name = key.getGroupId() + "_" + key.getArtifactId();
MultiValueMap<String, Contract> map = new LinkedMultiValueMap<>();
for (Contract dsl : value) {
if (dsl == null) {
continue;
}
if (dsl.getInput() != null && dsl.getInput().getMessageFrom() != null
&& StringUtils.hasText(dsl.getInput().getMessageFrom().getClientValue())) {
String from = dsl.getInput().getMessageFrom().getClientValue();
map.add(from, dsl);
}
}
for (Entry<String, List<Contract>> entries : map.entrySet()) {
List<Contract> matchingContracts = entries.getValue();
final String flowName = name + "_" + entries.getKey() + "_" + Math.abs(matchingContracts.hashCode());
// listener
StubRunnerKafkaRouter router = new StubRunnerKafkaRouter(matchingContracts, beanFactory);
StubRunnerKafkaRouter listener = (StubRunnerKafkaRouter) beanFactory.initializeBean(router, flowName);
if (log.isDebugEnabled()) {
log.debug("Initialized kafka router with name [" + flowName + "]");
}
beanFactory.registerSingleton(flowName, listener);
registerContainers(beanFactory, matchingContracts, flowName, listener);
}
}
return new FlowRegistrar();
}
private void registerContainers(ConfigurableListableBeanFactory beanFactory, List<Contract> matchingContracts,
String flowName, StubRunnerKafkaRouter listener) {
// listener's container
ConsumerFactory consumerFactory = beanFactory.getBean(ConsumerFactory.class);
for (Contract matchingContract : matchingContracts) {
if (matchingContract.getInput() == null) {
continue;
}
String destination = MapConverter.getStubSideValuesForNonBody(matchingContract.getInput().getMessageFrom())
.toString();
ContainerProperties containerProperties = new ContainerProperties(destination);
KafkaMessageListenerContainer container = listenerContainer(consumerFactory, containerProperties, listener);
String containerName = flowName + ".container";
Object initializedContainer = beanFactory.initializeBean(container, containerName);
beanFactory.registerSingleton(containerName, initializedContainer);
if (log.isDebugEnabled()) {
log.debug("Initialized kafka message container with name [" + containerName
+ "] listening to destination [" + destination + "]");
}
}
}
private KafkaMessageListenerContainer listenerContainer(ConsumerFactory consumerFactory,
ContainerProperties containerProperties, GenericMessageListener listener) {
KafkaMessageListenerContainer container = new KafkaMessageListenerContainer(consumerFactory,
containerProperties);
container.setupMessageListener(listener);
return container;
}
static class FlowRegistrar {
}
}

View File

@@ -1,239 +0,0 @@
/*
* 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.kafka;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.toomuchcoding.jsonassert.JsonAssertion;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.FromFileProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.spec.internal.RegexProperty;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.util.ContentType;
import org.springframework.cloud.contract.verifier.util.ContentUtils;
import org.springframework.cloud.contract.verifier.util.JsonPaths;
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
import org.springframework.cloud.contract.verifier.util.MapConverter;
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
import org.springframework.messaging.Message;
/**
* Passes through a message that matches the one defined in the DSL.
*
* @author Marcin Grzejszczak
*/
class StubRunnerKafkaMessageSelector {
private static final Map<Message<?>, Contract> CACHE = Collections.synchronizedMap(new WeakHashMap<>());
private static final Log log = LogFactory.getLog(StubRunnerKafkaMessageSelector.class);
private final List<Contract> groovyDsls;
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
StubRunnerKafkaMessageSelector(List<Contract> groovyDsls) {
this.groovyDsls = groovyDsls;
}
Contract matchingContract(Message<?> message) {
if (CACHE.containsKey(message)) {
return CACHE.get(message);
}
Contract contract = getContract(message);
if (contract != null) {
CACHE.put(message, contract);
}
return contract;
}
void updateCache(Message<?> message, Contract contract) {
CACHE.put(message, contract);
}
private Contract getContract(Message<?> message) {
for (Contract groovyDsl : this.groovyDsls) {
Contract contract = matchContract(message, groovyDsl);
if (contract != null) {
return contract;
}
}
return null;
}
private Contract matchContract(Message<?> message, Contract groovyDsl) {
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
if (!unmatchedHeaders.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
}
return null;
}
Object inputMessage = message.getPayload();
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
if (dslBody instanceof FromFileProperty) {
if (log.isDebugEnabled()) {
log.debug("Will compare file content");
}
FromFileProperty property = (FromFileProperty) dslBody;
if (property.isString()) {
// continue processing as if body was pure string
dslBody = property.asString();
}
else if (!(inputMessage instanceof byte[])) {
if (log.isDebugEnabled()) {
log.debug("Contract provided byte comparison, but the input message is of type ["
+ inputMessage.getClass() + "]. Can't compare the two.");
}
return null;
}
else {
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
if (log.isDebugEnabled() && !matches) {
log.debug("Contract provided byte comparison, but the byte arrays don't match");
}
return matches ? groovyDsl : null;
}
}
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
return groovyDsl;
}
return null;
}
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
boolean matches;
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
if (type == ContentType.JSON) {
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
}
else if (dslBody instanceof RegexProperty && inputMessage instanceof String) {
Pattern pattern = ((RegexProperty) dslBody).getPattern();
matches = pattern.matcher((String) inputMessage).matches();
bodyUnmatchedLog(dslBody, matches, pattern);
}
else {
matches = dslBody.equals(inputMessage);
bodyUnmatchedLog(dslBody, matches, inputMessage);
}
return matches;
}
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
if (log.isDebugEnabled() && !matches) {
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
+ "]");
}
}
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
Object dslBody) {
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
JsonPaths jsonPaths = JsonToJsonPathsConverter
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
DocumentContext parsedJson;
try {
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
}
catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot serialize to JSON", e);
}
List<String> unmatchedJsonPath = new ArrayList<>();
boolean matches = true;
for (MethodBufferingJsonVerifiable path : jsonPaths) {
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
}
if (matchers != null && matchers.hasMatchers()) {
for (BodyMatcher matcher : matchers.matchers()) {
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
}
}
if (!unmatchedJsonPath.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
}
}
return matches;
}
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
try {
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
return true;
}
catch (Exception e) {
unmatchedJsonPath.add(e.getLocalizedMessage());
return false;
}
}
private List<String> headersMatch(Message message, Contract groovyDsl) {
List<String> unmatchedHeaders = new ArrayList<>();
Map<String, Object> headers = message.getHeaders();
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
String name = it.getName();
Object value = it.getClientValue();
Object valueInHeader = headers.get(name);
valueInHeader = valueInHeader instanceof byte[] ? fromByte((byte[]) valueInHeader) : valueInHeader;
boolean matches;
if (value instanceof RegexProperty) {
Pattern pattern = ((RegexProperty) value).getPattern();
matches = pattern.matcher(valueInHeader.toString()).matches();
}
else {
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
}
if (!matches) {
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
}
}
return unmatchedHeaders;
}
private String fromByte(byte[] valueInHeader) {
String string = new String(valueInHeader);
if (string.startsWith("\"") && string.endsWith("\"")) {
return string.substring(1, string.length() - 1);
}
return string;
}
private String unmatchedText(Object expectedValue) {
return expectedValue instanceof RegexProperty
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
: "be equal to [" + expectedValue + "]";
}
}

View File

@@ -1,104 +0,0 @@
/*
* 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.kafka;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.listener.MessageListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.kafka.support.converter.MessagingMessageConverter;
import org.springframework.messaging.Message;
/**
* @author Marcin Grzejszczak
*/
class StubRunnerKafkaRouter implements MessageListener<Object, Object> {
private static final Log log = LogFactory.getLog(StubRunnerKafkaRouter.class);
private final MessagingMessageConverter messageConverter = new MessagingMessageConverter();
private final StubRunnerKafkaMessageSelector selector;
private final BeanFactory beanFactory;
private final List<Contract> contracts;
private KafkaTemplate kafkaTemplate;
StubRunnerKafkaRouter(List<Contract> groovyDsls, BeanFactory beanFactory) {
this.selector = new StubRunnerKafkaMessageSelector(groovyDsls);
this.beanFactory = beanFactory;
this.contracts = groovyDsls;
}
private KafkaTemplate kafkaTemplate() {
if (this.kafkaTemplate == null) {
this.kafkaTemplate = this.beanFactory.getBean(KafkaTemplate.class);
}
return this.kafkaTemplate;
}
@Override
public void onMessage(ConsumerRecord<Object, Object> data) {
if (log.isDebugEnabled()) {
log.debug("Received message [" + data + "]");
}
Message<?> message = messageConverter.toMessage(data, null, null, null);
Contract dsl = this.selector.matchingContract(message);
if (dsl != null && dsl.getOutputMessage() != null && dsl.getOutputMessage().getSentTo() != null) {
String destination = dsl.getOutputMessage().getSentTo().getClientValue();
if (log.isDebugEnabled()) {
log.debug("Found a matching contract with an output message. Will send it to the [" + destination
+ "] destination");
}
Message<?> transform = new StubRunnerKafkaTransformer(this.contracts).transform(dsl);
String defaultTopic = kafkaTemplate().getDefaultTopic();
try {
kafkaTemplate().setDefaultTopic(destination);
kafkaTemplate().send(transform);
}
finally {
kafkaTemplate().setDefaultTopic(defaultTopic);
}
}
}
@Override
public void onMessage(ConsumerRecord<Object, Object> data, Acknowledgment acknowledgment) {
onMessage(data);
}
@Override
public void onMessage(ConsumerRecord<Object, Object> data, Consumer<?, ?> consumer) {
onMessage(data);
}
@Override
public void onMessage(ConsumerRecord<Object, Object> data, Acknowledgment acknowledgment, Consumer<?, ?> consumer) {
onMessage(data);
}
}

View File

@@ -1,59 +0,0 @@
/*
* 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.kafka;
import java.util.List;
import java.util.Map;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
/**
* Sends forward a message defined in the DSL.
*
* @author Marcin Grzejszczak
*/
class StubRunnerKafkaTransformer {
private final StubRunnerKafkaMessageSelector selector;
StubRunnerKafkaTransformer(List<Contract> groovyDsls) {
this.selector = new StubRunnerKafkaMessageSelector(groovyDsls);
}
public Message<?> transform(Contract groovyDsl) {
Object outputBody = outputBody(groovyDsl);
Map<String, Object> headers = groovyDsl.getOutputMessage().getHeaders().asStubSideMap();
Message newMessage = MessageBuilder.createMessage(outputBody, new MessageHeaders(headers));
this.selector.updateCache(newMessage, groovyDsl);
return newMessage;
}
private Object outputBody(Contract groovyDsl) {
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
if (outputBody instanceof FromFileProperty) {
FromFileProperty property = (FromFileProperty) outputBody;
return property.asBytes();
}
return BodyExtractor.extractStubValueFrom(outputBody);
}
}

View File

@@ -1,54 +0,0 @@
/*
* 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.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.integration.router.AbstractMessageRouter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
/**
* @author Marcin Grzejszczak
*/
class StubRunnerMessageRouter extends AbstractMessageRouter {
private final StubRunnerStreamMessageSelector selector;
private final BeanFactory beanFactory;
StubRunnerMessageRouter(List<Contract> groovyDsls, BeanFactory beanFactory) {
this.selector = new StubRunnerStreamMessageSelector(groovyDsls);
this.beanFactory = beanFactory;
}
@Override
protected Collection<MessageChannel> determineTargetChannels(Message<?> message) {
Contract dsl = this.selector.matchingContract(message);
if (dsl != null && dsl.getOutputMessage() != null && dsl.getOutputMessage().getSentTo() != null) {
String channelName = StubRunnerStreamConfiguration.resolvedDestination(this.beanFactory,
dsl.getOutputMessage().getSentTo().getClientValue());
return Collections.singleton((MessageChannel) this.beanFactory.getBean(channelName));
}
return Collections.singleton((MessageChannel) this.beanFactory.getBean("nullChannel"));
}
}

View File

@@ -1,124 +0,0 @@
/*
* 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.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.cloud.contract.stubrunner.messaging.integration.StubRunnerIntegrationConfiguration;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.config.BindingProperties;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.context.Lifecycle;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlowBuilder;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Spring Cloud Stream configuration that iterates over the downloaded Groovy DSLs and
* registers a flow for each DSL.
*
* @author Marcin Grzejszczak
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ IntegrationFlow.class, InputDestination.class })
@ConditionalOnProperty(name = "stubrunner.stream.enabled", havingValue = "true", matchIfMissing = true)
@AutoConfigureBefore(StubRunnerIntegrationConfiguration.class)
public class StubRunnerStreamConfiguration {
private static final Log log = LogFactory.getLog(StubRunnerStreamConfiguration.class);
static String resolvedDestination(BeanFactory context, String destination) {
Map<String, BindingProperties> bindings = bindingProperties(context);
for (Map.Entry<String, BindingProperties> entry : bindings.entrySet()) {
if (destination.equals(entry.getValue().getDestination())) {
if (log.isDebugEnabled()) {
log.debug("Found a channel named [" + entry.getKey() + "] with destination [" + destination + "]");
}
return entry.getKey();
}
}
if (log.isDebugEnabled()) {
log.debug("No destination named [" + destination
+ "] was found. Assuming that the destination equals the channel name");
}
return destination;
}
private static Map<String, BindingProperties> bindingProperties(BeanFactory context) {
return context.getBean(BindingServiceProperties.class).getBindings();
}
@Bean
@ConditionalOnMissingBean(name = "stubFlowRegistrar")
@ConditionalOnBean(BindingServiceProperties.class)
public FlowRegistrar stubFlowRegistrar(AutowireCapableBeanFactory beanFactory, BatchStubRunner batchStubRunner) {
Map<StubConfiguration, Collection<Contract>> contracts = batchStubRunner.getContracts();
for (Entry<StubConfiguration, Collection<Contract>> entry : contracts.entrySet()) {
StubConfiguration key = entry.getKey();
Collection<Contract> value = entry.getValue();
String name = key.getGroupId() + "_" + key.getArtifactId();
MultiValueMap<String, Contract> map = new LinkedMultiValueMap<>();
for (Contract dsl : value) {
if (dsl == null) {
continue;
}
if (dsl.getInput() != null && dsl.getInput().getMessageFrom() != null
&& StringUtils.hasText(dsl.getInput().getMessageFrom().getClientValue())) {
String from = resolvedDestination(beanFactory, dsl.getInput().getMessageFrom().getClientValue());
map.add(from, dsl);
}
}
for (Entry<String, List<Contract>> entries : map.entrySet()) {
final String flowName = name + "_" + entries.getKey() + "_" + entries.getValue().hashCode();
IntegrationFlowBuilder builder = IntegrationFlow.from(entries.getKey())
.filter(new StubRunnerStreamMessageSelector(entries.getValue()),
e -> e.id(flowName + ".filter"))
.transform(new StubRunnerStreamTransformer(entries.getValue()))
.route(new StubRunnerMessageRouter(entries.getValue(), beanFactory));
beanFactory.initializeBean(builder.get(), flowName);
beanFactory.getBean(flowName + ".filter", Lifecycle.class).start();
}
}
return new FlowRegistrar();
}
static class FlowRegistrar {
}
}

View File

@@ -1,241 +0,0 @@
/*
* 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.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.toomuchcoding.jsonassert.JsonAssertion;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.FromFileProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.spec.internal.RegexProperty;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.util.ContentType;
import org.springframework.cloud.contract.verifier.util.ContentUtils;
import org.springframework.cloud.contract.verifier.util.JsonPaths;
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
import org.springframework.cloud.contract.verifier.util.MapConverter;
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
import org.springframework.integration.core.MessageSelector;
import org.springframework.messaging.Message;
/**
* Passes through a message that matches the one defined in the DSL.
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
class StubRunnerStreamMessageSelector implements MessageSelector {
private static final Map<Message, Contract> CACHE = Collections.synchronizedMap(new WeakHashMap<>());
private static final Log log = LogFactory.getLog(StubRunnerStreamMessageSelector.class);
private final List<Contract> groovyDsls;
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
StubRunnerStreamMessageSelector(Contract groovyDsl) {
this(Collections.singletonList(groovyDsl));
}
StubRunnerStreamMessageSelector(List<Contract> groovyDsls) {
this.groovyDsls = groovyDsls;
}
@Override
public boolean accept(Message<?> message) {
return matchingContract(message) != null;
}
Contract matchingContract(Message<?> message) {
if (CACHE.containsKey(message)) {
return CACHE.get(message);
}
Contract contract = getContract(message);
if (contract != null) {
CACHE.put(message, contract);
}
return contract;
}
void updateCache(Message<?> message, Contract contract) {
CACHE.put(message, contract);
}
private Contract getContract(Message<?> message) {
for (Contract groovyDsl : this.groovyDsls) {
Contract contract = matchContract(message, groovyDsl);
if (contract != null) {
return contract;
}
}
return null;
}
private Contract matchContract(Message<?> message, Contract groovyDsl) {
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
if (!unmatchedHeaders.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
}
return null;
}
Object inputMessage = message.getPayload();
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
if (dslBody instanceof FromFileProperty) {
if (log.isDebugEnabled()) {
log.debug("Will compare file content");
}
FromFileProperty property = (FromFileProperty) dslBody;
if (property.isString()) {
// continue processing as if body was pure string
dslBody = property.asString();
}
else if (!(inputMessage instanceof byte[])) {
if (log.isDebugEnabled()) {
log.debug("Contract provided byte comparison, but the input message is of type ["
+ inputMessage.getClass() + "]. Can't compare the two.");
}
return null;
}
else {
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
if (log.isDebugEnabled() && !matches) {
log.debug("Contract provided byte comparison, but the byte arrays don't match");
}
return matches ? groovyDsl : null;
}
}
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
return groovyDsl;
}
return null;
}
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
boolean matches;
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
if (type == ContentType.JSON) {
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
}
else if ((dslBody instanceof RegexProperty || dslBody instanceof Pattern) && inputMessage instanceof String) {
Pattern pattern = new RegexProperty(dslBody).getPattern();
matches = pattern.matcher((String) inputMessage).matches();
bodyUnmatchedLog(dslBody, matches, pattern);
}
else {
matches = dslBody.equals(inputMessage);
bodyUnmatchedLog(dslBody, matches, inputMessage);
}
return matches;
}
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
if (log.isDebugEnabled() && !matches) {
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
+ "]");
}
}
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
Object dslBody) {
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
JsonPaths jsonPaths = JsonToJsonPathsConverter
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
DocumentContext parsedJson;
try {
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
}
catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot serialize to JSON", e);
}
List<String> unmatchedJsonPath = new ArrayList<>();
boolean matches = true;
for (MethodBufferingJsonVerifiable path : jsonPaths) {
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
}
if (matchers != null && matchers.hasMatchers()) {
for (BodyMatcher matcher : matchers.matchers()) {
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
}
}
if (!unmatchedJsonPath.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
}
}
return matches;
}
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
try {
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
return true;
}
catch (Exception e) {
unmatchedJsonPath.add(e.getLocalizedMessage());
return false;
}
}
private List<String> headersMatch(Message<?> message, Contract groovyDsl) {
List<String> unmatchedHeaders = new ArrayList<>();
Map<String, Object> headers = message.getHeaders();
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
String name = it.getName();
Object value = it.getClientValue();
Object valueInHeader = headers.get(name);
boolean matches;
if (value instanceof RegexProperty || value instanceof Pattern) {
Pattern pattern = new RegexProperty(value).getPattern();
matches = pattern.matcher(valueInHeader.toString()).matches();
}
else {
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
}
if (!matches) {
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
}
}
return unmatchedHeaders;
}
private String unmatchedText(Object expectedValue) {
return expectedValue instanceof RegexProperty
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
: "be equal to [" + expectedValue + "]";
}
}

View File

@@ -1,73 +0,0 @@
/*
* 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.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
/**
* Sends forward a message defined in the DSL.
*
* @author Marcin Grzejszczak
*/
class StubRunnerStreamTransformer {
private final StubRunnerStreamMessageSelector selector;
StubRunnerStreamTransformer(Contract groovyDsl) {
this(Collections.singletonList(groovyDsl));
}
StubRunnerStreamTransformer(List<Contract> groovyDsls) {
this.selector = new StubRunnerStreamMessageSelector(groovyDsls);
}
public Message<?> transform(Message<?> source) {
Contract groovyDsl = matchingContract(source);
if (groovyDsl == null || groovyDsl.getOutputMessage() == null) {
return source;
}
byte[] outputBody = outputBodyAsBytes(groovyDsl);
Map<String, Object> headers = groovyDsl.getOutputMessage().getHeaders().asStubSideMap();
MessageHeaders messageHeaders = new MessageHeaders(headers);
Message<byte[]> message = MessageBuilder.createMessage(outputBody, messageHeaders);
this.selector.updateCache(message, groovyDsl);
return message;
}
private byte[] outputBodyAsBytes(Contract groovyDsl) {
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
if (outputBody instanceof FromFileProperty) {
FromFileProperty property = (FromFileProperty) outputBody;
return property.asBytes();
}
return BodyExtractor.extractStubValueFrom(outputBody).getBytes();
}
Contract matchingContract(Message<?> source) {
return this.selector.matchingContract(source);
}
}

View File

@@ -1,11 +1,6 @@
org.springframework.cloud.contract.stubrunner.spring.StubRunnerConfiguration
org.springframework.cloud.contract.stubrunner.spring.cloud.StubRunnerSpringCloudAutoConfiguration
org.springframework.cloud.contract.stubrunner.spring.cloud.loadbalancer.SpringCloudLoadBalancerAutoConfiguration
org.springframework.cloud.contract.stubrunner.messaging.integration.StubRunnerIntegrationConfiguration
org.springframework.cloud.contract.stubrunner.messaging.jms.StubRunnerJmsConfiguration
org.springframework.cloud.contract.stubrunner.messaging.stream.StubRunnerStreamConfiguration
org.springframework.cloud.contract.stubrunner.spring.cloud.eureka.StubRunnerSpringCloudEurekaAutoConfiguration
org.springframework.cloud.contract.stubrunner.spring.cloud.zookeeper.StubRunnerSpringCloudZookeeperAutoConfiguration
org.springframework.cloud.contract.stubrunner.spring.cloud.consul.StubRunnerSpringCloudConsulAutoConfiguration
org.springframework.cloud.contract.stubrunner.messaging.StubRunnerStreamsIntegrationAutoConfiguration
org.springframework.cloud.contract.stubrunner.messaging.camel.StubRunnerCamelConfiguration

View File

@@ -1,205 +0,0 @@
/*
* 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 org.apache.camel.Exchange
import org.apache.camel.Message
import org.apache.camel.impl.DefaultCamelContext
import org.apache.camel.support.DefaultExchange
import org.apache.camel.support.DefaultMessage
import spock.lang.Specification
import org.springframework.cloud.contract.spec.Contract
/**
* @author Marcin Grzejszczak
*/
class StubRunnerCamelPredicateSpec extends Specification {
Exchange exchange = new DefaultExchange(new DefaultCamelContext())
Message message = new DefaultMessage(exchange)
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")
bodyMatchers {
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)
bodyMatchers {
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)
}
def "should return true if headers and byte body matches"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
messagingContentType(applicationOctetStream())
}
messageBody(fileAsBytes("request.pdf"))
}
}
and:
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
exchange.in = message
message.headers = [
foo : 123,
contentType: "application/octet-stream"
]
message.body = StubRunnerCamelPredicate.getResource("/request.pdf").bytes
expect:
predicate.matches(exchange)
}
def "should return false if byte body types don't match for binary"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
messagingContentType(applicationOctetStream())
}
messageBody(fileAsBytes("request.pdf"))
}
}
and:
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
exchange.in = message
message.headers = [
foo : 123,
contentType: "application/octet-stream"
]
message.body = "hello world"
expect:
!predicate.matches(exchange)
}
}

View File

@@ -1,221 +0,0 @@
/*
* 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.integration
import spock.lang.Issue
import spock.lang.Specification
import org.springframework.cloud.contract.spec.Contract
import org.springframework.messaging.Message
/**
* @author Marcin Grzejszczak
*/
class StubRunnerIntegrationMessageSelectorSpec extends Specification {
Message message = Mock(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:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
message.headers >> [
foo: "non matching stuff"
]
expect:
!predicate.accept(message)
}
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:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
message.headers >> [
foo: 123
]
message.payload >> [
foo: "non matching stuff"
]
expect:
!predicate.accept(message)
}
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")
bodyMatchers {
jsonPath('$.foo', byRegex("[0-9]{3}"))
}
}
}
and:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
message.headers >> [
foo: 123
]
message.payload >> [
foo: "non matching stuff"
]
expect:
!predicate.accept(message)
}
def "should return true if headers and byte body matches"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
messagingContentType(applicationOctetStream())
}
messageBody(fileAsBytes("request.pdf"))
}
}
and:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
message.headers >> [
foo : 123,
contentType: "application/octet-stream"
]
message.payload >> StubRunnerIntegrationMessageSelector.getResource("/request.pdf").bytes
expect:
predicate.accept(message)
}
def "should return false if byte body types don't match for binary"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
messagingContentType(applicationOctetStream())
}
messageBody(fileAsBytes("request.pdf"))
}
}
and:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
message.headers >> [
foo : 123,
contentType: "application/octet-stream"
]
message.payload >> "hello world"
expect:
!predicate.accept(message)
}
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:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
message.headers >> [
foo: 123
]
message.payload >> [
foo: 123
]
expect:
predicate.accept(message)
}
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)
bodyMatchers {
jsonPath('$.foo', byRegex("[0-9]{3}"))
}
}
}
and:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(
dsl)
message.headers >> [
foo: 123
]
message.payload >> [
foo: 123
]
expect:
predicate.accept(message)
}
@Issue("1382")
def "should return true if header matches regex"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", $(anyUuid()))
}
messageBody(foo: 123)
}
}
and:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(
dsl)
message.headers >> [
foo: "fbcc2ed3-dbac-47e7-9a4b-c2d55709792c"
]
message.payload >> [
foo: 123
]
expect:
predicate.accept(message)
}
}

View File

@@ -1,250 +0,0 @@
/*
* 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 spock.lang.Issue
import spock.lang.Specification
import org.springframework.cloud.contract.spec.Contract
import org.springframework.http.MediaType
import org.springframework.messaging.Message
/**
* @author Marcin Grzejszczak
*/
class StubRunnerStreamMessageSelectorSpec extends Specification {
Message message = Mock(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:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
message.headers >> [
foo: "non matching stuff"
]
expect:
!predicate.accept(message)
}
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:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
message.headers >> [
foo: 123
]
message.payload >> [
foo: "non matching stuff"
]
expect:
!predicate.accept(message)
}
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")
bodyMatchers {
jsonPath('$.foo', byRegex("[0-9]{3}"))
}
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
message.headers >> [
foo: 123
]
message.payload >> [
foo: "non matching stuff"
]
expect:
!predicate.accept(message)
}
def "should return true if headers and body match"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
header("bar", "bar")
messagingContentType(applicationJsonUtf8())
header("regex", regex("234"))
}
messageBody(foo: $(c(regex("[0-9]{3}")), p(123)))
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
message.headers >> [
foo : 123,
bar : "bar",
contentType: MediaType.APPLICATION_JSON_UTF8,
regex : 234
]
message.payload >> [
foo: 123
]
expect:
predicate.accept(message)
}
def "should return true if headers and text body matches"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
messagingContentType(textPlain())
}
messageBody($(c(regex("[0-9]{3}")), p("123")))
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
message.headers >> [
foo : 123,
contentType: "text/plain"
]
message.payload >> "123"
expect:
predicate.accept(message)
}
def "should return true if headers and byte body matches"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
messagingContentType(applicationOctetStream())
}
messageBody(fileAsBytes("request.pdf"))
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
message.headers >> [
foo : 123,
contentType: "application/octet-stream"
]
message.payload >> StubRunnerStreamMessageSelectorSpec.getResource("/request.pdf").bytes
expect:
predicate.accept(message)
}
def "should return false if byte body types don't match for binary"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
messagingContentType(applicationOctetStream())
}
messageBody(fileAsBytes("request.pdf"))
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
message.headers >> [
foo : 123,
contentType: "application/octet-stream"
]
message.payload >> "hello world"
expect:
!predicate.accept(message)
}
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)
bodyMatchers {
jsonPath('$.foo', byRegex("[0-9]{3}"))
}
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(
dsl)
message.headers >> [
foo: 123
]
message.payload >> [
foo: 123
]
expect:
predicate.accept(message)
}
@Issue("1382")
def "should return true if header matches regex"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", $(anyUuid()))
}
messageBody(foo: 123)
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(
dsl)
message.headers >> [
foo: "fbcc2ed3-dbac-47e7-9a4b-c2d55709792c"
]
message.payload >> [
foo: 123
]
expect:
predicate.accept(message)
}
}

View File

@@ -85,7 +85,7 @@ class StubRunnerBootSpec {
String response = RestAssuredMockMvc.get('/triggers').body.asString()
then:
def root = new JsonSlurper().parseText(response)
assert root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs'?.containsAll(["delete_book", "return_book_1", "return_book_2"])
assert root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs'?.containsAll(["return_book_1"])
}
@Test

View File

@@ -15,13 +15,7 @@
*/ org.springframework.cloud.contract.spec.Contract.make {
label 'some_label'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
triggeredBy("hashCode()")
}
outputMessage {
sentTo('jms:output')

View File

@@ -15,13 +15,7 @@
*/ org.springframework.cloud.contract.spec.Contract.make {
label 'some_label'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
triggeredBy("hashCode()")
}
outputMessage {
sentTo('jms:output')

View File

@@ -15,13 +15,7 @@
*/ org.springframework.cloud.contract.spec.Contract.make {
label 'some_label'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
triggeredBy("hashCode()")
}
outputMessage {
sentTo('jms:output')

View File

@@ -15,13 +15,7 @@
*/ org.springframework.cloud.contract.spec.Contract.make {
label 'some_label'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
triggeredBy("hashCode()")
}
outputMessage {
sentTo('jms:output')

View File

@@ -80,9 +80,6 @@ class MessagePactCreator {
if (input.getTriggeredBy() != null) {
return input.getTriggeredBy().getExecutionCommand();
}
else if (input.getMessageFrom() != null) {
return "received message from " + clientValueExtractor.apply(input.getMessageFrom());
}
else {
return "";
}

View File

@@ -1,43 +0,0 @@
/*
* 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 contracts
import org.springframework.cloud.contract.spec.Contract
[
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')
}
}
}
]

View File

@@ -1,44 +0,0 @@
{
"consumer": {
"name": "Consumer"
},
"provider": {
"name": "Provider"
},
"messages": [
{
"description": "message sent to jms:output",
"metaData": {
"BOOK-NAME": "foo"
},
"contents": {
"bookName": "foo"
},
"providerStates": [
{
"name": "received message from jms:input"
}
],
"matchingRules": {
"body": {
"$.bookName": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "4.3.15"
}
}
}

View File

@@ -1,35 +0,0 @@
/*
* 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 contracts
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
label 'some_label'
input {
messageFrom('jms:delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
]

View File

@@ -1,28 +0,0 @@
{
"consumer": {
"name": "Consumer"
},
"provider": {
"name": "Provider"
},
"messages": [
{
"description": "assert that bookWasDeleted()",
"metaData": {
},
"providerStates": [
{
"name": "received message from jms:delete"
}
]
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "4.3.15"
}
}
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.builder;
import org.springframework.cloud.contract.verifier.config.TestFramework;
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata;
class JavaMessagingGiven extends MessagingGiven {
private final GeneratedClassMetaData generatedClassMetaData;
JavaMessagingGiven(BlockBuilder blockBuilder, GeneratedClassMetaData generatedClassMetaData) {
super(blockBuilder, generatedClassMetaData, JavaMessagingBodyParser.INSTANCE);
this.generatedClassMetaData = generatedClassMetaData;
}
@Override
public boolean accept(SingleContractMetadata metadata) {
return super.accept(metadata)
&& this.generatedClassMetaData.configProperties.getTestFramework() != TestFramework.SPOCK;
}
}

View File

@@ -1,68 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.builder;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.spec.internal.Input;
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata;
import org.springframework.cloud.contract.verifier.util.ContentType;
class MessagingBodyGiven implements Given, MethodVisitor<Given> {
private final BlockBuilder blockBuilder;
private final BodyReader bodyReader;
private final BodyParser bodyParser;
MessagingBodyGiven(BlockBuilder blockBuilder, BodyReader bodyReader, BodyParser bodyParser) {
this.blockBuilder = blockBuilder;
this.bodyReader = bodyReader;
this.bodyParser = bodyParser;
}
@Override
public MethodVisitor<Given> apply(SingleContractMetadata metadata) {
appendBodyGiven(metadata);
return this;
}
private void appendBodyGiven(SingleContractMetadata metadata) {
ContentType contentType = metadata.getInputTestContentType();
Input inputMessage = metadata.getContract().getInput();
Object bodyValue = this.bodyParser.extractServerValueFromBody(contentType,
inputMessage.getMessageBody().getServerValue());
if (bodyValue instanceof FromFileProperty) {
FromFileProperty fileProperty = (FromFileProperty) bodyValue;
String byteText = fileProperty.isByte()
? this.bodyReader.readBytesFromFileString(metadata, fileProperty, CommunicationType.REQUEST)
: this.bodyParser.quotedLongText(this.bodyReader.readStringFromFileString(metadata, fileProperty,
CommunicationType.REQUEST));
this.blockBuilder.addIndented(byteText);
}
else {
String text = this.bodyParser.convertToJsonString(bodyValue);
this.blockBuilder.addIndented(this.bodyParser.quotedEscapedLongText(text));
}
}
@Override
public boolean accept(SingleContractMetadata metadata) {
return metadata.getContract().getInput().getMessageBody() != null;
}
}

View File

@@ -1,48 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.builder;
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata;
class MessagingBodyWhen implements When {
private final BlockBuilder blockBuilder;
private final BodyReader bodyReader;
MessagingBodyWhen(BlockBuilder blockBuilder, GeneratedClassMetaData metaData) {
this.blockBuilder = blockBuilder;
this.bodyReader = new BodyReader(metaData);
}
@Override
public MethodVisitor<When> apply(SingleContractMetadata metadata) {
this.bodyReader.storeContractAsYaml(metadata);
this.blockBuilder
.addIndented("contractVerifierMessaging.send(inputMessage, \""
+ metadata.getContract().getInput().getMessageFrom().getServerValue() + "\",")
.addEmptyLine().indent().addIndented("contract(this, \"" + metadata.methodName() + ".yml\"))")
.addEndingIfNotPresent().unindent();
return this;
}
@Override
public boolean accept(SingleContractMetadata metadata) {
return metadata.getContract().getInput().getMessageFrom() != null;
}
}

View File

@@ -1,60 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.builder;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata;
class MessagingGiven implements Given, MethodVisitor<Given>, BodyMethodVisitor {
private final BlockBuilder blockBuilder;
private final GeneratedClassMetaData generatedClassMetaData;
private final List<Given> givens = new LinkedList<>();
MessagingGiven(BlockBuilder blockBuilder, GeneratedClassMetaData generatedClassMetaData, BodyParser bodyParser) {
this.blockBuilder = blockBuilder;
this.generatedClassMetaData = generatedClassMetaData;
this.givens.addAll(Arrays.asList(
new MessagingBodyGiven(this.blockBuilder, new BodyReader(this.generatedClassMetaData), bodyParser),
new MessagingHeadersGiven(this.blockBuilder)));
}
@Override
public MethodVisitor<Given> apply(SingleContractMetadata metadata) {
startBodyBlock(this.blockBuilder, "given:");
this.blockBuilder.addIndented("ContractVerifierMessage inputMessage = contractVerifierMessaging.create(")
.addEmptyLine().indent();
this.givens.stream().filter(given -> given.accept(metadata)).forEach(given -> {
given.apply(metadata);
this.blockBuilder.addEmptyLine();
});
this.blockBuilder.unindent().unindent().startBlock().addIndented(")").addEndingIfNotPresent().addEmptyLine()
.endBlock();
return this;
}
@Override
public boolean accept(SingleContractMetadata metadata) {
return metadata.isMessaging() && metadata.getContract().getInput().getTriggeredBy() == null;
}
}

View File

@@ -1,56 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.builder;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.spec.internal.Input;
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata;
import org.springframework.cloud.contract.verifier.util.MapConverter;
class MessagingHeadersGiven implements Given, MethodVisitor<Given> {
private final BlockBuilder blockBuilder;
MessagingHeadersGiven(BlockBuilder blockBuilder) {
this.blockBuilder = blockBuilder;
}
@Override
public MethodVisitor<Given> apply(SingleContractMetadata metadata) {
Input inputMessage = metadata.getContract().getInput();
this.blockBuilder.startBlock().addIndented(", headers()").startBlock();
inputMessage.getMessageHeaders().executeForEachHeader(header -> {
this.blockBuilder.addEmptyLine().addIndented(getHeaderString(header));
});
this.blockBuilder.endBlock();
return this;
}
private String getHeaderString(Header header) {
return ".header(" + getTestSideValue(header.getName()) + ", " + getTestSideValue(header.getServerValue()) + ")";
}
private String getTestSideValue(Object object) {
return '"' + MapConverter.getTestSideValues(object).toString() + '"';
}
@Override
public boolean accept(SingleContractMetadata metadata) {
return metadata.getContract().getInput().getMessageHeaders() != null;
}
}

View File

@@ -28,10 +28,9 @@ class MessagingWhen implements When, BodyMethodVisitor {
private final List<When> whens = new LinkedList<>();
MessagingWhen(BlockBuilder blockBuilder, GeneratedClassMetaData generatedClassMetaData) {
MessagingWhen(BlockBuilder blockBuilder) {
this.blockBuilder = blockBuilder;
this.whens.addAll(Arrays.asList(new MessagingTriggeredByWhen(this.blockBuilder),
new MessagingBodyWhen(this.blockBuilder, generatedClassMetaData),
new MessagingAssertThatWhen(this.blockBuilder)));
}

View File

@@ -117,9 +117,7 @@ class SingleMethodBuilder {
SingleMethodBuilder messaging() {
// @formatter:off
return given(new JavaMessagingGiven(this.blockBuilder, this.generatedClassMetaData))
.given(new SpockMessagingGiven(this.blockBuilder, this.generatedClassMetaData))
.when(new MessagingWhen(this.blockBuilder, this.generatedClassMetaData))
return when(new MessagingWhen(this.blockBuilder))
.then(new JavaMessagingWithBodyThen(this.blockBuilder,
this.generatedClassMetaData))
.then(new SpockMessagingWithBodyThen(this.blockBuilder,

View File

@@ -1,37 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.builder;
import org.springframework.cloud.contract.verifier.config.TestFramework;
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata;
class SpockMessagingGiven extends MessagingGiven {
private final GeneratedClassMetaData generatedClassMetaData;
SpockMessagingGiven(BlockBuilder blockBuilder, GeneratedClassMetaData generatedClassMetaData) {
super(blockBuilder, generatedClassMetaData, SpockMessagingBodyParser.INSTANCE);
this.generatedClassMetaData = generatedClassMetaData;
}
@Override
public boolean accept(SingleContractMetadata metadata) {
return super.accept(metadata)
&& this.generatedClassMetaData.configProperties.getTestFramework() == TestFramework.SPOCK;
}
}

View File

@@ -367,7 +367,6 @@ class ContractsToYaml {
protected void input(Contract contract, YamlContract yamlContract) {
Input input = contract.getInput();
if (input != null) {
ContentType contentType = evaluateClientSideContentType(input.getMessageHeaders(), input.getMessageBody());
yamlContract.input = new YamlContract.Input();
yamlContract.input.assertThat = Optional.ofNullable(input.getAssertThat())
.map(assertThat -> MapConverter
@@ -377,26 +376,6 @@ class ContractsToYaml {
.map(triggeredBy -> MapConverter
.getTestSideValues(triggeredBy.toString(), MapConverter.JSON_PARSING_FUNCTION).toString())
.orElse(null);
yamlContract.input.messageHeaders = input.getMessageHeaders().asTestSideMap();
yamlContract.input.messageBody = MapConverter.getTestSideValues(input.getMessageBody(),
MapConverter.JSON_PARSING_FUNCTION);
yamlContract.input.messageFrom = Optional
.ofNullable(input.getMessageFrom()).map(messageFrom -> MapConverter
.getTestSideValues(messageFrom, MapConverter.JSON_PARSING_FUNCTION).toString())
.orElse(null);
Optional.ofNullable(input.getBodyMatchers()).map(BodyMatchers::matchers)
.ifPresent(bodyMatchers -> bodyMatchers.forEach(bodyMatcher -> {
YamlContract.BodyStubMatcher bodyStubMatcher = new YamlContract.BodyStubMatcher();
bodyStubMatcher.path = bodyMatcher.path();
bodyStubMatcher.type = stubMatcherType(bodyMatcher.matchingType());
bodyStubMatcher.value = Optional.ofNullable(bodyMatcher.value()).map(Object::toString)
.orElse(null);
yamlContract.input.matchers.body.add(bodyStubMatcher);
}));
if (XML != contentType) {
setInputBodyMatchers(input.getMessageBody(), yamlContract.input.matchers.body);
}
setInputHeadersMatchers(input.getMessageHeaders(), yamlContract.input.matchers.headers);
}
}

View File

@@ -733,22 +733,10 @@ public class YamlContract {
public static class Input {
public String messageFrom;
public String triggeredBy;
public Map<String, Object> messageHeaders = new LinkedHashMap<String, Object>();
public Object messageBody;
public String messageBodyFromFile;
public String messageBodyFromFileAsBytes;
public String assertThat;
public StubMatchers matchers = new StubMatchers();
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -758,26 +746,17 @@ public class YamlContract {
return false;
}
Input input = (Input) o;
return Objects.equals(messageFrom, input.messageFrom) && Objects.equals(triggeredBy, input.triggeredBy)
&& Objects.equals(messageHeaders, input.messageHeaders)
&& Objects.equals(messageBody, input.messageBody)
&& Objects.equals(messageBodyFromFile, input.messageBodyFromFile)
&& Objects.equals(messageBodyFromFileAsBytes, input.messageBodyFromFileAsBytes)
&& Objects.equals(assertThat, input.assertThat) && Objects.equals(matchers, input.matchers);
return Objects.equals(triggeredBy, input.triggeredBy) && Objects.equals(assertThat, input.assertThat);
}
@Override
public int hashCode() {
return Objects.hash(messageFrom, triggeredBy, messageHeaders, messageBody, messageBodyFromFile,
messageBodyFromFileAsBytes, assertThat, matchers);
return Objects.hash(triggeredBy, assertThat);
}
@Override
public String toString() {
return "Input{" + "messageFrom='" + messageFrom + '\'' + ", triggeredBy='" + triggeredBy + '\''
+ ", messageHeaders=" + messageHeaders + ", messageBody=" + messageBody + ", messageBodyFromFile='"
+ messageBodyFromFile + '\'' + ", messageBodyFromFileAsBytes='" + messageBodyFromFileAsBytes + '\''
+ ", assertThat='" + assertThat + '\'' + ", matchers=" + matchers + '}';
return "Input{" + "triggeredBy='" + triggeredBy + '\'' + ", assertThat='" + assertThat + '\'' + '}';
}
}

View File

@@ -661,23 +661,13 @@ class YamlToContracts {
YamlContract.Input yamlContractInput = yamlContract.input;
if (yamlContractInput != null) {
dslContract.input(dslContractInput -> {
mapInputMessageFrom(yamlContractInput, dslContractInput);
mapInputAssertThat(yamlContractInput, dslContractInput);
mapInputTriggeredBy(yamlContractInput, dslContractInput);
mapInputMessageHeaders(yamlContractInput, dslContractInput);
mapInputMessageBody(yamlContractInput, dslContractInput);
mapInputBodyMatchers(yamlContractInput, dslContractInput);
});
}
}
private void mapInputMessageFrom(YamlContract.Input yamlContractInput, Input dslContractInput) {
if (yamlContractInput.messageFrom != null) {
dslContractInput.messageFrom(yamlContractInput.messageFrom);
}
}
private void mapInputAssertThat(YamlContract.Input yamlContractInput, Input dslContractInput) {
if (yamlContractInput.assertThat != null) {
dslContractInput.assertThat(yamlContractInput.assertThat);
@@ -690,80 +680,6 @@ class YamlToContracts {
}
}
private void mapInputMessageHeaders(YamlContract.Input yamlContractInput, Input dslContractInput) {
dslContractInput
.messageHeaders(dslContractMessageHeaders -> Optional.ofNullable(yamlContractInput.messageHeaders)
.ifPresent(yamlContractMessageHeaders -> yamlContractMessageHeaders.forEach((key, value) -> {
YamlContract.KeyValueMatcher matcher = Optional.ofNullable(yamlContractInput.matchers)
.map(yamlContractInputMatchers -> yamlContractInputMatchers.headers)
.flatMap(yamlContractInputMatchersHeaders -> yamlContractInputMatchersHeaders
.stream()
.filter(yamlContractInputMatchersHeader -> yamlContractInputMatchersHeader.key
.equals(key))
.findFirst())
.orElse(null);
dslContractMessageHeaders.header(key, clientValue(value, matcher, key));
})));
}
private void mapInputMessageBody(YamlContract.Input yamlContractInput, Input dslContractInput) {
if (yamlContractInput.messageBody != null) {
dslContractInput.messageBody(yamlContractInput.messageBody);
}
if (yamlContractInput.messageBodyFromFile != null) {
dslContractInput.messageBody(file(yamlContractInput.messageBodyFromFile));
}
if (yamlContractInput.messageBodyFromFileAsBytes != null) {
dslContractInput.messageBody(dslContractInput.fileAsBytes(yamlContractInput.messageBodyFromFileAsBytes));
}
}
private void mapInputBodyMatchers(YamlContract.Input yamlContractInput, Input dslContractInput) {
dslContractInput
.bodyMatchers(dslContractInputBodyMatchers -> Optional.ofNullable(yamlContractInput.matchers.body)
.ifPresent(yamlContractBodyStubMatchers -> yamlContractBodyStubMatchers
.forEach(yamlContractBodyStubMatcher -> {
ContentType contentType = evaluateClientSideContentType(
yamlHeadersToContractHeaders(
Optional.ofNullable(yamlContractInput.messageHeaders)
.orElse(new HashMap<>())),
Optional.ofNullable(yamlContractInput.messageBody).orElse(null));
MatchingTypeValue value;
switch (yamlContractBodyStubMatcher.type) {
case by_date:
value = dslContractInputBodyMatchers.byDate();
break;
case by_time:
value = dslContractInputBodyMatchers.byTime();
break;
case by_timestamp:
value = dslContractInputBodyMatchers.byTimestamp();
break;
case by_regex:
String regex = yamlContractBodyStubMatcher.value;
if (yamlContractBodyStubMatcher.predefined != null) {
regex = predefinedToPattern(yamlContractBodyStubMatcher.predefined)
.pattern();
}
value = dslContractInputBodyMatchers.byRegex(regex);
break;
case by_equality:
value = dslContractInputBodyMatchers.byEquality();
break;
default:
throw new UnsupportedOperationException("The type " + "["
+ yamlContractBodyStubMatcher.type + "] is unsupported. "
+ "Hint: If you're using <predefined> remember to pass < type:by_regex > ");
}
if (XML == contentType) {
dslContractInputBodyMatchers.xPath(yamlContractBodyStubMatcher.path, value);
}
else {
dslContractInputBodyMatchers.jsonPath(yamlContractBodyStubMatcher.path, value);
}
})));
}
private Headers yamlHeadersToContractHeaders(Map<String, Object> headers) {
Set<Header> convertedHeaders = headers.keySet().stream()
.map(header -> Header.build(header, headers.get(header))).collect(toSet());

View File

@@ -29,7 +29,6 @@ import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.DslProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.spec.internal.Headers;
import org.springframework.cloud.contract.spec.internal.Input;
import org.springframework.cloud.contract.spec.internal.OutputMessage;
import org.springframework.cloud.contract.spec.internal.Request;
import org.springframework.cloud.contract.spec.internal.Response;
@@ -183,12 +182,11 @@ public class SingleContractMetadata {
private DslProperty<?> inputBody(Contract contract) {
return Optional.ofNullable(contract.getRequest()).map(Request::getBody).map(DslProperty.class::cast)
.orElseGet(() -> Optional.ofNullable(contract.getInput()).map(Input::getMessageBody).orElse(null));
.orElse(null);
}
private Headers inputHeaders(Contract contract) {
return Optional.ofNullable(contract.getRequest()).map(Request::getHeaders)
.orElseGet(() -> Optional.ofNullable(contract.getInput()).map(Input::getMessageHeaders).orElse(null));
return Optional.ofNullable(contract.getRequest()).map(Request::getHeaders).orElse(null);
}
private DslProperty<?> outputBody(Contract contract) {

View File

@@ -56,7 +56,8 @@ public class ContractVerifierCamelConfiguration {
@Bean
@ConditionalOnMissingBean
public ContractVerifierMessaging<Message> contractVerifierMessaging(MessageVerifierSender<Message> sender, MessageVerifierReceiver<Message> receiver) {
public ContractVerifierMessaging<Message> contractVerifierMessaging(MessageVerifierSender<Message> sender,
MessageVerifierReceiver<Message> receiver) {
return new ContractVerifierCamelHelper(sender, receiver);
}

View File

@@ -49,7 +49,8 @@ public class ContractVerifierIntegrationConfiguration {
@Bean
@ConditionalOnMissingBean
public ContractVerifierMessaging<Message<?>> contractVerifierMessaging(MessageVerifierSender<Message<?>> sender, MessageVerifierReceiver<Message<?>> receiver) {
public ContractVerifierMessaging<Message<?>> contractVerifierMessaging(MessageVerifierSender<Message<?>> sender,
MessageVerifierReceiver<Message<?>> receiver) {
return new ContractVerifierHelper(sender, receiver);
}

View File

@@ -62,7 +62,8 @@ public class ContractVerifierJmsConfiguration {
@Bean
@ConditionalOnMissingBean
ContractVerifierMessaging<Message> contractVerifierJmsMessaging(MessageVerifierSender<Message> sender, MessageVerifierReceiver<Message> receiver) {
ContractVerifierMessaging<Message> contractVerifierJmsMessaging(MessageVerifierSender<Message> sender,
MessageVerifierReceiver<Message> receiver) {
return new ContractVerifierJmsHelper(sender, receiver);
}

View File

@@ -22,8 +22,6 @@ import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.context.annotation.Bean;

View File

@@ -19,7 +19,6 @@ package org.springframework.cloud.contract.verifier.messaging.stream;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver;
@@ -46,7 +45,8 @@ public class ContractVerifierStreamAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ContractVerifierMessaging<?> contractVerifierMessagingConverter(MessageVerifierSender<Message<?>> sender, MessageVerifierReceiver<Message<?>> receiver) {
public ContractVerifierMessaging<?> contractVerifierMessagingConverter(MessageVerifierSender<Message<?>> sender,
MessageVerifierReceiver<Message<?>> receiver) {
return new ContractVerifierHelper(sender, receiver);
}
@@ -63,20 +63,6 @@ public class ContractVerifierStreamAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingClass({ "org.springframework.cloud.stream.binder.test.InputDestination" })
static class NoOpStreamClassConfiguration {
@Bean
@ConditionalOnMissingBean
MessageVerifier<Message<?>> contractVerifierMessageExchangeWithNoMessageCollector(
ApplicationContext applicationContext) {
return new StreamStubMessages(new StreamStubMessageSender(applicationContext),
new StreamPollableChannelMessageReceiver(applicationContext));
}
}
}
class ContractVerifierHelper extends ContractVerifierMessaging<Message<?>> {

View File

@@ -1,67 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.stream;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.verifier.converter.YamlContract;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.context.ApplicationContext;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
/**
* @author Marcin Grzejszczak
*/
class StreamFromBinderMappingMessageSender implements MessageVerifierSender<Message<?>> {
private static final Log log = LogFactory.getLog(StreamFromBinderMappingMessageSender.class);
private final ApplicationContext context;
private final DestinationResolver resolver;
private final ContractVerifierStreamMessageBuilder builder = new ContractVerifierStreamMessageBuilder();
StreamFromBinderMappingMessageSender(ApplicationContext context, DestinationResolver resolver) {
this.context = context;
this.resolver = resolver;
}
@Override
public <T> void send(T payload, Map<String, Object> headers, String destination, YamlContract contract) {
send(this.builder.create(payload, headers), destination, contract);
}
@Override
public void send(Message<?> message, String destination, YamlContract contract) {
try {
MessageChannel messageChannel = this.context.getBean(
this.resolver.resolvedDestination(destination, DefaultChannels.OUTPUT), MessageChannel.class);
messageChannel.send(message);
}
catch (Exception e) {
log.error("Exception occurred while trying to send a message [" + message + "] "
+ "to a channel with name [" + destination + "]", e);
throw e;
}
}
}

View File

@@ -1,84 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.verifier.messaging.stream;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.verifier.converter.YamlContract;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver;
import org.springframework.context.ApplicationContext;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.SubscribableChannel;
class StreamPollableChannelMessageReceiver implements MessageVerifierReceiver<Message<?>> {
private static final Log log = LogFactory.getLog(StreamPollableChannelMessageReceiver.class);
private final ApplicationContext context;
private final DestinationResolver destinationResolver;
private final PollableChannel messageChannel;
StreamPollableChannelMessageReceiver(ApplicationContext context) {
this.context = context;
this.destinationResolver = new DestinationResolver(context);
this.messageChannel = new QueueChannel(1);
}
@Override
public Message<?> receive(String destination, long timeout, TimeUnit timeUnit, YamlContract contract) {
MessageHandler handler = this.messageChannel::send;
MessageChannel channel = null;
try {
channel = this.context.getBean(
this.destinationResolver.resolvedDestination(destination, DefaultChannels.INPUT),
MessageChannel.class);
if (channel instanceof SubscribableChannel) {
((SubscribableChannel) channel).subscribe(handler);
return this.messageChannel.receive(timeUnit.toMillis(timeout));
}
else if (channel instanceof PollableChannel) {
return ((PollableChannel) channel).receive(timeUnit.toMillis(timeout));
}
throw new IllegalStateException("Unsupported channel type");
}
catch (Exception e) {
log.error("Exception occurred while trying to read a message from " + " a channel with name [" + destination
+ "]", e);
throw new IllegalStateException(e);
}
finally {
if (channel instanceof SubscribableChannel) {
((SubscribableChannel) channel).unsubscribe(handler);
}
}
}
@Override
public Message<?> receive(String destination, YamlContract contract) {
return receive(destination, 5, TimeUnit.SECONDS, contract);
}
}

View File

@@ -234,550 +234,6 @@ public class FooTest {
test.trim() == messageWithoutTags(expectedMessage, "trigger_method_junit_test")
}
def "should generate tests triggered by a message for Spock"() {
given:
// tag::trigger_message_dsl[]
def contractDsl = Contract.make {
name "foo"
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')
}
}
}
// end::trigger_message_dsl[]
properties.testFramework = TestFramework.SPOCK
when:
String test = singleTestGenerator(contractDsl)
then:
String expectedMessage =
"""\
// tag::trigger_message_spock[]
package com.example
import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import spock.lang.Specification
import javax.inject.Inject
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes
@SuppressWarnings("rawtypes")
class FooSpec extends Specification {
@Inject ContractVerifierMessaging contractVerifierMessaging
@Inject ContractVerifierObjectMapper contractVerifierObjectMapper
def validate_foo() throws Exception {
given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
'''{"bookName":"foo"}'''
, headers()
.header("sample", "header")
)
when:
contractVerifierMessaging.send(inputMessage, "jms:input",
contract(this, "foo.yml"))
then:
ContractVerifierMessage response = contractVerifierMessaging.receive("jms:output",
contract(this, "foo.yml"))
response != null
and:
response.getHeader("BOOK-NAME") != null
response.getHeader("BOOK-NAME").toString() == 'foo'
and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo")
}
}
// end::trigger_message_spock[]
"""
test.trim() == messageWithoutTags(expectedMessage, "trigger_message_spock")
}
def "should generate tests triggered by a message for JUnit"() {
given:
def contractDsl = Contract.make {
name "foo"
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')
}
}
}
properties.testFramework = TestFramework.JUNIT
when:
String test = singleTestGenerator(contractDsl)
then:
String expectedMessage =
'''\
// tag::trigger_message_junit[]
package com.example;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.Test;
import org.junit.Rule;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes;
@SuppressWarnings("rawtypes")
public class FooTest {
@Inject ContractVerifierMessaging contractVerifierMessaging;
@Inject ContractVerifierObjectMapper contractVerifierObjectMapper;
@Test
public void validate_foo() throws Exception {
// given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
"{\\"bookName\\":\\"foo\\"}"
, headers()
.header("sample", "header")
);
// when:
contractVerifierMessaging.send(inputMessage, "jms:input",
contract(this, "foo.yml"));
// then:
ContractVerifierMessage response = contractVerifierMessaging.receive("jms:output",
contract(this, "foo.yml"));
assertThat(response).isNotNull();
// and:
assertThat(response.getHeader("BOOK-NAME")).isNotNull();
assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");
// and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo");
}
}
// end::trigger_message_junit[]
'''
test.trim() == messageWithoutTags(expectedMessage, "trigger_message_junit")
}
def "should generate tests without destination, triggered by a message"() {
given:
// tag::trigger_no_output_dsl[]
def contractDsl = Contract.make {
name "foo"
label 'some_label'
input {
messageFrom('jms:delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
// end::trigger_no_output_dsl[]
properties.testFramework = TestFramework.SPOCK
when:
String test = singleTestGenerator(contractDsl)
then:
String expectedMessage =
"""\
// tag::trigger_no_output_spock[]
package com.example
import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import spock.lang.Specification
import javax.inject.Inject
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes
@SuppressWarnings("rawtypes")
class FooSpec extends Specification {
@Inject ContractVerifierMessaging contractVerifierMessaging
@Inject ContractVerifierObjectMapper contractVerifierObjectMapper
def validate_foo() throws Exception {
given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
'''{"bookName":"foo"}'''
, headers()
.header("sample", "header")
)
when:
contractVerifierMessaging.send(inputMessage, "jms:delete",
contract(this, "foo.yml"))
bookWasDeleted()
then:
noExceptionThrown()
}
}
// end::trigger_no_output_spock[]
"""
test.trim() == messageWithoutTags(expectedMessage, "trigger_no_output_spock")
}
def "should generate tests without destination, triggered by a message for JUnit"() {
given:
def contractDsl = Contract.make {
name "foo"
label 'some_label'
input {
messageFrom('jms:delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
properties.testFramework = TestFramework.JUNIT
when:
String test = singleTestGenerator(contractDsl)
then:
String expectedMessage =
"""\
// tag::trigger_no_output_junit[]
package com.example;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.Test;
import org.junit.Rule;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes;
@SuppressWarnings("rawtypes")
public class FooTest {
@Inject ContractVerifierMessaging contractVerifierMessaging;
@Inject ContractVerifierObjectMapper contractVerifierObjectMapper;
@Test
public void validate_foo() throws Exception {
// given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
"{\\"bookName\\":\\"foo\\"}"
, headers()
.header("sample", "header")
);
// when:
contractVerifierMessaging.send(inputMessage, "jms:delete",
contract(this, "foo.yml"));
bookWasDeleted();
}
}
// end::trigger_no_output_junit[]
"""
test.trim() == messageWithoutTags(expectedMessage, "trigger_no_output_junit")
}
def "should generate tests without headers for JUnit"() {
given:
def contractDsl = Contract.make {
name "foo"
label 'some_label'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('jms:output')
body([
bookName: 'foo'
])
}
}
properties.testFramework = TestFramework.JUNIT
when:
String test = singleTestGenerator(contractDsl)
then:
String expectedMessage =
"""\
package com.example;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.Test;
import org.junit.Rule;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes;
@SuppressWarnings("rawtypes")
public class FooTest {
@Inject ContractVerifierMessaging contractVerifierMessaging;
@Inject ContractVerifierObjectMapper contractVerifierObjectMapper;
@Test
public void validate_foo() throws Exception {
// given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
"{\\"bookName\\":\\"foo\\"}"
, headers()
.header("sample", "header")
);
// when:
contractVerifierMessaging.send(inputMessage, "jms:input",
contract(this, "foo.yml"));
// then:
ContractVerifierMessage response = contractVerifierMessaging.receive("jms:output",
contract(this, "foo.yml"));
assertThat(response).isNotNull();
// and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo");
}
}
"""
test.trim() == messageWithoutTags(expectedMessage, "expectedMsg")
}
def "should generate tests without headers for Spock"() {
given:
def contractDsl = Contract.make {
name "foo"
label 'some_label'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('jms:output')
body([
bookName: 'foo'
])
}
}
properties.testFramework = TestFramework.SPOCK
when:
String test = singleTestGenerator(contractDsl)
then:
String expectedMessage =
"""\
package com.example
import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import spock.lang.Specification
import javax.inject.Inject
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes
@SuppressWarnings("rawtypes")
class FooSpec extends Specification {
@Inject ContractVerifierMessaging contractVerifierMessaging
@Inject ContractVerifierObjectMapper contractVerifierObjectMapper
def validate_foo() throws Exception {
given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
'''{"bookName":"foo"}'''
, headers()
.header("sample", "header")
)
when:
contractVerifierMessaging.send(inputMessage, "jms:input",
contract(this, "foo.yml"))
then:
ContractVerifierMessage response = contractVerifierMessaging.receive("jms:output",
contract(this, "foo.yml"))
response != null
and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo")
}
}
"""
test.trim() == messageWithoutTags(expectedMessage, "expectedMsg")
}
def "should generate tests without headers for JUnit with consumer / producer notation"() {
given:
def contractDsl =
// tag::consumer_producer[]
Contract.make {
name "foo"
label 'some_label'
input {
messageFrom value(consumer('jms:output'), producer('jms:input'))
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo $(consumer('jms:input'), producer('jms:output'))
body([
bookName: 'foo'
])
}
}
// end::consumer_producer[]
properties.testFramework = TestFramework.JUNIT
when:
String test = singleTestGenerator(contractDsl)
then:
String expectedMessage =
'''
package com.example;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.junit.Test;
import org.junit.Rule;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static com.toomuchcoding.jsonassert.JsonAssertion.assertThatJson;
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes;
@SuppressWarnings("rawtypes")
public class FooTest {
@Inject ContractVerifierMessaging contractVerifierMessaging;
@Inject ContractVerifierObjectMapper contractVerifierObjectMapper;
@Test
public void validate_foo() throws Exception {
// given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
"{\\"bookName\\":\\"foo\\"}"
, headers()
.header("sample", "header")
);
// when:
contractVerifierMessaging.send(inputMessage, "jms:input",
contract(this, "foo.yml"));
// then:
ContractVerifierMessage response = contractVerifierMessaging.receive("jms:output",
contract(this, "foo.yml"));
assertThat(response).isNotNull();
// and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
assertThatJson(parsedJson).field("['bookName']").isEqualTo("foo");
}
}
'''
test.trim() == messageWithoutTags(expectedMessage, "expectedMsg")
}
@Issue("336")
def "should generate tests with message headers containing regular expression for JUnit"() {
given:
@@ -1398,132 +854,6 @@ public class FooTest {
assertThatJson(parsedJson).field("['field']").isEqualTo("value");
}
}
"""
}
@Issue('#664')
def "should generate tests for messages having binary payloads [#methodBuilderName]"() {
given:
Contract contractDsl = Contract.make {
name "foo"
label 'shouldPublishMessage'
input {
messageFrom("foo")
messageBody(fileAsBytes("body_builder/request.pdf"))
messageHeaders {
messagingContentType(applicationOctetStream())
}
}
outputMessage {
sentTo('messageExchange')
body(fileAsBytes("body_builder/response.pdf"))
headers {
messagingContentType(applicationOctetStream())
}
}
}
methodBuilder()
when:
String test = singleTestGenerator(contractDsl)
then:
!test.contains('cursor')
!test.contains('REGEXP>>')
test.trim() == expectedTest.trim()
where:
methodBuilderName | methodBuilder | expectedTest
"spock" | { properties.testFramework = TestFramework.SPOCK } | """\
package com.example
import spock.lang.Specification
import javax.inject.Inject
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes
@SuppressWarnings("rawtypes")
class FooSpec extends Specification {
@Inject ContractVerifierMessaging contractVerifierMessaging
@Inject ContractVerifierObjectMapper contractVerifierObjectMapper
def validate_foo() throws Exception {
given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
fileToBytes(this, "foo_request_request.pdf")
, headers()
.header("contentType", "application/octet-stream")
)
when:
contractVerifierMessaging.send(inputMessage, "foo",
contract(this, "foo.yml"))
then:
ContractVerifierMessage response = contractVerifierMessaging.receive("messageExchange",
contract(this, "foo.yml"))
response != null
and:
response.getHeader("contentType") != null
response.getHeader("contentType").toString() == 'application/octet-stream'
and:
response.getPayloadAsByteArray() == fileToBytes(this, "foo_response_response.pdf")
}
}
"""
"junit" | { properties.testFramework = TestFramework.JUNIT } | """\
package com.example;
import org.junit.Test;
import org.junit.Rule;
import javax.inject.Inject;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.*;
import static org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil.headers;
import static org.springframework.cloud.contract.verifier.util.ContractVerifierUtil.fileToBytes;
@SuppressWarnings("rawtypes")
public class FooTest {
@Inject ContractVerifierMessaging contractVerifierMessaging;
@Inject ContractVerifierObjectMapper contractVerifierObjectMapper;
@Test
public void validate_foo() throws Exception {
// given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
fileToBytes(this, "foo_request_request.pdf")
, headers()
.header("contentType", "application/octet-stream")
);
// when:
contractVerifierMessaging.send(inputMessage, "foo",
contract(this, "foo.yml"));
// then:
ContractVerifierMessage response = contractVerifierMessaging.receive("messageExchange",
contract(this, "foo.yml"));
assertThat(response).isNotNull();
// and:
assertThat(response.getHeader("contentType")).isNotNull();
assertThat(response.getHeader("contentType").toString()).isEqualTo("application/octet-stream");
// and:
assertThat(response.getPayloadAsByteArray()).isEqualTo(fileToBytes(this, "foo_response_response.pdf"));
}
}
"""
}

View File

@@ -331,13 +331,7 @@ class SingleTestGeneratorSpec extends Specification {
ignored()
label 'some_label'
input {
messageFrom('delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
triggeredBy("hashCode()")
assertThat('hashCode()')
}
}

View File

@@ -64,11 +64,8 @@ class YamlContractConverterSpec extends Specification {
@Shared
File ymlWithRest3 = new File(ymlUrl3.toURI())
URL ymlMsgUrl = YamlContractConverterSpec.getResource("/yml/contract_message.yml")
File ymlMessaging = new File(ymlMsgUrl.toURI())
URL ymlMsgMethodUrl = YamlContractConverterSpec.getResource("/yml/contract_message_method.yml")
File ymlMessagingMethod = new File(ymlMsgMethodUrl.toURI())
URL ymlMsgMsgUrl = YamlContractConverterSpec.getResource("/yml/contract_message_input_message.yml")
File ymlMessagingMsg = new File(ymlMsgMsgUrl.toURI())
URL ymlBodyFile = YamlContractConverterSpec.getResource("/yml/contract_from_file.yml")
File ymlBody = new File(ymlBodyFile.toURI())
URL ymlReferenceFile = YamlContractConverterSpec.getResource("/yml/contract_reference_request.yml")
@@ -413,38 +410,7 @@ class YamlContractConverterSpec extends Specification {
Collection<Contract> contracts = converter.convertFrom(ymlMessagingMatchers)
then:
contracts.size() == 1
Contract contract = contracts.first()
contract.input.messageHeaders.entries.find {
it.name == "contentType" &&
((Pattern) it.clientValue).pattern() == "application/json.*" && it.serverValue == "application/json"
}
contract.input.bodyMatchers.matchers[0].path() == '$.duck'
contract.input.bodyMatchers.matchers[0].matchingType() == REGEX
contract.input.bodyMatchers.matchers[0].value().pattern() == '[0-9]{3}'
contract.input.bodyMatchers.matchers[1].path() == '$.duck'
contract.input.bodyMatchers.matchers[1].matchingType() == EQUALITY
contract.input.bodyMatchers.matchers[2].path() == '$.alpha'
contract.input.bodyMatchers.matchers[2].matchingType() == REGEX
contract.input.bodyMatchers.matchers[2].value().pattern() == RegexPatterns.onlyAlphaUnicode().pattern()
contract.input.bodyMatchers.matchers[3].path() == '$.alpha'
contract.input.bodyMatchers.matchers[3].matchingType() == EQUALITY
contract.input.bodyMatchers.matchers[4].path() == '$.number'
contract.input.bodyMatchers.matchers[4].matchingType() == REGEX
contract.input.bodyMatchers.matchers[4].value().pattern() == RegexPatterns.number().pattern()
contract.input.bodyMatchers.matchers[5].path() == '$.aBoolean'
contract.input.bodyMatchers.matchers[5].matchingType() == REGEX
contract.input.bodyMatchers.matchers[5].value().pattern() == RegexPatterns.anyBoolean().pattern()
contract.input.bodyMatchers.matchers[6].path() == '$.date'
contract.input.bodyMatchers.matchers[6].matchingType() == DATE
contract.input.bodyMatchers.matchers[6].value().pattern() == RegexPatterns.isoDate().pattern()
contract.input.bodyMatchers.matchers[7].path() == '$.dateTime'
contract.input.bodyMatchers.matchers[7].matchingType() == TIMESTAMP
contract.input.bodyMatchers.matchers[7].value().pattern() == RegexPatterns.isoDateTime().pattern()
contract.input.bodyMatchers.matchers[8].path() == '$.time'
contract.input.bodyMatchers.matchers[8].matchingType() == TIME
contract.input.bodyMatchers.matchers[8].value().pattern() == RegexPatterns.isoTime().pattern()
contract.input.bodyMatchers.matchers[9].path() == "\$.['key'].['complex.key']"
contract.input.bodyMatchers.matchers[9].matchingType() == EQUALITY
Contract contract = contracts[0]
and:
contract.outputMessage.bodyMatchers.matchers[0].path() == '$.duck'
contract.outputMessage.bodyMatchers.matchers[0].matchingType() == REGEX
@@ -534,51 +500,6 @@ class YamlContractConverterSpec extends Specification {
contract.response.status.serverValue == 200
}
def "should convert YAML with messaging to DSL"() {
given:
assert converter.isAccepted(ymlMessaging)
when:
Collection<Contract> contracts = converter.convertFrom(ymlMessaging)
then:
contracts.size() == 1
Contract contract = contracts.first()
contract.description == "Some description"
contract.name == "some name"
contract.label == "some_label"
contract.ignored == true
contract.input.assertThat.toString() == "bar()"
contract.input.messageFrom.serverValue == "foo"
contract.input.triggeredBy.toString() == "foo()"
contract.input.messageHeaders.entries.find {
it.name == "foo" &&
((Pattern) it.clientValue).pattern() == "bar" && it.serverValue == "bar"
}
contract.input.messageBody.clientValue == [foo: "bar"]
contract.input.bodyMatchers.matchers[0].path() == '$.bar'
contract.input.bodyMatchers.matchers[0].matchingType() == REGEX
contract.input.bodyMatchers.matchers[0].value().pattern() == 'bar'
and:
contract.outputMessage.assertThat.toString() == "baz()"
contract.outputMessage.headers.entries.find {
it.name == "foo2" &&
((Pattern) it.serverValue).pattern() == "bar" && it.clientValue == "bar"
}
contract.outputMessage.headers.entries.find {
it.name == "foo3" &&
((ExecutionProperty) it.serverValue).insertValue('foo') == "andMeToo(foo)"
}
contract.outputMessage.headers.entries.find {
it.name == "fooRes" &&
it.clientValue == "baz"
}
contract.outputMessage.body.clientValue == [foo2: "bar", foo3: "baz"]
contract.outputMessage.bodyMatchers.matchers[0].path() == '$.foo2'
contract.outputMessage.bodyMatchers.matchers[0].matchingType() == REGEX
contract.outputMessage.bodyMatchers.matchers[0].value().pattern() == 'bar'
contract.outputMessage.bodyMatchers.matchers[1].path() == '$.foo3'
contract.outputMessage.bodyMatchers.matchers[1].matchingType() == COMMAND
contract.outputMessage.bodyMatchers.matchers[1].value() == new ExecutionProperty('executeMe($it)')
}
def "should convert YAML with messaging triggered by a method to DSL"() {
given:
@@ -599,30 +520,6 @@ class YamlContractConverterSpec extends Specification {
contract.outputMessage.body.clientValue == [bookName: "foo"]
}
def "should convert YAML with messaging triggered by a message to DSL"() {
given:
assert converter.isAccepted(ymlMessagingMsg)
when:
Collection<Contract> contracts = converter.convertFrom(ymlMessagingMsg)
then:
contracts.size() == 1
Contract contract = contracts.first()
contract.description == "Some description"
contract.label == "some_label"
contract.input.messageFrom.serverValue == "input"
contract.input.messageHeaders.entries.find {
it.name == "sample" &&
it.serverValue == "header"
}
contract.input.messageBody.clientValue == [bookName: "foo"]
and:
contract.outputMessage.sentTo.clientValue == "output"
contract.outputMessage.headers.entries.find {
it.name == "BOOK-NAME" && it.clientValue == "foo"
}
contract.outputMessage.body.clientValue == [bookName: "foo"]
}
def "should convert YAML with HTTP binary body to DSL"() {
given:
assert converter.isAccepted(ymlBytes)
@@ -646,8 +543,6 @@ class YamlContractConverterSpec extends Specification {
then:
contracts.size() == 1
Contract contract = contracts.first()
contract.input.messageBody.clientValue instanceof FromFileProperty
((FromFileProperty) contract.input.messageBody.clientValue).type == byte[]
and:
contract.outputMessage.body.clientValue instanceof FromFileProperty
((FromFileProperty) contract.outputMessage.body.clientValue).type == byte[]
@@ -800,8 +695,10 @@ metadata: {}
)]
}
def "should parse messaging contract for [#file]"() {
def "should parse messaging contract for messaging scenario 1"() {
given:
URI uri = YamlContractConverterSpec.getResource("/yml/contract_message_scenario1.yml").toURI()
File file = new File(uri)
assert converter.isAccepted(file)
when:
Collection<Contract> contracts = converter.convertFrom(file)
@@ -809,10 +706,6 @@ metadata: {}
contracts.size() == 1
and:
contracts.first().input != null || contracts.first().outputMessage != null
where:
file << [1, 2, 3].collect {
new File(YamlContractConverterSpec.getResource("/yml/contract_message_scenario${it}.yml").toURI())
}
}
def "should convert HTTP DSL to YAML"() {
@@ -949,59 +842,6 @@ metadata: {}
yamlContract.outputMessage.headers == ["BOOK-NAME": "foo"]
}
def "should convert Messaging DSL with input and output message to YAML"() {
given:
List<Contract> contracts = [Contract.make {
input {
messageFrom("jms:input")
messageBody([bookName: 'foo'])
messageHeaders {
header("sample", "header")
}
}
outputMessage {
sentTo("output")
body([bookName: "foo"])
headers {
header("BOOK-NAME", "foo")
}
}
}]
when:
Collection<YamlContract> yamlContracts = converter.convertTo(contracts)
then:
yamlContracts.size() == 1
YamlContract yamlContract = yamlContracts.first()
yamlContract.input.messageFrom == "jms:input"
yamlContract.input.messageBody == [bookName: 'foo']
yamlContract.input.messageHeaders == ["sample": "header"]
yamlContract.outputMessage.sentTo == "output"
yamlContract.outputMessage.body == [bookName: "foo"]
yamlContract.outputMessage.headers == ["BOOK-NAME": "foo"]
}
def "should convert Messaging DSL with only input message to YAML"() {
given:
List<Contract> contracts = [Contract.make {
input {
messageFrom("jms:input")
messageBody([bookName: 'foo'])
messageHeaders {
header("sample", "header")
}
assertThat("bookWasDeleted()")
}
}]
when:
Collection<YamlContract> yamlContracts = converter.convertTo(contracts)
then:
yamlContracts.size() == 1
YamlContract yamlContract = yamlContracts.first()
yamlContract.input.messageFrom == "jms:input"
yamlContract.input.messageBody == [bookName: 'foo']
yamlContract.input.messageHeaders == ["sample": "header"]
yamlContract.input.assertThat == "bookWasDeleted()"
}
def "should convert Messaging with a message DSL to YAML"() {
given:
@@ -1013,35 +853,7 @@ metadata: {}
ignored()
inProgress()
input {
messageFrom("input")
messageBody([
duck : 123,
alpha : "abc",
number : 123,
aBoolean : true,
date : "2017-01-01",
dateTime : "2017-01-01T01:23:45",
time : "01:02:34",
valueWithoutAMatcher: "foo",
valueWithTypeMatch : "string",
key : ["complex.key": 'foo']
])
bodyMatchers {
jsonPath('$.duck', byRegex("[0-9]{3}"))
jsonPath('$.duck', byEquality())
jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
jsonPath('$.alpha', byEquality())
jsonPath('$.number', byRegex(number()))
jsonPath('$.aBoolean', byRegex(anyBoolean()))
jsonPath('$.date', byDate())
jsonPath('$.dateTime', byTimestamp())
jsonPath('$.time', byTime())
jsonPath("\$.['key'].['complex.key']", byEquality())
}
messageHeaders {
header("sample", $(c(regex("foo.*")), p("foo")))
messagingContentType(applicationJson())
}
triggeredBy("foo()")
}
outputMessage {
sentTo("channel")
@@ -1121,67 +933,8 @@ metadata: {}
yamlContract.name == "fooo"
yamlContract.ignored == true
yamlContract.inProgress == true
yamlContract.input.triggeredBy == "foo()"
yamlContract.label == "card_rejected"
yamlContract.input.messageFrom == "input"
yamlContract.input.messageBody == [
duck : 123,
alpha : "abc",
number : 123,
aBoolean : true,
date : "2017-01-01",
dateTime : "2017-01-01T01:23:45",
time : "01:02:34",
valueWithoutAMatcher: "foo",
valueWithTypeMatch : "string",
key : ["complex.key": 'foo']
]
yamlContract.input.messageHeaders == [
sample : 'foo',
contentType: "application/json"
]
yamlContract.input.matchers.headers == [
new YamlContract.KeyValueMatcher(
key: "sample", regex: "foo.*", regexType: YamlContract.RegexType.as_string)
]
yamlContract.input.matchers.body == [
new YamlContract.BodyStubMatcher(
path: '$.duck',
type: YamlContract.StubMatcherType.by_regex,
value: "[0-9]{3}"),
new YamlContract.BodyStubMatcher(
path: '$.duck',
type: YamlContract.StubMatcherType.by_equality),
new YamlContract.BodyStubMatcher(
path: '$.alpha',
type: YamlContract.StubMatcherType.by_regex,
value: "[\\p{L}]*"),
new YamlContract.BodyStubMatcher(
path: '$.alpha',
type: YamlContract.StubMatcherType.by_equality),
new YamlContract.BodyStubMatcher(
path: '$.number',
type: YamlContract.StubMatcherType.by_regex,
value: "-?(\\d*\\.\\d+|\\d+)"),
new YamlContract.BodyStubMatcher(
path: '$.aBoolean',
type: YamlContract.StubMatcherType.by_regex,
value: "(true|false)"),
new YamlContract.BodyStubMatcher(
path: '$.date',
type: YamlContract.StubMatcherType.by_date,
value: "(\\d\\d\\d\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])"),
new YamlContract.BodyStubMatcher(
path: '$.dateTime',
type: YamlContract.StubMatcherType.by_timestamp,
value: "([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])"),
new YamlContract.BodyStubMatcher(
path: '$.time',
type: YamlContract.StubMatcherType.by_time,
value: "(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])"),
new YamlContract.BodyStubMatcher(
path: "\$.['key'].['complex.key']",
type: YamlContract.StubMatcherType.by_equality),
]
yamlContract.outputMessage.sentTo == "channel"
yamlContract.outputMessage.body == [duck : 123,
alpha : "abc",

View File

@@ -25,7 +25,7 @@ class ContractVerifierHelperForStreamTest extends Specification {
def 'should throw exception when a null payload was sent'() {
given:
ContractVerifierHelper helper = new ContractVerifierHelper(null)
ContractVerifierHelper helper = new ContractVerifierHelper(null, null)
when:
helper.convert(null)
then:

View File

@@ -3,21 +3,8 @@ label: some_label
name: some name
ignored: true
input:
messageFrom: foo
triggeredBy: foo()
messageHeaders:
foo: bar
messageBody:
foo: bar
assertThat: bar()
matchers:
body:
- path: $.bar
type: by_regex
value: bar
headers:
- key: foo
regex: bar
outputMessage:
sentTo: bar
headers:

View File

@@ -1,23 +0,0 @@
# Human readable description
description: Some description
# Label by means of which the output message can be triggered
label: some_label
# input is a message
input:
messageFrom: input
# has the following body
messageBody:
bookName: 'foo'
# and the following headers
messageHeaders:
sample: 'header'
# output message of the contract
outputMessage:
# destination to which the output message will be sent
sentTo: output
# the body of the output message
body:
bookName: foo
# the headers of the output message
headers:
BOOK-NAME: foo

View File

@@ -1,50 +1,6 @@
label: card_rejected
input:
messageFrom: input
messageBody:
duck: 123
alpha: "abc"
number: 123
aBoolean: true
date: "2017-01-01"
dateTime: "2017-01-01T01:23:45"
time: "01:02:34"
valueWithoutAMatcher: "foo"
valueWithTypeMatch: "string"
key:
"complex.key": 'foo'
messageHeaders:
sample: 'header'
contentType: application/json
matchers:
headers:
- key: contentType
regex: "application/json.*"
body:
- path: $.duck
type: by_regex
value: "[0-9]{3}"
- path: $.duck
type: by_equality
- path: $.alpha
type: by_regex
predefined: only_alpha_unicode
- path: $.alpha
type: by_equality
- path: $.number
type: by_regex
predefined: number
- path: $.aBoolean
type: by_regex
predefined: any_boolean
- path: $.date
type: by_date
- path: $.dateTime
type: by_timestamp
- path: $.time
type: by_time
- path: "$.['key'].['complex.key']"
type: by_equality
triggeredBy: "foo()"
outputMessage:
sentTo: channel
body:

View File

@@ -1,13 +0,0 @@
label: some_label
input:
messageFrom: jms:input
messageBody:
bookName: 'foo'
messageHeaders:
sample: header
outputMessage:
sentTo: jms:output
body:
bookName: foo
headers:
BOOK-NAME: foo

View File

@@ -1,8 +0,0 @@
label: some_label
input:
messageFrom: jms:delete
messageBody:
bookName: 'foo'
messageHeaders:
sample: header
assertThat: bookWasDeleted()

View File

@@ -1,9 +1,6 @@
label: some_label
input:
messageFrom: jms:input
messageBodyFromFileAsBytes: request.pdf
messageHeaders:
contentType: application/octet-stream
triggeredBy: foo()
outputMessage:
sentTo: jms:output
bodyFromFileAsBytes: response.pdf

View File

@@ -49,7 +49,6 @@
<module>samples-messaging-integration</module>
<module>samples-messaging-amqp</module>
<module>samples-messaging-jms</module>
<module>spring-cloud-contract-stub-runner-camel</module>
<module>spring-cloud-contract-stub-runner-boot-eureka</module>
<module>spring-cloud-contract-stub-runner-boot-zookeeper</module>
<module>spring-cloud-contract-stub-runner-context-path</module>
@@ -72,7 +71,6 @@
<module>samples-messaging-integration</module>
<module>samples-messaging-amqp</module>
<module>samples-messaging-jms</module>
<module>spring-cloud-contract-stub-runner-camel</module>
<module>spring-cloud-contract-stub-runner-boot-eureka</module>
<module>spring-cloud-contract-stub-runner-boot-zookeeper</module>
<module>spring-cloud-contract-stub-runner-context-path</module>

View File

@@ -87,115 +87,6 @@ class AmqpMessagingApplicationSpec {
JsonAssertion.assertThat(parsedJson).field('name').isEqualTo('some')
}
// @Issue("332")
@Test
void should_work_for_second_scenario() {
// given:
def dsl =
Contract.make {
description("""
Represents scenario 2 from documentation:
https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html#_publisher_side_test_generation
"The input message triggers an output message."
```
// given:
rabbit service is running
// when:
input message is received
// then:
message is send
```
""")
label 'some_label2'
input {
messageFrom('input')
messageBody([
name: 'foo2'
])
messageHeaders {
messagingContentType(applicationJson())
header('amqp_replyTo', 'amq.rabbitmq.reply-to')
header('bill', 'bill')
}
}
outputMessage {
sentTo('')
body('''{ "name" : "foo2" }''')
headers {
messagingContentType(applicationJson())
}
}
}
// generated test should look like this:
// and:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
"{\"name\":\"foo2\"}"
, headers()
.header("contentType", "application/json")
.header("amqp_replyTo", "amq.rabbitmq.reply-to")
.header("bill", "bill")
)
// when:
contractVerifierMessaging.send(inputMessage, "input")
// then:
ContractVerifierMessage response = contractVerifierMessaging.receive("")
assertThat(response).isNotNull()
assertThat(response.getHeader("contentType")).isNotNull()
assertThat(response.getHeader("contentType").toString()).
isEqualTo("application/json")
// and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.
writeValueAsString(response.getPayload()))
assertThatJson(parsedJson).field("['name']").isEqualTo("foo2")
}
// @Issue("178")
@Test
void should_work_for_input_output_when_bytes_are_used() {
// given:
def inputBody = [
ratedItemId: "992e46d8-ab05-4a26-a740-6ef7b0daeab3",
eventType : "CREATED"
]
def dsl = Contract.make {
label 'ratedItem-no-metricid'
input {
messageFrom("rated-item-service.rated-item-event.exchange")
messageHeaders {
header("X-tenant", "1234")
header("contentType", "application/json")
}
messageBody(inputBody)
}
outputMessage {
sentTo('bill-service.rated-item-event.retry-exchange')
body(
ratedItemId: "992e46d8-ab05-4a26-a740-6ef7b0daeab3",
eventType: "CREATED"
)
}
}
// when:
contractVerifierMessaging.send(contractVerifierMessaging.
create(new JsonOutput().toJson(inputBody), [
"X-tenant" : "1234",
"contentType": "application/json"
]), "rated-item-service.rated-item-event.exchange")
// then:
def response = contractVerifierMessaging.
receive('bill-service.rated-item-event.retry-exchange')
// and:
DocumentContext parsedJson = JsonPath.
parse(contractVerifierObjectMapper.writeValueAsString(response.payload))
JsonAssertion.assertThat(parsedJson).field('ratedItemId').
isEqualTo('992e46d8-ab05-4a26-a740-6ef7b0daeab3')
JsonAssertion.assertThat(parsedJson).field('eventType').isEqualTo('CREATED')
}
// BASE CLASS WOULD HAVE THIS:
@Autowired

View File

@@ -1,34 +0,0 @@
/*
* 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 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;
}
}

View File

@@ -1,48 +0,0 @@
/*
* 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 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);
private AtomicBoolean bookSuccessfulyDeleted = new AtomicBoolean(false);
/**
* 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.
* @param exchange - input exchange.
*/
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 + "]");
}
}

View File

@@ -33,7 +33,7 @@ import org.springframework.context.annotation.Configuration;
public class BookRouteConfiguration {
@Bean
RoutesBuilder myRouter(final BookService bookService, final BookDeleter bookDeleter, CamelContext context,
RoutesBuilder myRouter(final BookService bookService, CamelContext context,
@Value("${spring.rabbitmq.port}") int port) {
return new RouteBuilder() {
@@ -45,13 +45,6 @@ public class BookRouteConfiguration {
// scenario 1 - from bean to output
from("direct:start").unmarshal().json(JsonLibrary.Jackson, BookReturned.class).bean(bookService)
.marshal().json(JsonLibrary.Jackson, BookReturned.class).to("rabbitmq:output?queue=output");
// scenario 2 - from input to output
from("rabbitmq:input?queue=input").unmarshal().json(JsonLibrary.Jackson, BookReturned.class)
.bean(bookService).marshal().json(JsonLibrary.Jackson, BookReturned.class)
.to("rabbitmq:output");
// scenario 3 - from input to no output
from("rabbitmq:delete?queue=delete").unmarshal().json(JsonLibrary.Jackson, BookDeleted.class)
.bean(bookDeleter);
}
};

View File

@@ -29,9 +29,7 @@ public class BookService {
/**
* 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.
* a possibility to "trigger" sending of a message to the given message
* @param exchange - input exchange.
*/
public void returnBook(Exchange exchange) {

View File

@@ -23,7 +23,6 @@ import com.jayway.jsonpath.JsonPath
import com.toomuchcoding.jsonassert.JsonAssertion
import org.apache.camel.Message
import org.apache.camel.model.ModelCamelContext
import org.awaitility.Awaitility
import org.junit.jupiter.api.Test
import org.testcontainers.containers.RabbitMQContainer
import org.testcontainers.junit.jupiter.Container
@@ -49,8 +48,6 @@ class CamelMessagingApplicationSpec {
// ALL CASES
@Autowired
ModelCamelContext camelContext
@Autowired
BookDeleter bookDeleter
@Inject
MessageVerifier<Message> contractVerifierMessaging
@@ -67,24 +64,26 @@ class CamelMessagingApplicationSpec {
@Test
void "should work for triggered based messaging"() {
given:
// tag::sample_dsl[]
Contract.make {
label 'some_label'
label 'return_book_1'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('rabbitmq:output')
sentTo('rabbitmq:output?queue=output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
}
}
}
// end::sample_dsl[]
// generated test should look like this:
when:
bookReturnedTriggered()
then:
def response = contractVerifierMessaging.receive('rabbitmq:output')
def response = contractVerifierMessaging.receive('rabbitmq:output?queue=output')
assert response.headers.get('BOOK-NAME') == 'foo'
and:
DocumentContext parsedJson = JsonPath.
@@ -92,78 +91,9 @@ class CamelMessagingApplicationSpec {
JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo')
}
@Test
void "should generate tests triggered by a message"() {
given:
Contract.make {
label 'some_label'
input {
messageFrom('rabbitmq:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('rabbitmq:output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}
// generated test should look like this:
when:
contractVerifierMessaging.send(
contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']),
[sample: 'header'], 'rabbitmq:input')
then:
def response = contractVerifierMessaging.receive('rabbitmq:output')
assert response.headers.get('BOOK-NAME') == 'foo'
and:
DocumentContext parsedJson = JsonPath.
parse(contractVerifierObjectMapper.writeValueAsString(response.body))
JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo')
}
@Test
void "should generate tests without destination, triggered by a message"() {
given:
Contract.make {
label 'some_label'
input {
messageFrom('rabbitmq: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'], 'rabbitmq:delete')
then:
bookWasDeleted()
}
void bookReturnedTriggered() {
camelContext.createProducerTemplate().
sendBody('direct:start', '''{"bookName" : "foo" }''')
}
void bookWasDeleted() {
Awaitility.await().untilAsserted(() -> {
assert bookDeleter.bookSuccessfulyDeleted.get()
})
}
}

View File

@@ -16,8 +16,6 @@
package com.example;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -28,8 +26,6 @@ public class BookListener {
private static final Logger log = LoggerFactory.getLogger(BookListener.class);
public AtomicBoolean bookSuccessfullyDeleted = new AtomicBoolean(false);
/**
* 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
@@ -43,16 +39,4 @@ public class BookListener {
return MessageBuilder.withPayload(bookReturned).setHeader("BOOK-NAME", bookReturned.bookName).build();
}
/**
* 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.
* @param bookDeleted - payload
*/
public void bookDeleted(BookDeleted bookDeleted) {
log.info("Deleting book [ " + bookDeleted + "]");
this.bookSuccessfullyDeleted.set(true);
}
}

View File

@@ -23,25 +23,8 @@
http://www.springframework.org/schema/integration
http://www.springframework.org/schema/integration/spring-integration.xsd">
<channel id="input"/>
<channel id="delete"/>
<channel id="outputChannel"/>
<service-activator input-channel="inputChannel"
output-channel="outputChannel"
ref="bookListener"
method="returnBook"/>
<json-to-object-transformer input-channel="input" type="com.example.BookReturned"
output-channel="inputChannel"/>
<service-activator input-channel="deleteChannel"
ref="bookListener"
method="bookDeleted"/>
<json-to-object-transformer input-channel="delete" type="com.example.BookDeleted"
output-channel="deleteChannel"/>
<beans:bean id="bookListener" class="com.example.BookListener"/>

View File

@@ -81,79 +81,6 @@ class IntegrationMessagingApplicationSpec {
JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo')
}
void "should generate tests triggered by a message"() {
given:
// tag::message_trigger[]
def dsl = Contract.make {
description 'Some Description'
label 'some_label'
// input is a message
input {
// the message was received from this destination
messageFrom('input')
// has the following body
messageBody([
bookName: 'foo'
])
// and the following headers
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}
// end::message_trigger[]
// generated test should look like this:
when:
contractVerifierMessaging.send(
contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']),
[sample: 'header'], 'input')
then:
def response = contractVerifierMessaging.receive('output')
response.headers.get('BOOK-NAME') == 'foo'
and:
DocumentContext parsedJson = JsonPath.
parse(contractVerifierObjectMapper.writeValueAsString(response.payload))
JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo')
}
@Test
void "should generate tests without destination, triggered by a message"() {
given:
def dsl = Contract.make {
label 'some_label'
input {
messageFrom('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'], 'delete')
then:
bookWasDeleted()
}
// BASE CLASS WOULD HAVE THIS:
@Autowired
@@ -165,8 +92,4 @@ class IntegrationMessagingApplicationSpec {
bookService.returnBook(new BookReturned("foo"))
}
void bookWasDeleted() {
assert bookListener.bookSuccessfullyDeleted.get()
}
}

View File

@@ -1,51 +0,0 @@
/*
* 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 com.example;
import java.util.concurrent.atomic.AtomicBoolean;
import jakarta.jms.JMSException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
@Component
public class BookDeleter {
private static final Logger log = LoggerFactory.getLogger(BookDeleter.class);
private AtomicBoolean bookSuccessfulyDeleted = new AtomicBoolean(false);
/**
* 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.
* @param message - input message.
* @throws JMSException - jms exception.
*/
@JmsListener(destination = "delete")
public void bookDeleted(Message message) throws JMSException {
log.info("Deleting book " + message);
this.bookSuccessfulyDeleted.set(true);
log.info("Book successfully deleted [" + this.bookSuccessfulyDeleted + "]");
}
}

View File

@@ -50,8 +50,6 @@ class JmsMessagingApplicationSpec {
// ALL CASES
@Autowired
JmsTemplate jmsTemplate
@Autowired
BookDeleter bookDeleter
@Inject
MessageVerifier<Message> messageVerifier
@Inject
@@ -92,70 +90,6 @@ class JmsMessagingApplicationSpec {
JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo')
}
@DirtiesContext
@Test
void "should generate tests triggered by a message"() {
given:
Contract.make {
label 'some_label'
input {
messageFrom('input2')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('output2')
body([
bookName: 'foo'
])
headers {
header('BOOKNAME', 'foo')
}
}
}
// generated test should look like this:
when:
messageVerifier.send(
contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']),
[sample: 'header'], 'input2')
then:
ContractVerifierMessage response = contractVerifierMessaging.receive('output2')
assert response.getHeader('BOOKNAME') == 'foo'
and:
DocumentContext parsedJson = JsonPath.
parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo')
}
@Test
void "should generate tests without destination, triggered by a message"() {
given:
Contract.make {
label 'some_label'
input {
messageFrom('delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
// generated test should look like this:
when:
messageVerifier.
send(contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']),
[sample: 'header'], 'delete')
then:
bookWasDeleted()
}
void bookReturnedTriggered() {
jmsTemplate.convertAndSend("output", '''{"bookName" : "foo" }''', new MessagePostProcessor() {
@Override
@@ -166,10 +100,4 @@ class JmsMessagingApplicationSpec {
})
}
void bookWasDeleted() {
Awaitility.await().untilAsserted( () -> {
assert bookDeleter.bookSuccessfulyDeleted.get()
})
}
}

View File

@@ -1,67 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-tests</artifactId>
<version>4.0.0-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-contract-stub-runner-camel</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Contract Stub Runner Camel</name>
<description>Spring Cloud Contract Stub Runner Camel</description>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-stub-runner</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner-jetty</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-jackson</artifactId>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-activemq</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -1,32 +0,0 @@
/*
* 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 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
}
}

View File

@@ -1,120 +0,0 @@
/*
* 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 org.apache.camel.CamelContext
import org.apache.camel.Exchange
import org.apache.camel.builder.ExchangeBuilder
import org.apache.camel.spring.SpringCamelContext
import spock.lang.Specification
import org.springframework.cloud.contract.spec.Contract
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()
when:
message.in.body = new StubRunnerCamelPayload(noOutputMessageContract)
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()
when:
message.in.body = new StubRunnerCamelPayload(dsl)
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()
when:
message.in.body = new StubRunnerCamelPayload(dslWithRegexInGString)
processor.process(message)
then:
message.getIn().getBody(String) == '''{"id":"99","temperature":"123.45"}'''
}
}

View File

@@ -1,8 +0,0 @@
stubrunner:
repository-root: classpath:m2repo/repository/
ids: org.springframework.cloud.contract.verifier.stubs:camelService:0.0.1-SNAPSHOT:stubs
stubs-mode: remote
server:
port: 0
debug: true
logging.level.org.springframework.cloud.contract: debug

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.cloud.contract.verifier.stubs</groupId>
<artifactId>camelService</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
</project>

Some files were not shown because too many files have changed in this diff Show More