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:
@@ -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 )"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()')
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 + '\'' + '}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<?>> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
"""
|
||||
}
|
||||
|
||||
@@ -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()')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -1,8 +0,0 @@
|
||||
label: some_label
|
||||
input:
|
||||
messageFrom: jms:delete
|
||||
messageBody:
|
||||
bookName: 'foo'
|
||||
messageHeaders:
|
||||
sample: header
|
||||
assertThat: bookWasDeleted()
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 + "]");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"}'''
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
Binary file not shown.
@@ -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
Reference in New Issue
Block a user