Merge pull request #1831 from spring-cloud/polishing

Major refactoring
This commit is contained in:
Marcin Grzejszczak
2022-11-15 17:24:08 +01:00
committed by GitHub
202 changed files with 2113 additions and 10508 deletions

View File

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

View File

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

View File

@@ -205,6 +205,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.

View File

@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View File

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

View File

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

View File

@@ -40,6 +40,13 @@
<type>pom</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-dependencies</artifactId>
<version>${spring-cloud-netflix.version}</version>
<type>pom</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-zookeeper-dependencies</artifactId>

View File

@@ -19,8 +19,7 @@
<main.basedir>${basedir}/..</main.basedir>
<maven.plugin.plugin.version>3.4</maven.plugin.plugin.version>
<configprops.inclusionPattern>stubrunner.*|wiremock.*|</configprops.inclusionPattern>
<!-- TODO: Remove me -->
<!--<upload-docs-zip.phase>deploy</upload-docs-zip.phase>-->
<upload-docs-zip.phase>deploy</upload-docs-zip.phase>
<!-- Aligned with Groovy in SC-Build -->
<groovy.version>4.0.0</groovy.version>
</properties>
@@ -149,11 +148,10 @@
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
</plugin>
<!-- TODO: Remove me -->
<!--<plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
</plugin>-->
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
</plugin>

View File

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

View File

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

94
pom.xml
View File

@@ -30,11 +30,12 @@
<spring-cloud-build.version>4.0.0-SNAPSHOT</spring-cloud-build.version>
<spring-cloud-zookeeper.version>4.0.0-SNAPSHOT</spring-cloud-zookeeper.version>
<spring-cloud-stream.version>4.0.0-SNAPSHOT</spring-cloud-stream.version>
<spring-cloud-netflix.version>4.0.0-SNAPSHOT</spring-cloud-netflix.version>
<spring-cloud-consul.version>4.0.0-SNAPSHOT</spring-cloud-consul.version>
<spring-cloud-commons.version>4.0.0-SNAPSHOT</spring-cloud-commons.version>
<jopt-simple.version>5.0.4</jopt-simple.version>
<cglib.version>3.2.11</cglib.version>
<spock-spring.version>2.2-M1-groovy-4.0</spock-spring.version> <!-- @releaser:version-check-off -->
<cglib.version>3.3.0</cglib.version>
<spock-spring.version>2.3-groovy-4.0</spock-spring.version> <!-- @releaser:version-check-off -->
<hoverfly-junit.version>0.2.2</hoverfly-junit.version>
<commons-text.version>1.10.0</commons-text.version>
<handlebars.version>4.3.0</handlebars.version>
@@ -42,11 +43,15 @@
<javax-inject.version>1</javax-inject.version>
<json-unit-assertj.version>2.32.0</json-unit-assertj.version>
<rest-assured.version>5.1.0</rest-assured.version>
<slf4j.version>[2.0.0,)</slf4j.version>
<junit-vintage.version>5.9.1</junit-vintage.version>
<junit-jupiter.version>5.9.1</junit-jupiter.version>
<gmavenplus-plugin.version>1.13.0</gmavenplus-plugin.version>
<maven-surefire-plugin.version>3.0.0-M7</maven-surefire-plugin.version>
<awaitility.version>4.2.0</awaitility.version>
<artemis.version>${artemis-jms-server.version}</artemis.version>
<testcontainers.version>1.17.5</testcontainers.version>
<!-- We need to have compatibility with Gradle -->
<groovy.version>4.0.0</groovy.version>
@@ -67,6 +72,7 @@
<javadoc.failOnError>false</javadoc.failOnError>
<javadoc.failOnWarnings>false</javadoc.failOnWarnings>
<artemis-jms-server.version>2.26.0</artemis-jms-server.version>
</properties>
<modules>
@@ -116,6 +122,26 @@
<artifactId>camel-spring-boot</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-rabbitmq-starter</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-direct-starter</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-bean-starter</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel.springboot</groupId>
<artifactId>camel-jackson-starter</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-core</artifactId>
@@ -141,6 +167,11 @@
<artifactId>camel-activemq</artifactId>
<version>${camel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-jms-server</artifactId>
<version>${artemis-jms-server.version}</version>
</dependency>
<dependency>
<groupId>net.sf.jopt-simple</groupId>
<artifactId>jopt-simple</artifactId>
@@ -272,6 +303,13 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-dependencies</artifactId>
<version>${spring-cloud-netflix.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-dependencies</artifactId>
@@ -449,6 +487,40 @@
<artifactId>kotlin-compiler-embeddable</artifactId>
<version>${contract.kotlin.version}</version>
</dependency>
<!-- Ensure that we don't have 2 sets of slf4j on the classpath -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>${awaitility.version}</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-junit</artifactId>
<version>${artemis.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>rabbitmq</artifactId>
<version>${testcontainers.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
@@ -590,6 +662,24 @@
<enabled>true</enabled>
</snapshots>
</repository>
<!-- FIXME: 4.0 -->
<!-- Netflix -->
<!--<repository>
<id>netflix-snapshots</id>
<name>Netflix Snapshots</name>
<url>https://artifactory-oss.prod.netflix.net/artifactory/maven-oss-snapshots</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>-->
<repository>
<id>netflix-candidates</id>
<name>Netflix Candidates</name>
<url>https://artifactory-oss.prod.netflix.net/artifactory/maven-oss-candidates</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<profiles>

View File

@@ -71,8 +71,7 @@
<profile>integration</profile>
</profiles>
<pomIncludes>
<!-- FIXME: 4.0 -->
<!--<pomInclude>*/pom.xml</pomInclude>-->
<pomInclude>*/pom.xml</pomInclude>
<pomInclude>contracts/pom.xml</pomInclude>
<pomInclude>dsl/pom.xml</pomInclude>
<pomInclude>webclient/pom.xml</pomInclude>

View File

@@ -36,6 +36,12 @@
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>

View File

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

View File

@@ -81,6 +81,12 @@
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
<scope>compile</scope>
</dependency>
<!-- end::httpclient[] -->

View File

@@ -20,7 +20,6 @@
<properties>
<spring-cloud-contract.version>4.0.0-SNAPSHOT</spring-cloud-contract.version>
<skipTests>true</skipTests>
</properties>
<modules>

View File

@@ -4,9 +4,7 @@
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>
<properties>
<!-- TODO: Bump-->
<!-- <automaton.version>1.12-1</automaton.version>-->
<automaton.version>1.11-8</automaton.version>
<automaton.version>1.12-4</automaton.version>
</properties>
<parent>
<groupId>org.springframework.cloud</groupId>
@@ -29,7 +27,7 @@
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>dk.brics.automaton</groupId>
<groupId>dk.brics</groupId>
<artifactId>automaton</artifactId>
<version>${automaton.version}</version>
</dependency>

View File

@@ -1,25 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.spec.internal;
class ClientInput extends Input {
ClientInput(Input request) {
super(request);
}
}

View File

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

View File

@@ -1,25 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.spec.internal;
class ServerInput extends Input {
ServerInput(Input request) {
super(request);
}
}

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@
<name>spring-cloud-contract-dependencies</name>
<description>Spring Cloud Contract Dependencies</description>
<properties>
<wiremock.version>2.34.0</wiremock.version>
<wiremock.version>2.35.0</wiremock.version>
<jsonassert.version>0.6.2</jsonassert.version>
</properties>
<dependencyManagement>

View File

@@ -4,17 +4,17 @@
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>
<properties>
<org.eclipse.sisu.inject.version>0.3.4</org.eclipse.sisu.inject.version>
<org.eclipse.sisu.plexus.version>0.3.4</org.eclipse.sisu.plexus.version>
<org.eclipse.sisu.inject.version>0.3.5</org.eclipse.sisu.inject.version>
<org.eclipse.sisu.plexus.version>0.3.5</org.eclipse.sisu.plexus.version>
<sisu-guice.version>4.2.0</sisu-guice.version>
<guice.version>5.0.0</guice.version>
<guava.version>30.0-jre</guava.version>
<asm.version>9.0</asm.version>
<checker.version>3.6.1</checker.version>
<asm.version>9.4</asm.version>
<checker.version>3.25.0</checker.version>
<maven-dependency-plugin.version>3.1.2</maven-dependency-plugin.version>
<build-helper-maven-plugin.version>3.0.0</build-helper-maven-plugin.version>
<!-- We need to set this cause resolvers are taking in an older version -->
<plexus-utils.version>3.3.0</plexus-utils.version>
<plexus-utils.version>3.5.0</plexus-utils.version>
<apache-client.version>4.5.13</apache-client.version>
</properties>
<parent>
<groupId>org.springframework.cloud</groupId>
@@ -174,6 +174,12 @@
<optional>true</optional>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${apache-client.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>

View File

@@ -14,7 +14,7 @@
<name>Spring Cloud Contract Stub Runner Boot</name>
<description>Spring Cloud Contract Stub Runner Boot</description>
<properties>
<thin-jar.version>1.0.27.RELEASE</thin-jar.version>
<thin-jar.version>1.0.28.RELEASE</thin-jar.version>
</properties>
<dependencies>
<dependency>

View File

@@ -13,10 +13,6 @@
<packaging>jar</packaging>
<name>Spring Cloud Contract Stub Runner</name>
<description>Spring Cloud Contract Stub Runner</description>
<properties>
<!-- TODO: remove -->
<skipTests>true</skipTests>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
@@ -79,11 +75,6 @@
<artifactId>jopt-simple</artifactId>
<optional>true</optional>
</dependency>
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
<optional>true</optional>
</dependency>-->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
@@ -215,6 +206,11 @@
<artifactId>curator-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-server</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>

View File

@@ -16,7 +16,7 @@
package org.springframework.cloud.contract.stubrunner;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpStubMessages;
/**
@@ -32,13 +32,13 @@ public class BatchStubRunnerFactory {
private final StubDownloader stubDownloader;
private final MessageVerifier<?> contractVerifierMessaging;
private final MessageVerifierSender<?> contractVerifierMessaging;
public BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions) {
this(stubRunnerOptions, new NoOpStubMessages());
}
public BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions, MessageVerifier verifier) {
public BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions, MessageVerifierSender<?> verifier) {
this(stubRunnerOptions, aetherStubDownloader(stubRunnerOptions), verifier);
}
@@ -47,7 +47,7 @@ public class BatchStubRunnerFactory {
}
public BatchStubRunnerFactory(StubRunnerOptions stubRunnerOptions, StubDownloader stubDownloader,
MessageVerifier<?> contractVerifierMessaging) {
MessageVerifierSender<?> contractVerifierMessaging) {
this.stubRunnerOptions = stubRunnerOptions;
this.stubDownloader = stubDownloader;
this.contractVerifierMessaging = contractVerifierMessaging;

View File

@@ -30,7 +30,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpStubMessages;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.StringUtils;
@@ -60,7 +60,7 @@ public class StubRunner implements StubRunning {
}
public StubRunner(StubRunnerOptions stubRunnerOptions, String repositoryPath, StubConfiguration stubsConfiguration,
MessageVerifier<?> contractVerifierMessaging) {
MessageVerifierSender<?> contractVerifierMessaging) {
this.stubsConfiguration = stubsConfiguration;
this.stubRunnerOptions = stubRunnerOptions;
List<HttpServerStub> serverStubs = SpringFactoriesLoader.loadFactories(HttpServerStub.class, null);

View File

@@ -41,7 +41,7 @@ import org.springframework.cloud.contract.stubrunner.AvailablePortScanner.PortCa
import org.springframework.cloud.contract.stubrunner.provider.wiremock.WireMockHttpServerStub;
import org.springframework.cloud.contract.verifier.converter.YamlContract;
import org.springframework.cloud.contract.verifier.converter.YamlContractConverter;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessageMetadata;
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpStubMessages;
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
@@ -57,7 +57,7 @@ class StubRunnerExecutor implements StubFinder {
private final AvailablePortScanner portScanner;
private final MessageVerifier<?> 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, MessageVerifier<?> contractVerifierMessaging,
StubRunnerExecutor(AvailablePortScanner portScanner, MessageVerifierSender<?> messageVerifierSender,
List<HttpServerStub> serverStubs) {
this.portScanner = portScanner;
this.contractVerifierMessaging = contractVerifierMessaging;
this.messageVerifierSender = messageVerifierSender;
this.serverStubs = serverStubs;
}
@@ -249,7 +249,7 @@ class StubRunnerExecutor implements StubFinder {
YamlContract contract = yamlContracts.get(0);
setMessageType(contract, ContractVerifierMessageMetadata.MessageType.OUTPUT);
// TODO: Json is harcoded here
this.contractVerifierMessaging.send(
this.messageVerifierSender.send(
JsonOutput
.toJson(BodyExtractor.extractClientValueFromBody(body == null ? null : body.getClientValue())),
headers == null ? null : headers.asStubSideMap(), outputMessage.getSentTo().getClientValue(), contract);

View File

@@ -36,7 +36,7 @@ import org.springframework.cloud.contract.stubrunner.provider.wiremock.WireMockH
import org.springframework.cloud.contract.verifier.converter.RecursiveFilesConverter;
import org.springframework.cloud.contract.verifier.converter.StubGenerator;
import org.springframework.cloud.contract.verifier.converter.StubGeneratorProvider;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
import org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.SpringFactoriesLoader;
@@ -53,10 +53,10 @@ class StubRunnerFactory {
private final StubDownloader stubDownloader;
private final MessageVerifier<?> contractVerifierMessaging;
private final MessageVerifierSender<?> contractVerifierMessaging;
StubRunnerFactory(StubRunnerOptions stubRunnerOptions, StubDownloader stubDownloader,
MessageVerifier<?> contractVerifierMessaging) {
MessageVerifierSender<?> contractVerifierMessaging) {
this.stubRunnerOptions = stubRunnerOptions;
this.stubDownloader = stubDownloader;
this.contractVerifierMessaging = contractVerifierMessaging;

View File

@@ -1,50 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.annotation.Configuration;
/**
* Supports
* {@link org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner} by
* loading in AutoConfigurations related to Stream and Integration only if the relevant
* jars are in classpath.
*
* @author Biju Kunjummen
*/
@Configuration(proxyBeanMethods = false)
public class StubRunnerStreamsIntegrationAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(TestChannelBinderConfiguration.class)
@ImportAutoConfiguration(classes = TestChannelBinderConfiguration.class)
static class StreamsRelatedAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(IntegrationAutoConfiguration.class)
@ImportAutoConfiguration(classes = { IntegrationAutoConfiguration.class })
static class IntegrationRelatedAutoConfiguration {
}
}

View File

@@ -1,101 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.camel;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.RoutesBuilder;
import org.apache.camel.builder.RouteBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.stubrunner.BatchStubRunner;
import org.springframework.cloud.contract.stubrunner.StubConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Camel configuration that iterates over the downloaded Groovy DSLs and registers a route
* for each DSL.
*
* @author Marcin Grzejszczak
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RoutesBuilder.class)
@ConditionalOnProperty(name = "stubrunner.camel.enabled", havingValue = "true", matchIfMissing = true)
public class StubRunnerCamelConfiguration {
static final String STUBRUNNER_DESTINATION_URL_HEADER_NAME = "STUBRUNNER_DESTINATION_URL";
@Bean
public RoutesBuilder myRouter(final BatchStubRunner batchStubRunner) {
return new RouteBuilder() {
@Override
public void configure() throws Exception {
Map<StubConfiguration, Collection<Contract>> contracts = batchStubRunner.getContracts();
for (Map.Entry<StubConfiguration, Collection<Contract>> entry : contracts.entrySet()) {
Collection<Contract> value = entry.getValue();
MultiValueMap<String, Contract> map = new LinkedMultiValueMap<>();
for (Contract dsl : value) {
if (dsl == null) {
continue;
}
if (dsl.getInput() != null && dsl.getInput().getMessageFrom() != null
&& StringUtils.hasText(dsl.getInput().getMessageFrom().getClientValue())) {
String from = dsl.getInput().getMessageFrom().getClientValue();
map.add(from, dsl);
}
}
for (Map.Entry<String, List<Contract>> entries : map.entrySet()) {
from(entries.getKey()).filter(new StubRunnerCamelPredicate(entries.getValue()))
.process(new StubRunnerCamelProcessor()).dynamicRouter(
header(StubRunnerCamelConfiguration.STUBRUNNER_DESTINATION_URL_HEADER_NAME));
}
}
}
};
}
@Bean
DummyProcessor dummyStubRunnerProcessor() {
return new DummyProcessor();
}
private static class DummyProcessor implements Processor {
private static final Log log = LogFactory.getLog(DummyProcessor.class);
@Override
public void process(Exchange exchange) {
if (log.isTraceEnabled()) {
log.trace("Got exchange [" + exchange + "]");
}
}
}
}

View File

@@ -1,35 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.camel;
import org.springframework.cloud.contract.spec.Contract;
/**
* @author Marcin Grzejszczak
*/
class StubRunnerCamelPayload {
final Object payload;
final Contract contract;
StubRunnerCamelPayload(Contract contract) {
this.contract = contract;
this.payload = null;
}
}

View File

@@ -1,226 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.camel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.toomuchcoding.jsonassert.JsonAssertion;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.BodyMatcher;
import org.springframework.cloud.contract.spec.internal.BodyMatchers;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.spec.internal.RegexProperty;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.util.ContentType;
import org.springframework.cloud.contract.verifier.util.ContentUtils;
import org.springframework.cloud.contract.verifier.util.JsonPaths;
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
import org.springframework.cloud.contract.verifier.util.MapConverter;
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
/**
* Passes through a message that matches the one defined in the DSL.
*
* @author Marcin Grzejszczak
*/
class StubRunnerCamelPredicate implements Predicate {
private static final Log log = LogFactory.getLog(StubRunnerCamelPredicate.class);
private final List<Contract> groovyDsls;
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
StubRunnerCamelPredicate(List<Contract> groovyDsls) {
this.groovyDsls = groovyDsls;
}
@Override
public boolean matches(Exchange exchange) {
Contract contract = getContract(exchange.getMessage());
if (log.isDebugEnabled()) {
log.debug("For exchange [" + exchange + "] found contract [" + contract + "]");
}
if (contract == null) {
return false;
}
exchange.getIn().setBody(new StubRunnerCamelPayload(contract));
return true;
}
private Contract getContract(Message message) {
for (Contract groovyDsl : this.groovyDsls) {
Contract contract = matchContract(message, groovyDsl);
if (contract != null) {
return contract;
}
}
return null;
}
private Contract matchContract(Message message, Contract groovyDsl) {
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
if (!unmatchedHeaders.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
}
return null;
}
Object inputMessage = message.getBody();
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
if (dslBody instanceof FromFileProperty) {
if (log.isDebugEnabled()) {
log.debug("Will compare file content");
}
FromFileProperty property = (FromFileProperty) dslBody;
if (property.isString()) {
// continue processing as if body was pure string
dslBody = property.asString();
}
else if (!(inputMessage instanceof byte[])) {
if (log.isDebugEnabled()) {
log.debug("Contract provided byte comparison, but the input message is of type ["
+ inputMessage.getClass() + "]. Can't compare the two.");
}
return null;
}
else {
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
if (log.isDebugEnabled() && !matches) {
log.debug("Contract provided byte comparison, but the byte arrays don't match");
}
return matches ? groovyDsl : null;
}
}
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
return groovyDsl;
}
return null;
}
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
boolean matches;
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
if (type == ContentType.JSON) {
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
}
else if (dslBody instanceof RegexProperty && inputMessage instanceof String) {
Pattern pattern = ((RegexProperty) dslBody).getPattern();
matches = pattern.matcher((String) inputMessage).matches();
bodyUnmatchedLog(dslBody, matches, pattern);
}
else {
matches = dslBody.equals(inputMessage);
bodyUnmatchedLog(dslBody, matches, inputMessage);
}
return matches;
}
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
if (log.isDebugEnabled() && !matches) {
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
+ "]");
}
}
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
Object dslBody) {
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
JsonPaths jsonPaths = JsonToJsonPathsConverter
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
DocumentContext parsedJson;
try {
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
}
catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot serialize to JSON", e);
}
List<String> unmatchedJsonPath = new ArrayList<>();
boolean matches = true;
for (MethodBufferingJsonVerifiable path : jsonPaths) {
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
}
if (matchers != null && matchers.hasMatchers()) {
for (BodyMatcher matcher : matchers.matchers()) {
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
}
}
if (!unmatchedJsonPath.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
}
}
return matches;
}
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
try {
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
return true;
}
catch (Exception e) {
unmatchedJsonPath.add(e.getLocalizedMessage());
return false;
}
}
private List<String> headersMatch(Message message, Contract groovyDsl) {
List<String> unmatchedHeaders = new ArrayList<>();
Map<String, Object> headers = message.getHeaders();
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
String name = it.getName();
Object value = it.getClientValue();
Object valueInHeader = headers.get(name);
boolean matches;
if (value instanceof RegexProperty) {
Pattern pattern = ((RegexProperty) value).getPattern();
matches = pattern.matcher(valueInHeader.toString()).matches();
}
else {
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
}
if (!matches) {
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
}
}
return unmatchedHeaders;
}
private String unmatchedText(Object expectedValue) {
return expectedValue instanceof RegexProperty
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
: "be equal to [" + expectedValue + "]";
}
}

View File

@@ -1,88 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.camel;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.Processor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
/**
* Sends forward a message defined in the DSL. Also removes headers from the input message
* and provides the headers from the DSL.
*
* @author Marcin Grzejszczak
*/
class StubRunnerCamelProcessor implements Processor {
private static final Log log = LogFactory.getLog(StubRunnerCamelProcessor.class);
private static final String DUMMY_BEAN_URL = "bean:dummyStubRunnerProcessor";
@Override
public void process(Exchange exchange) {
Message input = exchange.getIn();
StubRunnerCamelPayload body = input.getBody(StubRunnerCamelPayload.class);
Contract groovyDsl = body.contract;
setStubRunnerDestinationHeader(exchange, body);
if (groovyDsl.getInput().getMessageHeaders() != null) {
for (Header entry : groovyDsl.getInput().getMessageHeaders().getEntries()) {
input.removeHeader(entry.getName());
}
}
if (groovyDsl.getOutputMessage() == null) {
if (log.isDebugEnabled()) {
log.debug("No output message provided, will not modify the body");
}
return;
}
input.setBody(outputBody(groovyDsl));
if (groovyDsl.getOutputMessage().getHeaders() != null) {
for (Header entry : groovyDsl.getOutputMessage().getHeaders().getEntries()) {
input.setHeader(entry.getName(), entry.getClientValue());
}
}
}
private Object outputBody(Contract groovyDsl) {
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
if (outputBody instanceof FromFileProperty) {
FromFileProperty property = (FromFileProperty) outputBody;
return property.asBytes();
}
return BodyExtractor.extractStubValueFrom(outputBody);
}
private void setStubRunnerDestinationHeader(Exchange exchange, StubRunnerCamelPayload body) {
boolean outputPart = body.contract.getOutputMessage() != null;
String url = DUMMY_BEAN_URL;
if (outputPart && body.contract.getOutputMessage().getSentTo() != null) {
url = body.contract.getOutputMessage().getSentTo().getClientValue();
}
exchange.getIn().setHeader(StubRunnerCamelConfiguration.STUBRUNNER_DESTINATION_URL_HEADER_NAME, url);
if (log.isDebugEnabled()) {
log.debug("Set stub runner destination header to [" + url + "]");
}
}
}

View File

@@ -1,109 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.integration;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.function.Consumer;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.stubrunner.BatchStubRunner;
import org.springframework.cloud.contract.stubrunner.StubConfiguration;
import org.springframework.context.Lifecycle;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.FilterEndpointSpec;
import org.springframework.integration.dsl.IntegrationFlowBuilder;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.messaging.Message;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Spring Integration configuration that iterates over the downloaded Groovy DSLs and
* registers a flow for each DSL.
*
* @author Marcin Grzejszczak
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(IntegrationFlowBuilder.class)
@ConditionalOnProperty(name = "stubrunner.integration.enabled", havingValue = "true", matchIfMissing = true)
public class StubRunnerIntegrationConfiguration {
@Bean
@ConditionalOnMissingBean(name = "stubFlowRegistrar")
public FlowRegistrar stubFlowRegistrar(AutowireCapableBeanFactory beanFactory, BatchStubRunner batchStubRunner) {
Map<StubConfiguration, Collection<Contract>> contracts = batchStubRunner.getContracts();
IntegrationFlowBuilder dummyBuilder = IntegrationFlows.from(DummyMessageHandler.CHANNEL_NAME)
.handle(new DummyMessageHandler(), "handle");
beanFactory.initializeBean(dummyBuilder.get(), DummyMessageHandler.CHANNEL_NAME + ".flow");
for (Entry<StubConfiguration, Collection<Contract>> entry : contracts.entrySet()) {
StubConfiguration key = entry.getKey();
Collection<Contract> value = entry.getValue();
String name = key.getGroupId() + "_" + key.getArtifactId();
MultiValueMap<String, Contract> map = new LinkedMultiValueMap<>();
for (Contract dsl : value) {
if (dsl == null) {
continue;
}
if (dsl.getInput() != null && dsl.getInput().getMessageFrom() != null
&& StringUtils.hasText(dsl.getInput().getMessageFrom().getClientValue())) {
String from = dsl.getInput().getMessageFrom().getClientValue();
map.add(from, dsl);
}
}
for (Entry<String, List<Contract>> entries : map.entrySet()) {
final String flowName = name + "_" + entries.getKey() + "_" + entries.getValue().hashCode();
IntegrationFlowBuilder builder = IntegrationFlows.from(entries.getKey())
.filter(new StubRunnerIntegrationMessageSelector(entries.getValue()),
new Consumer<FilterEndpointSpec>() {
@Override
public void accept(FilterEndpointSpec e) {
e.id(flowName + ".filter");
}
})
.transform(new StubRunnerIntegrationTransformer(entries.getValue()))
.route(new StubRunnerIntegrationRouter(entries.getValue(), beanFactory));
beanFactory.initializeBean(builder.get(), flowName);
beanFactory.getBean(flowName + ".filter", Lifecycle.class).start();
}
}
return new FlowRegistrar();
}
static class DummyMessageHandler {
static String CHANNEL_NAME = "stub_runner_dummy_channel";
public void handle(Message<?> message) {
}
}
static class FlowRegistrar {
}
}

View File

@@ -1,241 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.integration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.toomuchcoding.jsonassert.JsonAssertion;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.BodyMatcher;
import org.springframework.cloud.contract.spec.internal.BodyMatchers;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.spec.internal.RegexProperty;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.util.ContentType;
import org.springframework.cloud.contract.verifier.util.ContentUtils;
import org.springframework.cloud.contract.verifier.util.JsonPaths;
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
import org.springframework.cloud.contract.verifier.util.MapConverter;
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
import org.springframework.integration.core.MessageSelector;
import org.springframework.messaging.Message;
/**
* Passes through a message that matches the one defined in the DSL.
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
class StubRunnerIntegrationMessageSelector implements MessageSelector {
private static final Map<Message, Contract> CACHE = Collections.synchronizedMap(new WeakHashMap<>());
private static final Log log = LogFactory.getLog(StubRunnerIntegrationMessageSelector.class);
private final List<Contract> groovyDsls;
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
StubRunnerIntegrationMessageSelector(Contract groovyDsl) {
this(Collections.singletonList(groovyDsl));
}
StubRunnerIntegrationMessageSelector(List<Contract> groovyDsls) {
this.groovyDsls = groovyDsls;
}
@Override
public boolean accept(Message<?> message) {
return matchingContract(message) != null;
}
Contract matchingContract(Message<?> message) {
if (CACHE.containsKey(message)) {
return CACHE.get(message);
}
Contract contract = getContract(message);
if (contract != null) {
CACHE.put(message, contract);
}
return contract;
}
void updateCache(Message<?> message, Contract contract) {
CACHE.put(message, contract);
}
private Contract getContract(Message<?> message) {
for (Contract groovyDsl : this.groovyDsls) {
Contract contract = matchContract(message, groovyDsl);
if (contract != null) {
return contract;
}
}
return null;
}
private Contract matchContract(Message<?> message, Contract groovyDsl) {
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
if (!unmatchedHeaders.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
}
return null;
}
Object inputMessage = message.getPayload();
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
if (dslBody instanceof FromFileProperty) {
if (log.isDebugEnabled()) {
log.debug("Will compare file content");
}
FromFileProperty property = (FromFileProperty) dslBody;
if (property.isString()) {
// continue processing as if body was pure string
dslBody = property.asString();
}
else if (!(inputMessage instanceof byte[])) {
if (log.isDebugEnabled()) {
log.debug("Contract provided byte comparison, but the input message is of type ["
+ inputMessage.getClass() + "]. Can't compare the two.");
}
return null;
}
else {
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
if (log.isDebugEnabled() && !matches) {
log.debug("Contract provided byte comparison, but the byte arrays don't match");
}
return matches ? groovyDsl : null;
}
}
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
return groovyDsl;
}
return null;
}
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
boolean matches;
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
if (type == ContentType.JSON) {
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
}
else if (dslBody instanceof RegexProperty && inputMessage instanceof String) {
Pattern pattern = ((RegexProperty) dslBody).getPattern();
matches = pattern.matcher((String) inputMessage).matches();
bodyUnmatchedLog(dslBody, matches, pattern);
}
else {
matches = dslBody.equals(inputMessage);
bodyUnmatchedLog(dslBody, matches, inputMessage);
}
return matches;
}
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
if (log.isDebugEnabled() && !matches) {
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
+ "]");
}
}
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
Object dslBody) {
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
JsonPaths jsonPaths = JsonToJsonPathsConverter
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
DocumentContext parsedJson;
try {
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
}
catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot serialize to JSON", e);
}
List<String> unmatchedJsonPath = new ArrayList<>();
boolean matches = true;
for (MethodBufferingJsonVerifiable path : jsonPaths) {
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
}
if (matchers != null && matchers.hasMatchers()) {
for (BodyMatcher matcher : matchers.matchers()) {
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
}
}
if (!unmatchedJsonPath.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
}
}
return matches;
}
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
try {
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
return true;
}
catch (Exception e) {
unmatchedJsonPath.add(e.getLocalizedMessage());
return false;
}
}
private List<String> headersMatch(Message<?> message, Contract groovyDsl) {
List<String> unmatchedHeaders = new ArrayList<>();
Map<String, Object> headers = message.getHeaders();
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
String name = it.getName();
Object value = it.getClientValue();
Object valueInHeader = headers.get(name);
boolean matches;
if (value instanceof RegexProperty || value instanceof Pattern) {
Pattern pattern = new RegexProperty(value).getPattern();
matches = pattern.matcher(valueInHeader.toString()).matches();
}
else {
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
}
if (!matches) {
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
}
}
return unmatchedHeaders;
}
private String unmatchedText(Object expectedValue) {
return expectedValue instanceof RegexProperty
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
: "be equal to [" + expectedValue + "]";
}
}

View File

@@ -1,54 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.integration;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.integration.router.AbstractMessageRouter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
/**
* @author Marcin Grzejszczak
*/
class StubRunnerIntegrationRouter extends AbstractMessageRouter {
private final StubRunnerIntegrationMessageSelector selector;
private final BeanFactory beanFactory;
StubRunnerIntegrationRouter(List<Contract> groovyDsls, BeanFactory beanFactory) {
this.selector = new StubRunnerIntegrationMessageSelector(groovyDsls);
this.beanFactory = beanFactory;
}
@Override
protected Collection<MessageChannel> determineTargetChannels(Message<?> message) {
Contract dsl = this.selector.matchingContract(message);
if (dsl != null && dsl.getOutputMessage() != null && dsl.getOutputMessage().getSentTo() != null) {
String channelName = dsl.getOutputMessage().getSentTo().getClientValue();
return Collections.singleton((MessageChannel) this.beanFactory.getBean(channelName));
}
return Collections.singleton((MessageChannel) this.beanFactory
.getBean(StubRunnerIntegrationConfiguration.DummyMessageHandler.CHANNEL_NAME));
}
}

View File

@@ -1,73 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.integration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
/**
* Sends forward a message defined in the DSL.
*
* @author Marcin Grzejszczak
*/
class StubRunnerIntegrationTransformer {
private final StubRunnerIntegrationMessageSelector selector;
StubRunnerIntegrationTransformer(Contract groovyDsl) {
this(Collections.singletonList(groovyDsl));
}
StubRunnerIntegrationTransformer(List<Contract> groovyDsls) {
this.selector = new StubRunnerIntegrationMessageSelector(groovyDsls);
}
public Message<?> transform(Message<?> source) {
Contract groovyDsl = matchingContract(source);
if (groovyDsl == null || groovyDsl.getOutputMessage() == null) {
return source;
}
Object outputBody = outputBody(groovyDsl);
Map<String, Object> headers = groovyDsl.getOutputMessage().getHeaders().asStubSideMap();
MessageHeaders messageHeaders = new MessageHeaders(headers);
Message<Object> message = MessageBuilder.createMessage(outputBody, messageHeaders);
this.selector.updateCache(message, groovyDsl);
return message;
}
private Object outputBody(Contract groovyDsl) {
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
if (outputBody instanceof FromFileProperty) {
FromFileProperty property = (FromFileProperty) outputBody;
return property.asBytes();
}
return BodyExtractor.extractStubValueFrom(outputBody);
}
Contract matchingContract(Message<?> source) {
return this.selector.matchingContract(source);
}
}

View File

@@ -1,84 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.jms;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.ObjectMessage;
import jakarta.jms.StreamMessage;
import jakarta.jms.TextMessage;
final class StubRunnerJmsAccessor {
private StubRunnerJmsAccessor() {
throw new IllegalStateException("Can't instantiate an utility class");
}
static Object getBody(Message message) {
try {
return getPayload(message);
}
catch (JMSException ex) {
throw new IllegalStateException(ex);
}
}
static Map<String, Object> getHeaders(Message message) {
try {
return headers(message);
}
catch (JMSException ex) {
throw new IllegalStateException(ex);
}
}
private static Map<String, Object> headers(Message message) throws JMSException {
Map<String, Object> headers = new HashMap<>();
if (message == null) {
return headers;
}
Enumeration enumeration = message.getPropertyNames();
while (enumeration.hasMoreElements()) {
Object element = enumeration.nextElement();
String asString = element.toString();
Object property = message.getObjectProperty(asString);
headers.put(asString, property);
}
return headers;
}
private static Object getPayload(Message message) throws JMSException {
if (message == null) {
return null;
}
else if (message instanceof TextMessage) {
return ((TextMessage) message).getText();
}
else if (message instanceof StreamMessage) {
return ((StreamMessage) message).readObject();
}
else if (message instanceof ObjectMessage) {
return ((ObjectMessage) message).getObject();
}
return message.getBody(Object.class);
}
}

View File

@@ -1,119 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.jms;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import jakarta.jms.ConnectionFactory;
import jakarta.jms.MessageListener;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.stubrunner.BatchStubRunner;
import org.springframework.cloud.contract.stubrunner.StubConfiguration;
import org.springframework.cloud.contract.verifier.util.MapConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.listener.DefaultMessageListenerContainer;
import org.springframework.jms.listener.MessageListenerContainer;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Spring Integration configuration that iterates over the downloaded Groovy DSLs and
* registers a flow for each DSL.
*
* @author Marcin Grzejszczak
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(JmsTemplate.class)
@ConditionalOnProperty(name = "stubrunner.jms.enabled", havingValue = "true", matchIfMissing = true)
public class StubRunnerJmsConfiguration {
@Bean
@ConditionalOnMissingBean(name = "stubFlowRegistrar")
public FlowRegistrar stubFlowRegistrar(ConfigurableListableBeanFactory beanFactory,
BatchStubRunner batchStubRunner) {
Map<StubConfiguration, Collection<Contract>> contracts = batchStubRunner.getContracts();
for (Entry<StubConfiguration, Collection<Contract>> entry : contracts.entrySet()) {
StubConfiguration key = entry.getKey();
Collection<Contract> value = entry.getValue();
String name = key.getGroupId() + "_" + key.getArtifactId();
MultiValueMap<String, Contract> map = new LinkedMultiValueMap<>();
for (Contract dsl : value) {
if (dsl == null) {
continue;
}
if (dsl.getInput() != null && dsl.getInput().getMessageFrom() != null
&& StringUtils.hasText(dsl.getInput().getMessageFrom().getClientValue())) {
String from = dsl.getInput().getMessageFrom().getClientValue();
map.add(from, dsl);
}
}
for (Entry<String, List<Contract>> entries : map.entrySet()) {
List<Contract> matchingContracts = entries.getValue();
final String flowName = name + "_" + entries.getKey() + "_" + Math.abs(matchingContracts.hashCode());
// listener
StubRunnerJmsRouter router = new StubRunnerJmsRouter(matchingContracts, beanFactory);
StubRunnerJmsRouter listener = (StubRunnerJmsRouter) beanFactory.initializeBean(router, flowName);
beanFactory.registerSingleton(flowName, listener);
registerContainers(beanFactory, matchingContracts, flowName, listener);
}
}
return new FlowRegistrar();
}
private void registerContainers(ConfigurableListableBeanFactory beanFactory, List<Contract> matchingContracts,
String flowName, StubRunnerJmsRouter listener) {
// listener's container
ConnectionFactory connectionFactory = beanFactory.getBean(ConnectionFactory.class);
for (Contract matchingContract : matchingContracts) {
if (matchingContract.getInput() == null) {
continue;
}
String destination = MapConverter.getStubSideValuesForNonBody(matchingContract.getInput().getMessageFrom())
.toString();
MessageListenerContainer container = listenerContainer(destination, connectionFactory, listener);
String containerName = flowName + ".container";
Object initializedContainer = beanFactory.initializeBean(container, containerName);
beanFactory.registerSingleton(containerName, initializedContainer);
}
}
private MessageListenerContainer listenerContainer(String queueName, ConnectionFactory connectionFactory,
MessageListener listener) {
DefaultMessageListenerContainer container = new DefaultMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setDestinationName(queueName);
container.setMessageListener(listener);
return container;
}
static class FlowRegistrar {
}
}

View File

@@ -1,231 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.jms;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.toomuchcoding.jsonassert.JsonAssertion;
import jakarta.jms.Message;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.BodyMatcher;
import org.springframework.cloud.contract.spec.internal.BodyMatchers;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.spec.internal.RegexProperty;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.util.ContentType;
import org.springframework.cloud.contract.verifier.util.ContentUtils;
import org.springframework.cloud.contract.verifier.util.JsonPaths;
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
import org.springframework.cloud.contract.verifier.util.MapConverter;
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
/**
* Passes through a message that matches the one defined in the DSL.
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
class StubRunnerJmsMessageSelector {
private static final Map<Message, Contract> CACHE = Collections.synchronizedMap(new WeakHashMap<>());
private static final Log log = LogFactory.getLog(StubRunnerJmsMessageSelector.class);
private final List<Contract> groovyDsls;
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
StubRunnerJmsMessageSelector(List<Contract> groovyDsls) {
this.groovyDsls = groovyDsls;
}
Contract matchingContract(Message message) {
if (CACHE.containsKey(message)) {
return CACHE.get(message);
}
Contract contract = getContract(message);
if (contract != null) {
CACHE.put(message, contract);
}
return contract;
}
void updateCache(Message message, Contract contract) {
CACHE.put(message, contract);
}
private Contract getContract(Message message) {
for (Contract groovyDsl : this.groovyDsls) {
Contract contract = matchContract(message, groovyDsl);
if (contract != null) {
return contract;
}
}
return null;
}
private Contract matchContract(Message message, Contract groovyDsl) {
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
if (!unmatchedHeaders.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
}
return null;
}
Object inputMessage = StubRunnerJmsAccessor.getBody(message);
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
if (dslBody instanceof FromFileProperty) {
if (log.isDebugEnabled()) {
log.debug("Will compare file content");
}
FromFileProperty property = (FromFileProperty) dslBody;
if (property.isString()) {
// continue processing as if body was pure string
dslBody = property.asString();
}
else if (!(inputMessage instanceof byte[])) {
if (log.isDebugEnabled()) {
log.debug("Contract provided byte comparison, but the input message is of type ["
+ inputMessage.getClass() + "]. Can't compare the two.");
}
return null;
}
else {
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
if (log.isDebugEnabled() && !matches) {
log.debug("Contract provided byte comparison, but the byte arrays don't match");
}
return matches ? groovyDsl : null;
}
}
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
return groovyDsl;
}
return null;
}
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
boolean matches;
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
if (type == ContentType.JSON) {
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
}
else if (dslBody instanceof RegexProperty && inputMessage instanceof String) {
Pattern pattern = ((RegexProperty) dslBody).getPattern();
matches = pattern.matcher((String) inputMessage).matches();
bodyUnmatchedLog(dslBody, matches, pattern);
}
else {
matches = dslBody.equals(inputMessage);
bodyUnmatchedLog(dslBody, matches, inputMessage);
}
return matches;
}
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
if (log.isDebugEnabled() && !matches) {
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
+ "]");
}
}
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
Object dslBody) {
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
JsonPaths jsonPaths = JsonToJsonPathsConverter
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
DocumentContext parsedJson;
try {
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
}
catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot serialize to JSON", e);
}
List<String> unmatchedJsonPath = new ArrayList<>();
boolean matches = true;
for (MethodBufferingJsonVerifiable path : jsonPaths) {
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
}
if (matchers != null && matchers.hasMatchers()) {
for (BodyMatcher matcher : matchers.matchers()) {
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
}
}
if (!unmatchedJsonPath.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
}
}
return matches;
}
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
try {
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
return true;
}
catch (Exception e) {
unmatchedJsonPath.add(e.getLocalizedMessage());
return false;
}
}
private List<String> headersMatch(Message message, Contract groovyDsl) {
List<String> unmatchedHeaders = new ArrayList<>();
Map<String, Object> headers = StubRunnerJmsAccessor.getHeaders(message);
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
String name = it.getName();
Object value = it.getClientValue();
Object valueInHeader = headers.get(name);
boolean matches;
if (value instanceof RegexProperty) {
Pattern pattern = ((RegexProperty) value).getPattern();
matches = pattern.matcher(valueInHeader.toString()).matches();
}
else {
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
}
if (!matches) {
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
}
}
return unmatchedHeaders;
}
private String unmatchedText(Object expectedValue) {
return expectedValue instanceof RegexProperty
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
: "be equal to [" + expectedValue + "]";
}
}

View File

@@ -1,76 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.jms;
import java.util.List;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.MessageListener;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessagePostProcessor;
/**
* @author Marcin Grzejszczak
*/
class StubRunnerJmsRouter implements MessageListener {
private final StubRunnerJmsMessageSelector selector;
private final BeanFactory beanFactory;
private final List<Contract> contracts;
private JmsTemplate jmsTemplate;
StubRunnerJmsRouter(List<Contract> groovyDsls, BeanFactory beanFactory) {
this.selector = new StubRunnerJmsMessageSelector(groovyDsls);
this.beanFactory = beanFactory;
this.contracts = groovyDsls;
}
@Override
public void onMessage(jakarta.jms.Message message) {
Contract dsl = this.selector.matchingContract(message);
if (dsl != null && dsl.getOutputMessage() != null && dsl.getOutputMessage().getSentTo() != null) {
String destination = dsl.getOutputMessage().getSentTo().getClientValue();
jmsTemplate().send(destination,
session -> new StubRunnerJmsTransformer(this.contracts).transform(session, dsl));
}
}
private JmsTemplate jmsTemplate() {
if (this.jmsTemplate == null) {
this.jmsTemplate = this.beanFactory.getBean(JmsTemplate.class);
}
return this.jmsTemplate;
}
}
class ReplyToProcessor implements MessagePostProcessor {
@Override
public jakarta.jms.Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("requiresReply", "no");
return message;
}
}

View File

@@ -1,108 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.jms;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import jakarta.jms.BytesMessage;
import jakarta.jms.JMSException;
import jakarta.jms.Message;
import jakarta.jms.Session;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
/**
* Sends forward a message defined in the DSL.
*
* @author Marcin Grzejszczak
*/
class StubRunnerJmsTransformer {
private final StubRunnerJmsMessageSelector selector;
StubRunnerJmsTransformer(List<Contract> groovyDsls) {
this.selector = new StubRunnerJmsMessageSelector(groovyDsls);
}
public Message transform(Session session, Contract groovyDsl) {
Object outputBody = outputBody(groovyDsl);
Map<String, Object> headers = groovyDsl.getOutputMessage().getHeaders().asStubSideMap();
Message newMessage = createMessage(session, outputBody);
setHeaders(newMessage, headers);
this.selector.updateCache(newMessage, groovyDsl);
return newMessage;
}
private Object outputBody(Contract groovyDsl) {
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
if (outputBody instanceof FromFileProperty) {
FromFileProperty property = (FromFileProperty) outputBody;
return property.asBytes();
}
return BodyExtractor.extractStubValueFrom(outputBody);
}
Contract matchingContract(Message source) {
return this.selector.matchingContract(source);
}
private Message createMessage(Session session, Object payload) {
try {
if (payload instanceof String) {
return session.createTextMessage((String) payload);
}
else if (payload instanceof byte[]) {
BytesMessage bytesMessage = session.createBytesMessage();
bytesMessage.writeBytes((byte[]) payload);
return bytesMessage;
}
else if (payload instanceof Serializable) {
return session.createObjectMessage((Serializable) payload);
}
return session.createMessage();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
private void setHeaders(Message message, Map<String, Object> headers) {
for (Map.Entry<String, Object> entry : headers.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
try {
if (value instanceof String) {
message.setStringProperty(key, (String) value);
}
else if (value instanceof Boolean) {
message.setBooleanProperty(key, (Boolean) value);
}
else {
message.setObjectProperty(key, value);
}
}
catch (JMSException ex) {
throw new IllegalStateException(ex);
}
}
}
}

View File

@@ -1,148 +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.HashMap;
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.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.verifier.messaging.kafka.ContractVerifierKafkaConfiguration;
import org.springframework.cloud.contract.verifier.messaging.kafka.KafkaStubMessagesInitializer;
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)
@AutoConfigureBefore(ContractVerifierKafkaConfiguration.class)
public class StubRunnerKafkaConfiguration {
private static final Log log = LogFactory.getLog(StubRunnerKafkaConfiguration.class);
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "stubrunner.kafka.initializer.enabled", havingValue = "true", matchIfMissing = true)
KafkaStubMessagesInitializer stubRunnerKafkaStubMessagesInitializer() {
if (log.isDebugEnabled()) {
log.debug("Registering a noop kafka messages initializer");
}
return (broker, kafkaProperties) -> new HashMap<>();
}
@Bean
@ConditionalOnMissingBean(name = "stubFlowRegistrar")
public FlowRegistrar stubFlowRegistrar(ConfigurableListableBeanFactory beanFactory,
BatchStubRunner batchStubRunner) {
Map<StubConfiguration, Collection<Contract>> contracts = batchStubRunner.getContracts();
for (Entry<StubConfiguration, Collection<Contract>> entry : contracts.entrySet()) {
StubConfiguration key = entry.getKey();
Collection<Contract> value = entry.getValue();
String name = key.getGroupId() + "_" + key.getArtifactId();
MultiValueMap<String, Contract> map = new LinkedMultiValueMap<>();
for (Contract dsl : value) {
if (dsl == null) {
continue;
}
if (dsl.getInput() != null && dsl.getInput().getMessageFrom() != null
&& StringUtils.hasText(dsl.getInput().getMessageFrom().getClientValue())) {
String from = dsl.getInput().getMessageFrom().getClientValue();
map.add(from, dsl);
}
}
for (Entry<String, List<Contract>> entries : map.entrySet()) {
List<Contract> matchingContracts = entries.getValue();
final String flowName = name + "_" + entries.getKey() + "_" + Math.abs(matchingContracts.hashCode());
// listener
StubRunnerKafkaRouter router = new StubRunnerKafkaRouter(matchingContracts, beanFactory);
StubRunnerKafkaRouter listener = (StubRunnerKafkaRouter) beanFactory.initializeBean(router, flowName);
if (log.isDebugEnabled()) {
log.debug("Initialized kafka router with name [" + flowName + "]");
}
beanFactory.registerSingleton(flowName, listener);
registerContainers(beanFactory, matchingContracts, flowName, listener);
}
}
return new FlowRegistrar();
}
private void registerContainers(ConfigurableListableBeanFactory beanFactory, List<Contract> matchingContracts,
String flowName, StubRunnerKafkaRouter listener) {
// listener's container
ConsumerFactory consumerFactory = beanFactory.getBean(ConsumerFactory.class);
for (Contract matchingContract : matchingContracts) {
if (matchingContract.getInput() == null) {
continue;
}
String destination = MapConverter.getStubSideValuesForNonBody(matchingContract.getInput().getMessageFrom())
.toString();
ContainerProperties containerProperties = new ContainerProperties(destination);
KafkaMessageListenerContainer container = listenerContainer(consumerFactory, containerProperties, listener);
String containerName = flowName + ".container";
Object initializedContainer = beanFactory.initializeBean(container, containerName);
beanFactory.registerSingleton(containerName, initializedContainer);
if (log.isDebugEnabled()) {
log.debug("Initialized kafka message container with name [" + containerName
+ "] listening to destination [" + destination + "]");
}
}
}
private KafkaMessageListenerContainer listenerContainer(ConsumerFactory consumerFactory,
ContainerProperties containerProperties, GenericMessageListener listener) {
KafkaMessageListenerContainer container = new KafkaMessageListenerContainer(consumerFactory,
containerProperties);
container.setupMessageListener(listener);
return container;
}
static class FlowRegistrar {
}
}

View File

@@ -1,239 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.kafka;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.toomuchcoding.jsonassert.JsonAssertion;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.BodyMatcher;
import org.springframework.cloud.contract.spec.internal.BodyMatchers;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.spec.internal.RegexProperty;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.util.ContentType;
import org.springframework.cloud.contract.verifier.util.ContentUtils;
import org.springframework.cloud.contract.verifier.util.JsonPaths;
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
import org.springframework.cloud.contract.verifier.util.MapConverter;
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
import org.springframework.messaging.Message;
/**
* Passes through a message that matches the one defined in the DSL.
*
* @author Marcin Grzejszczak
*/
class StubRunnerKafkaMessageSelector {
private static final Map<Message<?>, Contract> CACHE = Collections.synchronizedMap(new WeakHashMap<>());
private static final Log log = LogFactory.getLog(StubRunnerKafkaMessageSelector.class);
private final List<Contract> groovyDsls;
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
StubRunnerKafkaMessageSelector(List<Contract> groovyDsls) {
this.groovyDsls = groovyDsls;
}
Contract matchingContract(Message<?> message) {
if (CACHE.containsKey(message)) {
return CACHE.get(message);
}
Contract contract = getContract(message);
if (contract != null) {
CACHE.put(message, contract);
}
return contract;
}
void updateCache(Message<?> message, Contract contract) {
CACHE.put(message, contract);
}
private Contract getContract(Message<?> message) {
for (Contract groovyDsl : this.groovyDsls) {
Contract contract = matchContract(message, groovyDsl);
if (contract != null) {
return contract;
}
}
return null;
}
private Contract matchContract(Message<?> message, Contract groovyDsl) {
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
if (!unmatchedHeaders.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
}
return null;
}
Object inputMessage = message.getPayload();
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
if (dslBody instanceof FromFileProperty) {
if (log.isDebugEnabled()) {
log.debug("Will compare file content");
}
FromFileProperty property = (FromFileProperty) dslBody;
if (property.isString()) {
// continue processing as if body was pure string
dslBody = property.asString();
}
else if (!(inputMessage instanceof byte[])) {
if (log.isDebugEnabled()) {
log.debug("Contract provided byte comparison, but the input message is of type ["
+ inputMessage.getClass() + "]. Can't compare the two.");
}
return null;
}
else {
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
if (log.isDebugEnabled() && !matches) {
log.debug("Contract provided byte comparison, but the byte arrays don't match");
}
return matches ? groovyDsl : null;
}
}
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
return groovyDsl;
}
return null;
}
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
boolean matches;
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
if (type == ContentType.JSON) {
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
}
else if (dslBody instanceof RegexProperty && inputMessage instanceof String) {
Pattern pattern = ((RegexProperty) dslBody).getPattern();
matches = pattern.matcher((String) inputMessage).matches();
bodyUnmatchedLog(dslBody, matches, pattern);
}
else {
matches = dslBody.equals(inputMessage);
bodyUnmatchedLog(dslBody, matches, inputMessage);
}
return matches;
}
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
if (log.isDebugEnabled() && !matches) {
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
+ "]");
}
}
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
Object dslBody) {
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
JsonPaths jsonPaths = JsonToJsonPathsConverter
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
DocumentContext parsedJson;
try {
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
}
catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot serialize to JSON", e);
}
List<String> unmatchedJsonPath = new ArrayList<>();
boolean matches = true;
for (MethodBufferingJsonVerifiable path : jsonPaths) {
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
}
if (matchers != null && matchers.hasMatchers()) {
for (BodyMatcher matcher : matchers.matchers()) {
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
}
}
if (!unmatchedJsonPath.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
}
}
return matches;
}
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
try {
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
return true;
}
catch (Exception e) {
unmatchedJsonPath.add(e.getLocalizedMessage());
return false;
}
}
private List<String> headersMatch(Message message, Contract groovyDsl) {
List<String> unmatchedHeaders = new ArrayList<>();
Map<String, Object> headers = message.getHeaders();
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
String name = it.getName();
Object value = it.getClientValue();
Object valueInHeader = headers.get(name);
valueInHeader = valueInHeader instanceof byte[] ? fromByte((byte[]) valueInHeader) : valueInHeader;
boolean matches;
if (value instanceof RegexProperty) {
Pattern pattern = ((RegexProperty) value).getPattern();
matches = pattern.matcher(valueInHeader.toString()).matches();
}
else {
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
}
if (!matches) {
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
}
}
return unmatchedHeaders;
}
private String fromByte(byte[] valueInHeader) {
String string = new String(valueInHeader);
if (string.startsWith("\"") && string.endsWith("\"")) {
return string.substring(1, string.length() - 1);
}
return string;
}
private String unmatchedText(Object expectedValue) {
return expectedValue instanceof RegexProperty
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
: "be equal to [" + expectedValue + "]";
}
}

View File

@@ -1,104 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.kafka;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.listener.MessageListener;
import org.springframework.kafka.support.Acknowledgment;
import org.springframework.kafka.support.converter.MessagingMessageConverter;
import org.springframework.messaging.Message;
/**
* @author Marcin Grzejszczak
*/
class StubRunnerKafkaRouter implements MessageListener<Object, Object> {
private static final Log log = LogFactory.getLog(StubRunnerKafkaRouter.class);
private final MessagingMessageConverter messageConverter = new MessagingMessageConverter();
private final StubRunnerKafkaMessageSelector selector;
private final BeanFactory beanFactory;
private final List<Contract> contracts;
private KafkaTemplate kafkaTemplate;
StubRunnerKafkaRouter(List<Contract> groovyDsls, BeanFactory beanFactory) {
this.selector = new StubRunnerKafkaMessageSelector(groovyDsls);
this.beanFactory = beanFactory;
this.contracts = groovyDsls;
}
private KafkaTemplate kafkaTemplate() {
if (this.kafkaTemplate == null) {
this.kafkaTemplate = this.beanFactory.getBean(KafkaTemplate.class);
}
return this.kafkaTemplate;
}
@Override
public void onMessage(ConsumerRecord<Object, Object> data) {
if (log.isDebugEnabled()) {
log.debug("Received message [" + data + "]");
}
Message<?> message = messageConverter.toMessage(data, null, null, null);
Contract dsl = this.selector.matchingContract(message);
if (dsl != null && dsl.getOutputMessage() != null && dsl.getOutputMessage().getSentTo() != null) {
String destination = dsl.getOutputMessage().getSentTo().getClientValue();
if (log.isDebugEnabled()) {
log.debug("Found a matching contract with an output message. Will send it to the [" + destination
+ "] destination");
}
Message<?> transform = new StubRunnerKafkaTransformer(this.contracts).transform(dsl);
String defaultTopic = kafkaTemplate().getDefaultTopic();
try {
kafkaTemplate().setDefaultTopic(destination);
kafkaTemplate().send(transform);
}
finally {
kafkaTemplate().setDefaultTopic(defaultTopic);
}
}
}
@Override
public void onMessage(ConsumerRecord<Object, Object> data, Acknowledgment acknowledgment) {
onMessage(data);
}
@Override
public void onMessage(ConsumerRecord<Object, Object> data, Consumer<?, ?> consumer) {
onMessage(data);
}
@Override
public void onMessage(ConsumerRecord<Object, Object> data, Acknowledgment acknowledgment, Consumer<?, ?> consumer) {
onMessage(data);
}
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.kafka;
import java.util.List;
import java.util.Map;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
/**
* Sends forward a message defined in the DSL.
*
* @author Marcin Grzejszczak
*/
class StubRunnerKafkaTransformer {
private final StubRunnerKafkaMessageSelector selector;
StubRunnerKafkaTransformer(List<Contract> groovyDsls) {
this.selector = new StubRunnerKafkaMessageSelector(groovyDsls);
}
public Message<?> transform(Contract groovyDsl) {
Object outputBody = outputBody(groovyDsl);
Map<String, Object> headers = groovyDsl.getOutputMessage().getHeaders().asStubSideMap();
Message newMessage = MessageBuilder.createMessage(outputBody, new MessageHeaders(headers));
this.selector.updateCache(newMessage, groovyDsl);
return newMessage;
}
private Object outputBody(Contract groovyDsl) {
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
if (outputBody instanceof FromFileProperty) {
FromFileProperty property = (FromFileProperty) outputBody;
return property.asBytes();
}
return BodyExtractor.extractStubValueFrom(outputBody);
}
}

View File

@@ -1,54 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.stream;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.integration.router.AbstractMessageRouter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
/**
* @author Marcin Grzejszczak
*/
class StubRunnerMessageRouter extends AbstractMessageRouter {
private final StubRunnerStreamMessageSelector selector;
private final BeanFactory beanFactory;
StubRunnerMessageRouter(List<Contract> groovyDsls, BeanFactory beanFactory) {
this.selector = new StubRunnerStreamMessageSelector(groovyDsls);
this.beanFactory = beanFactory;
}
@Override
protected Collection<MessageChannel> determineTargetChannels(Message<?> message) {
Contract dsl = this.selector.matchingContract(message);
if (dsl != null && dsl.getOutputMessage() != null && dsl.getOutputMessage().getSentTo() != null) {
String channelName = StubRunnerStreamConfiguration.resolvedDestination(this.beanFactory,
dsl.getOutputMessage().getSentTo().getClientValue());
return Collections.singleton((MessageChannel) this.beanFactory.getBean(channelName));
}
return Collections.singleton((MessageChannel) this.beanFactory.getBean("nullChannel"));
}
}

View File

@@ -1,131 +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 java.util.function.Consumer;
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.FilterEndpointSpec;
import org.springframework.integration.dsl.IntegrationFlowBuilder;
import org.springframework.integration.dsl.IntegrationFlows;
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({ IntegrationFlows.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 = IntegrationFlows.from(entries.getKey())
.filter(new StubRunnerStreamMessageSelector(entries.getValue()),
new Consumer<FilterEndpointSpec>() {
@Override
public void accept(FilterEndpointSpec e) {
e.id(flowName + ".filter");
}
})
.transform(new StubRunnerStreamTransformer(entries.getValue()))
.route(new StubRunnerMessageRouter(entries.getValue(), beanFactory));
beanFactory.initializeBean(builder.get(), flowName);
beanFactory.getBean(flowName + ".filter", Lifecycle.class).start();
}
}
return new FlowRegistrar();
}
static class FlowRegistrar {
}
}

View File

@@ -1,241 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.stream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.toomuchcoding.jsonassert.JsonAssertion;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.BodyMatcher;
import org.springframework.cloud.contract.spec.internal.BodyMatchers;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.spec.internal.Header;
import org.springframework.cloud.contract.spec.internal.RegexProperty;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
import org.springframework.cloud.contract.verifier.util.ContentType;
import org.springframework.cloud.contract.verifier.util.ContentUtils;
import org.springframework.cloud.contract.verifier.util.JsonPaths;
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
import org.springframework.cloud.contract.verifier.util.MapConverter;
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
import org.springframework.integration.core.MessageSelector;
import org.springframework.messaging.Message;
/**
* Passes through a message that matches the one defined in the DSL.
*
* @author Marcin Grzejszczak
* @author Tim Ysewyn
*/
class StubRunnerStreamMessageSelector implements MessageSelector {
private static final Map<Message, Contract> CACHE = Collections.synchronizedMap(new WeakHashMap<>());
private static final Log log = LogFactory.getLog(StubRunnerStreamMessageSelector.class);
private final List<Contract> groovyDsls;
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
StubRunnerStreamMessageSelector(Contract groovyDsl) {
this(Collections.singletonList(groovyDsl));
}
StubRunnerStreamMessageSelector(List<Contract> groovyDsls) {
this.groovyDsls = groovyDsls;
}
@Override
public boolean accept(Message<?> message) {
return matchingContract(message) != null;
}
Contract matchingContract(Message<?> message) {
if (CACHE.containsKey(message)) {
return CACHE.get(message);
}
Contract contract = getContract(message);
if (contract != null) {
CACHE.put(message, contract);
}
return contract;
}
void updateCache(Message<?> message, Contract contract) {
CACHE.put(message, contract);
}
private Contract getContract(Message<?> message) {
for (Contract groovyDsl : this.groovyDsls) {
Contract contract = matchContract(message, groovyDsl);
if (contract != null) {
return contract;
}
}
return null;
}
private Contract matchContract(Message<?> message, Contract groovyDsl) {
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
if (!unmatchedHeaders.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
}
return null;
}
Object inputMessage = message.getPayload();
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
if (dslBody instanceof FromFileProperty) {
if (log.isDebugEnabled()) {
log.debug("Will compare file content");
}
FromFileProperty property = (FromFileProperty) dslBody;
if (property.isString()) {
// continue processing as if body was pure string
dslBody = property.asString();
}
else if (!(inputMessage instanceof byte[])) {
if (log.isDebugEnabled()) {
log.debug("Contract provided byte comparison, but the input message is of type ["
+ inputMessage.getClass() + "]. Can't compare the two.");
}
return null;
}
else {
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
if (log.isDebugEnabled() && !matches) {
log.debug("Contract provided byte comparison, but the byte arrays don't match");
}
return matches ? groovyDsl : null;
}
}
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
return groovyDsl;
}
return null;
}
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
boolean matches;
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
if (type == ContentType.JSON) {
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
}
else if ((dslBody instanceof RegexProperty || dslBody instanceof Pattern) && inputMessage instanceof String) {
Pattern pattern = new RegexProperty(dslBody).getPattern();
matches = pattern.matcher((String) inputMessage).matches();
bodyUnmatchedLog(dslBody, matches, pattern);
}
else {
matches = dslBody.equals(inputMessage);
bodyUnmatchedLog(dslBody, matches, inputMessage);
}
return matches;
}
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
if (log.isDebugEnabled() && !matches) {
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
+ "]");
}
}
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
Object dslBody) {
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
JsonPaths jsonPaths = JsonToJsonPathsConverter
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
DocumentContext parsedJson;
try {
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
}
catch (JsonProcessingException e) {
throw new IllegalStateException("Cannot serialize to JSON", e);
}
List<String> unmatchedJsonPath = new ArrayList<>();
boolean matches = true;
for (MethodBufferingJsonVerifiable path : jsonPaths) {
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
}
if (matchers != null && matchers.hasMatchers()) {
for (BodyMatcher matcher : matchers.matchers()) {
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
}
}
if (!unmatchedJsonPath.isEmpty()) {
if (log.isDebugEnabled()) {
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
}
}
return matches;
}
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
try {
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
return true;
}
catch (Exception e) {
unmatchedJsonPath.add(e.getLocalizedMessage());
return false;
}
}
private List<String> headersMatch(Message<?> message, Contract groovyDsl) {
List<String> unmatchedHeaders = new ArrayList<>();
Map<String, Object> headers = message.getHeaders();
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
String name = it.getName();
Object value = it.getClientValue();
Object valueInHeader = headers.get(name);
boolean matches;
if (value instanceof RegexProperty || value instanceof Pattern) {
Pattern pattern = new RegexProperty(value).getPattern();
matches = pattern.matcher(valueInHeader.toString()).matches();
}
else {
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
}
if (!matches) {
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
}
}
return unmatchedHeaders;
}
private String unmatchedText(Object expectedValue) {
return expectedValue instanceof RegexProperty
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
: "be equal to [" + expectedValue + "]";
}
}

View File

@@ -1,73 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.stream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.springframework.cloud.contract.spec.Contract;
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
/**
* Sends forward a message defined in the DSL.
*
* @author Marcin Grzejszczak
*/
class StubRunnerStreamTransformer {
private final StubRunnerStreamMessageSelector selector;
StubRunnerStreamTransformer(Contract groovyDsl) {
this(Collections.singletonList(groovyDsl));
}
StubRunnerStreamTransformer(List<Contract> groovyDsls) {
this.selector = new StubRunnerStreamMessageSelector(groovyDsls);
}
public Message<?> transform(Message<?> source) {
Contract groovyDsl = matchingContract(source);
if (groovyDsl == null || groovyDsl.getOutputMessage() == null) {
return source;
}
byte[] outputBody = outputBodyAsBytes(groovyDsl);
Map<String, Object> headers = groovyDsl.getOutputMessage().getHeaders().asStubSideMap();
MessageHeaders messageHeaders = new MessageHeaders(headers);
Message<byte[]> message = MessageBuilder.createMessage(outputBody, messageHeaders);
this.selector.updateCache(message, groovyDsl);
return message;
}
private byte[] outputBodyAsBytes(Contract groovyDsl) {
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
if (outputBody instanceof FromFileProperty) {
FromFileProperty property = (FromFileProperty) outputBody;
return property.asBytes();
}
return BodyExtractor.extractStubValueFrom(outputBody).getBytes();
}
Contract matchingContract(Message<?> source) {
return this.selector.matchingContract(source);
}
}

View File

@@ -18,11 +18,11 @@ package org.springframework.cloud.contract.stubrunner.server;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.contract.stubrunner.StubRunning;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -36,17 +36,16 @@ public class HttpStubsController {
private final StubRunning stubRunning;
@Autowired
public HttpStubsController(StubRunning stubRunning) {
this.stubRunning = stubRunning;
}
@RequestMapping
@GetMapping
public Map<String, Integer> stubs() {
return this.stubRunning.runStubs().toIvyToPortMapping();
}
@RequestMapping(path = "/{ivy:.*}")
@GetMapping(path = "/{ivy:.*}")
public ResponseEntity<Integer> consumer(@PathVariable String ivy) {
Integer port = this.stubRunning.runStubs().getPort(ivy);
if (port != null) {

View File

@@ -21,7 +21,6 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
@@ -36,6 +35,8 @@ import org.springframework.cloud.contract.stubrunner.StubRunnerOptions;
import org.springframework.cloud.contract.stubrunner.StubRunnerOptionsBuilder;
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.noop.NoOpStubMessages;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -156,9 +157,12 @@ public class StubRunnerConfiguration {
}
@SuppressWarnings("unchecked")
class LazyMessageVerifier implements MessageVerifier {
private MessageVerifier<?> messageVerifier;
private MessageVerifierSender<?> messageVerifierSender;
private MessageVerifierReceiver<?> messageVerifierReceiver;
private final BeanFactory beanFactory;
@@ -166,36 +170,40 @@ class LazyMessageVerifier implements MessageVerifier {
this.beanFactory = beanFactory;
}
private MessageVerifier messageVerifier() {
if (this.messageVerifier == null) {
try {
this.messageVerifier = this.beanFactory.getBean(MessageVerifier.class);
}
catch (BeansException ex) {
this.messageVerifier = new NoOpStubMessages();
}
private MessageVerifierSender messageVerifierSender() {
if (this.messageVerifierSender == null) {
this.messageVerifierSender = this.beanFactory.getBeanProvider(MessageVerifierSender.class)
.getIfAvailable(NoOpStubMessages::new);
}
return this.messageVerifier;
return this.messageVerifierSender;
}
private MessageVerifierReceiver messageVerifierReceiver() {
if (this.messageVerifierReceiver == null) {
this.messageVerifierReceiver = this.beanFactory.getBeanProvider(MessageVerifierReceiver.class)
.getIfAvailable(NoOpStubMessages::new);
}
return this.messageVerifierReceiver;
}
@Override
public void send(Object message, String destination, YamlContract contract) {
messageVerifier().send(message, destination, contract);
messageVerifierSender().send(message, destination, contract);
}
@Override
public Object receive(String destination, long timeout, TimeUnit timeUnit, YamlContract contract) {
return messageVerifier().receive(destination, timeout, timeUnit, contract);
return messageVerifierReceiver().receive(destination, timeout, timeUnit, contract);
}
@Override
public Object receive(String destination, YamlContract contract) {
return messageVerifier().receive(destination, contract);
return messageVerifierReceiver().receive(destination, contract);
}
@Override
public void send(Object payload, Map headers, String destination, YamlContract contract) {
messageVerifier().send(payload, headers, destination, contract);
messageVerifierSender().send(payload, headers, destination, contract);
}
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2012-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.spring.cloud.eureka;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
/**
* Conditional that checks if Eureka is enabled.
*
* @author Marcin Grzejszczak
*
* @since 1.0.0
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@ConditionalOnProperty(value = "eureka.client.enabled", havingValue = "true", matchIfMissing = true)
@interface ConditionalOnEurekaEnabled {
}

View File

@@ -0,0 +1,171 @@
/*
* 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.spring.cloud.eureka;
import java.lang.invoke.MethodHandles;
import java.net.InetAddress;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import com.netflix.appinfo.ApplicationInfoManager;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.AbstractDiscoveryClientOptionalArgs;
import com.netflix.discovery.EurekaClient;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.boot.actuate.health.StatusAggregator;
import org.springframework.cloud.client.serviceregistry.ServiceRegistry;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.cloud.contract.stubrunner.StubConfiguration;
import org.springframework.cloud.contract.stubrunner.StubRunning;
import org.springframework.cloud.contract.stubrunner.spring.cloud.StubMapperProperties;
import org.springframework.cloud.contract.stubrunner.spring.cloud.StubsRegistrar;
import org.springframework.cloud.loadbalancer.support.SimpleObjectProvider;
import org.springframework.cloud.netflix.eureka.CloudEurekaClient;
import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean;
import org.springframework.cloud.netflix.eureka.EurekaHealthCheckHandler;
import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean;
import org.springframework.cloud.netflix.eureka.InstanceInfoFactory;
import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaRegistration;
import org.springframework.context.ApplicationContext;
import org.springframework.util.StringUtils;
/**
* Registers all stubs in Eureka Service Discovery.
*
* @author Marcin Grzejszczak
* @since 1.0.0
*/
public class EurekaStubsRegistrar implements StubsRegistrar {
private static final Log log = LogFactory.getLog(MethodHandles.lookup().lookupClass());
private final StubRunning stubRunning;
private final StubMapperProperties stubMapperProperties;
private final InetUtils inetUtils;
private final EurekaInstanceConfigBean eurekaInstanceConfigBean;
private final EurekaClientConfigBean eurekaClientConfigBean;
private final List<EurekaRegistration> registrations = new LinkedList<>();
private final ServiceRegistry<EurekaRegistration> serviceRegistry;
private final ApplicationContext context;
public EurekaStubsRegistrar(StubRunning stubRunning, ServiceRegistry<EurekaRegistration> serviceRegistry,
StubMapperProperties stubMapperProperties, InetUtils inetUtils,
EurekaInstanceConfigBean eurekaInstanceConfigBean, EurekaClientConfigBean eurekaClientConfigBean,
ApplicationContext context) {
this.stubRunning = stubRunning;
this.stubMapperProperties = stubMapperProperties;
this.serviceRegistry = serviceRegistry;
this.inetUtils = inetUtils;
this.eurekaInstanceConfigBean = eurekaInstanceConfigBean;
this.eurekaClientConfigBean = eurekaClientConfigBean;
this.context = context;
}
@Override
public void registerStubs() {
Map<StubConfiguration, Integer> activeStubs = this.stubRunning.runStubs().validNamesAndPorts();
for (Map.Entry<StubConfiguration, Integer> entry : activeStubs.entrySet()) {
EurekaInstanceConfigBean instance = registration(entry);
log.info("Will register stub in Eureka " + "[" + instance.getAppname() + ", " + instance.getHostname()
+ ", " + instance.getNonSecurePort() + ", " + instance.getInstanceId() + "]");
InstanceInfo instanceInfo = new InstanceInfoFactory().create(instance);
ApplicationInfoManager applicationInfoManager = new ApplicationInfoManager(instance, instanceInfo);
AbstractDiscoveryClientOptionalArgs<?> args = args();
EurekaClient client = new CloudEurekaClient(applicationInfoManager, this.eurekaClientConfigBean, args,
this.context);
EurekaRegistration registration = EurekaRegistration.builder(instance)
.with(this.eurekaClientConfigBean, this.context).with(client).build();
EurekaHealthCheckHandler eurekaHealthCheckHandler = new EurekaHealthCheckHandler(
StatusAggregator.getDefault());
eurekaHealthCheckHandler.setApplicationContext(context);
eurekaHealthCheckHandler.afterPropertiesSet();
registration.setHealthCheckHandler(new SimpleObjectProvider<>(eurekaHealthCheckHandler));
this.registrations.add(registration);
try {
this.serviceRegistry.register(registration);
log.info("Successfully registered stub " + "[" + entry.getKey().toColonSeparatedDependencyNotation()
+ "] in Service Discovery");
}
catch (Exception e) {
log.warn("Exception occurred while trying to register a stub ["
+ entry.getKey().toColonSeparatedDependencyNotation() + "] in Service Discovery", e);
}
}
}
private AbstractDiscoveryClientOptionalArgs<?> args() {
try {
return this.context.getBean(AbstractDiscoveryClientOptionalArgs.class);
}
catch (BeansException e) {
return null;
}
}
private EurekaInstanceConfigBean registration(Map.Entry<StubConfiguration, Integer> entry) {
EurekaInstanceConfigBean config = new EurekaInstanceConfigBean(this.inetUtils);
String appName = name(entry.getKey());
config.setInstanceEnabledOnit(true);
InetAddress address = this.inetUtils.findFirstNonLoopbackAddress();
config.setIpAddress(address.getHostAddress());
config.setHostname(StringUtils.hasText(hostName(entry)) ? hostName(entry) : address.getHostName());
config.setAppname(appName);
config.setVirtualHostName(appName);
config.setSecureVirtualHostName(appName);
int port = port(entry);
config.setNonSecurePort(port);
config.setInstanceId(address.getHostAddress() + ":" + entry.getKey().getArtifactId() + ":" + port);
config.setLeaseRenewalIntervalInSeconds(1);
return config;
}
protected String hostName(Map.Entry<StubConfiguration, Integer> entry) {
return this.eurekaInstanceConfigBean.getHostname();
}
protected int port(Map.Entry<StubConfiguration, Integer> entry) {
return entry.getValue();
}
private String name(StubConfiguration stubConfiguration) {
String resolvedName = this.stubMapperProperties
.fromIvyNotationToId(stubConfiguration.toColonSeparatedDependencyNotation());
if (StringUtils.hasText(resolvedName)) {
return resolvedName;
}
return stubConfiguration.getArtifactId();
}
@Override
public void close() throws Exception {
for (EurekaRegistration registration : this.registrations) {
this.serviceRegistry.deregister(registration);
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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.spring.cloud.eureka;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.client.serviceregistry.ServiceRegistry;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.cloud.contract.stubrunner.StubConfiguration;
import org.springframework.cloud.contract.stubrunner.StubRunning;
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerConfiguration;
import org.springframework.cloud.contract.stubrunner.spring.cloud.ConditionalOnStubbedDiscoveryDisabled;
import org.springframework.cloud.contract.stubrunner.spring.cloud.StubMapperProperties;
import org.springframework.cloud.contract.stubrunner.spring.cloud.StubsRegistrar;
import org.springframework.cloud.netflix.eureka.CloudEurekaClient;
import org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration;
import org.springframework.cloud.netflix.eureka.EurekaClientConfigBean;
import org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean;
import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaRegistration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
/**
* Autoconfiguration for registering stubs in a Eureka Service discovery.
*
* @author Marcin Grzejszczak
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter({ StubRunnerConfiguration.class, EurekaClientAutoConfiguration.class })
@ConditionalOnClass(CloudEurekaClient.class)
@ConditionalOnStubbedDiscoveryDisabled
@ConditionalOnEurekaEnabled
@ConditionalOnProperty(value = "stubrunner.cloud.eureka.enabled", matchIfMissing = true)
public class StubRunnerSpringCloudEurekaAutoConfiguration {
@Profile("!cloud")
@Configuration(proxyBeanMethods = false)
protected static class NonCloudConfig {
@Bean(initMethod = "registerStubs")
public StubsRegistrar stubsRegistrar(StubRunning stubRunning,
ServiceRegistry<EurekaRegistration> serviceRegistry, ApplicationContext context,
StubMapperProperties stubMapperProperties, InetUtils inetUtils,
EurekaInstanceConfigBean eurekaInstanceConfigBean, EurekaClientConfigBean eurekaClientConfigBean) {
return new EurekaStubsRegistrar(stubRunning, serviceRegistry, stubMapperProperties, inetUtils,
eurekaInstanceConfigBean, eurekaClientConfigBean, context);
}
}
@Profile("cloud")
@Configuration(proxyBeanMethods = false)
protected static class CloudConfig {
private static final int DEFAULT_PORT = 80;
private static final Log log = LogFactory.getLog(CloudConfig.class);
@Autowired
Environment environment;
@Bean(initMethod = "registerStubs")
public StubsRegistrar stubsRegistrar(StubRunning stubRunning,
ServiceRegistry<EurekaRegistration> serviceRegistry, ApplicationContext context,
StubMapperProperties stubMapperProperties, InetUtils inetUtils,
EurekaInstanceConfigBean eurekaInstanceConfigBean, EurekaClientConfigBean eurekaClientConfigBean) {
return new EurekaStubsRegistrar(stubRunning, serviceRegistry, stubMapperProperties, inetUtils,
eurekaInstanceConfigBean, eurekaClientConfigBean, context) {
@Override
protected String hostName(Map.Entry<StubConfiguration, Integer> entry) {
String hostname = CloudConfig.this.environment.getProperty("application.hostname") + "-"
+ entry.getValue() + "." + CloudConfig.this.environment.getProperty("application.domain");
log.info("Registering stub [" + entry.getKey().getArtifactId() + "] with hostname [" + hostname
+ "]");
return hostname;
}
@Override
protected int port(Map.Entry<StubConfiguration, Integer> entry) {
return DEFAULT_PORT;
}
};
}
}
}

View File

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

View File

@@ -77,7 +77,7 @@ class StubRunnerExecutorSpec extends Specification {
int port = TestSocketUtils.findAvailableTcpPort()
StubRunnerExecutor executor = new StubRunnerExecutor(portScanner)
stubRunnerOptions = new StubRunnerOptionsBuilder(stubIdsToPortMapping:
stubIdsWithPortsFromString("group:artifact:${port},someotherartifact:${SocketUtils.findAvailableTcpPort()}"))
stubIdsWithPortsFromString("group:artifact:${port},someotherartifact:${TestSocketUtils.findAvailableTcpPort()}"))
.build()
when:
executor.runStubs(stubRunnerOptions, repository, stub)

View File

@@ -1,205 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.camel
import org.apache.camel.Exchange
import org.apache.camel.Message
import org.apache.camel.impl.DefaultCamelContext
import org.apache.camel.support.DefaultExchange
import org.apache.camel.support.DefaultMessage
import spock.lang.Specification
import org.springframework.cloud.contract.spec.Contract
/**
* @author Marcin Grzejszczak
*/
class StubRunnerCamelPredicateSpec extends Specification {
Exchange exchange = new DefaultExchange(new DefaultCamelContext())
Message message = new DefaultMessage(exchange)
def "should return false if headers don't match"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageBody(foo: "bar")
messageHeaders {
header("foo", $(c(regex("[0-9]{3}")), p(123)))
}
}
}
and:
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
exchange.in = message
message.headers = [
foo: "non matching stuff"
]
expect:
!predicate.matches(exchange)
}
def "should return false if headers match and body doesn't"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
}
messageBody(foo: $(c(regex("[0-9]{3}")), p(123)))
}
}
and:
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
exchange.in = message
message.headers = [
foo: 123
]
message.body = [
foo: "non matching stuff"
]
expect:
!predicate.matches(exchange)
}
def "should return false if headers match and body doesn't when it's using matchers"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
}
messageBody(foo: "non matching stuff")
bodyMatchers {
jsonPath('$.foo', byRegex("[0-9]{3}"))
}
}
}
and:
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
exchange.in >> message
message.headers >> [
foo: 123
]
message.body >> [
foo: "non matching stuff"
]
expect:
!predicate.matches(exchange)
}
def "should return true if headers and body match"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
}
messageBody(foo: $(c(regex("[0-9]{3}")), p(123)))
}
}
and:
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
exchange.in = message
message.headers = [
foo: 123
]
message.body = [
foo: 123
]
expect:
predicate.matches(exchange)
}
def "should return true if headers and body using matchers match"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
}
messageBody(foo: 123)
bodyMatchers {
jsonPath('$.foo', byRegex("[0-9]{3}"))
}
}
}
and:
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
exchange.in = message
message.headers = [
foo: 123
]
message.body = [
foo: 123
]
expect:
predicate.matches(exchange)
}
def "should return true if headers and byte body matches"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
messagingContentType(applicationOctetStream())
}
messageBody(fileAsBytes("request.pdf"))
}
}
and:
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
exchange.in = message
message.headers = [
foo : 123,
contentType: "application/octet-stream"
]
message.body = StubRunnerCamelPredicate.getResource("/request.pdf").bytes
expect:
predicate.matches(exchange)
}
def "should return false if byte body types don't match for binary"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
messagingContentType(applicationOctetStream())
}
messageBody(fileAsBytes("request.pdf"))
}
}
and:
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
exchange.in = message
message.headers = [
foo : 123,
contentType: "application/octet-stream"
]
message.body = "hello world"
expect:
!predicate.matches(exchange)
}
}

View File

@@ -1,221 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.integration
import spock.lang.Issue
import spock.lang.Specification
import org.springframework.cloud.contract.spec.Contract
import org.springframework.messaging.Message
/**
* @author Marcin Grzejszczak
*/
class StubRunnerIntegrationMessageSelectorSpec extends Specification {
Message message = Mock(Message)
def "should return false if headers don't match"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageBody(foo: "bar")
messageHeaders {
header("foo", $(c(regex("[0-9]{3}")), p(123)))
}
}
}
and:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
message.headers >> [
foo: "non matching stuff"
]
expect:
!predicate.accept(message)
}
def "should return false if headers match and body doesn't"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
}
messageBody(foo: $(c(regex("[0-9]{3}")), p(123)))
}
}
and:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
message.headers >> [
foo: 123
]
message.payload >> [
foo: "non matching stuff"
]
expect:
!predicate.accept(message)
}
def "should return false if headers match and body doesn't when it's using matchers"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
}
messageBody(foo: "non matching stuff")
bodyMatchers {
jsonPath('$.foo', byRegex("[0-9]{3}"))
}
}
}
and:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
message.headers >> [
foo: 123
]
message.payload >> [
foo: "non matching stuff"
]
expect:
!predicate.accept(message)
}
def "should return true if headers and byte body matches"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
messagingContentType(applicationOctetStream())
}
messageBody(fileAsBytes("request.pdf"))
}
}
and:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
message.headers >> [
foo : 123,
contentType: "application/octet-stream"
]
message.payload >> StubRunnerIntegrationMessageSelector.getResource("/request.pdf").bytes
expect:
predicate.accept(message)
}
def "should return false if byte body types don't match for binary"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
messagingContentType(applicationOctetStream())
}
messageBody(fileAsBytes("request.pdf"))
}
}
and:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
message.headers >> [
foo : 123,
contentType: "application/octet-stream"
]
message.payload >> "hello world"
expect:
!predicate.accept(message)
}
def "should return true if headers and body match"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
}
messageBody(foo: $(c(regex("[0-9]{3}")), p(123)))
}
}
and:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
message.headers >> [
foo: 123
]
message.payload >> [
foo: 123
]
expect:
predicate.accept(message)
}
def "should return true if headers and body using matchers match"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
}
messageBody(foo: 123)
bodyMatchers {
jsonPath('$.foo', byRegex("[0-9]{3}"))
}
}
}
and:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(
dsl)
message.headers >> [
foo: 123
]
message.payload >> [
foo: 123
]
expect:
predicate.accept(message)
}
@Issue("1382")
def "should return true if header matches regex"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", $(anyUuid()))
}
messageBody(foo: 123)
}
}
and:
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(
dsl)
message.headers >> [
foo: "fbcc2ed3-dbac-47e7-9a4b-c2d55709792c"
]
message.payload >> [
foo: 123
]
expect:
predicate.accept(message)
}
}

View File

@@ -1,250 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.contract.stubrunner.messaging.stream
import spock.lang.Issue
import spock.lang.Specification
import org.springframework.cloud.contract.spec.Contract
import org.springframework.http.MediaType
import org.springframework.messaging.Message
/**
* @author Marcin Grzejszczak
*/
class StubRunnerStreamMessageSelectorSpec extends Specification {
Message message = Mock(Message)
def "should return false if headers don't match"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageBody(foo: "bar")
messageHeaders {
header("foo", $(c(regex("[0-9]{3}")), p(123)))
}
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
message.headers >> [
foo: "non matching stuff"
]
expect:
!predicate.accept(message)
}
def "should return false if headers match and body doesn't"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
}
messageBody(foo: $(c(regex("[0-9]{3}")), p(123)))
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
message.headers >> [
foo: 123
]
message.payload >> [
foo: "non matching stuff"
]
expect:
!predicate.accept(message)
}
def "should return false if headers match and body doesn't when it's using matchers"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
}
messageBody(foo: "non matching stuff")
bodyMatchers {
jsonPath('$.foo', byRegex("[0-9]{3}"))
}
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
message.headers >> [
foo: 123
]
message.payload >> [
foo: "non matching stuff"
]
expect:
!predicate.accept(message)
}
def "should return true if headers and body match"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
header("bar", "bar")
messagingContentType(applicationJsonUtf8())
header("regex", regex("234"))
}
messageBody(foo: $(c(regex("[0-9]{3}")), p(123)))
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
message.headers >> [
foo : 123,
bar : "bar",
contentType: MediaType.APPLICATION_JSON_UTF8,
regex : 234
]
message.payload >> [
foo: 123
]
expect:
predicate.accept(message)
}
def "should return true if headers and text body matches"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
messagingContentType(textPlain())
}
messageBody($(c(regex("[0-9]{3}")), p("123")))
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
message.headers >> [
foo : 123,
contentType: "text/plain"
]
message.payload >> "123"
expect:
predicate.accept(message)
}
def "should return true if headers and byte body matches"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
messagingContentType(applicationOctetStream())
}
messageBody(fileAsBytes("request.pdf"))
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
message.headers >> [
foo : 123,
contentType: "application/octet-stream"
]
message.payload >> StubRunnerStreamMessageSelectorSpec.getResource("/request.pdf").bytes
expect:
predicate.accept(message)
}
def "should return false if byte body types don't match for binary"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
messagingContentType(applicationOctetStream())
}
messageBody(fileAsBytes("request.pdf"))
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
message.headers >> [
foo : 123,
contentType: "application/octet-stream"
]
message.payload >> "hello world"
expect:
!predicate.accept(message)
}
def "should return true if headers and body using matchers match"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", 123)
}
messageBody(foo: 123)
bodyMatchers {
jsonPath('$.foo', byRegex("[0-9]{3}"))
}
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(
dsl)
message.headers >> [
foo: 123
]
message.payload >> [
foo: 123
]
expect:
predicate.accept(message)
}
@Issue("1382")
def "should return true if header matches regex"() {
given:
Contract dsl = Contract.make {
input {
messageFrom "foo"
messageHeaders {
header("foo", $(anyUuid()))
}
messageBody(foo: 123)
}
}
and:
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(
dsl)
message.headers >> [
foo: "fbcc2ed3-dbac-47e7-9a4b-c2d55709792c"
]
message.payload >> [
foo: 123
]
expect:
predicate.accept(message)
}
}

View File

@@ -22,8 +22,10 @@ import com.github.tomakehurst.wiremock.extension.Parameters
import com.github.tomakehurst.wiremock.extension.ResponseTransformer
import com.github.tomakehurst.wiremock.http.ChunkedDribbleDelay
import com.github.tomakehurst.wiremock.http.HttpHeader
import com.github.tomakehurst.wiremock.http.HttpHeaders
import com.github.tomakehurst.wiremock.http.Request
import com.github.tomakehurst.wiremock.http.Response
import groovy.transform.CompileStatic
import org.springframework.cloud.contract.verifier.dsl.wiremock.DefaultResponseTransformer
import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockExtensions
@@ -31,16 +33,15 @@ import org.springframework.cloud.contract.verifier.dsl.wiremock.WireMockExtensio
/**
* Extension that registers the default response transformer and a custom one too
*/
@CompileStatic
class TestWireMockExtensions implements WireMockExtensions {
@Override
List<Extension> extensions() {
return [
new DefaultResponseTransformer(),
new CustomExtension()
]
return [ new DefaultResponseTransformer(), new CustomExtension() ] as List<Extension>
}
}
@CompileStatic
class CustomExtension extends ResponseTransformer {
/**
@@ -57,10 +58,18 @@ class CustomExtension extends ResponseTransformer {
*/
@Override
Response transform(Request request, Response response, FileSource files, Parameters parameters) {
def headers = response.headers + new HttpHeader("X-My-Header", "surprise!")
return new Response(response.status, response.statusMessage,
response.body, headers, response.wasConfigured(), response.fault,
response.initialDelay, new ChunkedDribbleDelay(0, 0), response.fromProxy)
HttpHeaders headers = response.headers + new HttpHeader("X-My-Header", "surprise!")
return Response.response()
.status(response.status)
.statusMessage(response.statusMessage)
.body(response.body)
.headers(headers)
.configured(response.wasConfigured())
.fault(response.fault)
.incrementInitialDelay(response.initialDelay)
.chunkedDribbleDelay(response.chunkedDribbleDelay)
.fromProxy(response.fromProxy)
.build()
}
/**

View File

@@ -19,6 +19,7 @@ package org.springframework.cloud.contract.stubrunner.provider.wiremock
import com.github.tomakehurst.wiremock.http.RequestMethod
import com.github.tomakehurst.wiremock.stubbing.StubMapping
import org.junit.Rule
import spock.lang.Ignore
import spock.lang.Specification
import org.springframework.boot.test.system.OutputCaptureRule
@@ -66,10 +67,11 @@ class WireMockHttpServerStubSpec extends Specification {
mappingDescriptor?.stop()
}
@Ignore("There's sth wrong with SLF4J versions")
def 'should make WireMock print out logs on INFO'() {
given:
WireMockHttpServerStub mappingDescriptor = new WireMockHttpServerStub().start(new HttpServerStubConfiguration(HttpServerStubConfigurer.NoOpHttpServerStubConfigurer.INSTANCE, null,
null, SocketUtils.findAvailableTcpPort())) as WireMockHttpServerStub
null, TestSocketUtils.findAvailableTcpPort())) as WireMockHttpServerStub
mappingDescriptor.registerMappings([
new File(WireMockHttpServerStubSpec.classLoader.getResource("simple.json").toURI())
])

View File

@@ -18,14 +18,15 @@ package org.springframework.cloud.contract.stubrunner.server
import groovy.json.JsonSlurper
import io.restassured.module.mockmvc.RestAssuredMockMvc
import spock.lang.Specification
import org.assertj.core.api.BDDAssertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootContextLoader
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.contract.stubrunner.StubRunning
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.ContextConfiguration
/**
* @author Marcin Grzejszczak
@@ -33,88 +34,99 @@ import org.springframework.test.context.ContextConfiguration
// tag::boot_usage[]
@SpringBootTest(classes = StubRunnerBoot, properties = "spring.cloud.zookeeper.enabled=false")
@ActiveProfiles("test")
class StubRunnerBootSpec extends Specification {
class StubRunnerBootSpec {
@Autowired
StubRunning stubRunning
def setup() {
@BeforeEach
void setup() {
RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning),
new TriggerController(stubRunning))
}
def 'should return a list of running stub servers in "full ivy:port" notation'() {
@Test
void 'should return a list of running stub servers in "full ivy port" notation'() {
when:
String response = RestAssuredMockMvc.get('/stubs').body.asString()
then:
def root = new JsonSlurper().parseText(response)
root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs' instanceof Integer
assert root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs' instanceof Integer
}
def 'should return a port on which a [#stubId] stub is running'() {
when:
def response = RestAssuredMockMvc.get("/stubs/${stubId}")
then:
response.statusCode == 200
Integer.valueOf(response.body.asString()) > 0
where:
stubId << ['org.springframework.cloud.contract.verifier.stubs:bootService:+:stubs',
'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs',
'org.springframework.cloud.contract.verifier.stubs:bootService:+',
'org.springframework.cloud.contract.verifier.stubs:bootService',
'bootService']
@Test
void 'should return a port on which a #stubId stub is running'() {
given:
def stubIds = ['org.springframework.cloud.contract.verifier.stubs:bootService:+:stubs',
'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs',
'org.springframework.cloud.contract.verifier.stubs:bootService:+',
'org.springframework.cloud.contract.verifier.stubs:bootService',
'bootService']
stubIds.each {
when:
def response = RestAssuredMockMvc.get("/stubs/${it}")
then:
assert response.statusCode == 200
assert Integer.valueOf(response.body.asString()) > 0
}
}
def 'should return 404 when missing stub was called'() {
@Test
void 'should return 404 when missing stub was called'() {
when:
def response = RestAssuredMockMvc.get("/stubs/a:b:c:d")
then:
response.statusCode == 404
assert response.statusCode == 404
}
def 'should return a list of messaging labels that can be triggered when version and classifier are passed'() {
@Test
void 'should return a list of messaging labels that can be triggered when version and classifier are passed'() {
when:
String response = RestAssuredMockMvc.get('/triggers').body.asString()
then:
def root = new JsonSlurper().parseText(response)
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"])
}
def 'should trigger a messaging label'() {
@Test
void 'should trigger a messaging label'() {
given:
StubRunning stubRunning = Mock()
StubRunning stubRunning = Mockito.mock(StubRunning)
RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
when:
def response = RestAssuredMockMvc.post("/triggers/delete_book")
then:
response.statusCode == 200
and:
1 * stubRunning.trigger('delete_book')
Mockito.verify(stubRunning).trigger('delete_book')
}
def 'should trigger a messaging label for a stub with [#stubId] ivy notation'() {
@Test
void 'should trigger a messaging label for a stub with #stubId ivy notation'() {
given:
StubRunning stubRunning = Mock()
StubRunning stubRunning = Mockito.mock(StubRunning)
RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
when:
def response = RestAssuredMockMvc.post("/triggers/$stubId/delete_book")
then:
response.statusCode == 200
and:
1 * stubRunning.trigger(stubId, 'delete_book')
where:
stubId << ['org.springframework.cloud.contract.verifier.stubs:bootService:stubs', 'org.springframework.cloud.contract.verifier.stubs:bootService', 'bootService']
def stubIds = ['org.springframework.cloud.contract.verifier.stubs:bootService:stubs', 'org.springframework.cloud.contract.verifier.stubs:bootService', 'bootService']
stubIds.each {
when:
def response = RestAssuredMockMvc.post("/triggers/$it/delete_book")
then:
assert response.statusCode == 200
and:
Mockito.verify(stubRunning).trigger(it, 'delete_book')
}
}
def 'should throw exception when trigger is missing'() {
@Test
void 'should throw exception when trigger is missing'() {
when:
RestAssuredMockMvc.post("/triggers/missing_label")
then:
Exception e = thrown(Exception)
e.message.contains("Exception occurred while trying to return [missing_label] label.")
e.message.contains("Available labels are")
e.message.contains("org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs=[]")
e.message.contains("org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs=")
BDDAssertions.thenThrownBy(() -> RestAssuredMockMvc.post("/triggers/missing_label"))
.hasMessageContaining("Exception occurred while trying to return [missing_label] label.")
.hasMessageContaining("Available labels are")
.hasMessageContaining("org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs=[]")
.hasMessageContaining("org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs=")
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.serverexamples;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.contract.stubrunner.server.EnableStubRunnerServer;
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner;
/**
* @author Marcin Grzejszczak
*/
// tag::stubrunnereureka[]
@SpringBootApplication
@EnableStubRunnerServer
@AutoConfigureStubRunner
public class StubRunnerBootEurekaExample {
public static void main(String[] args) {
SpringApplication.run(StubRunnerBootEurekaExample.class, args);
}
}
// end::stubrunnereureka[]
/*
*
* // tag::stubrunnereureka_args[]
* -Dstubrunner.repositoryRoot=https://repo.spring.io/snapshot (1)
* -Dstubrunner.cloud.stubbed.discovery.enabled=false (2)
* -Dstubrunner.ids=org.springframework.cloud.contract.verifier.stubs:loanIssuance,org.
* springframework.cloud.contract.verifier.stubs:fraudDetectionServer,org.springframework.
* cloud.contract.verifier.stubs:bootService (3)
* -Dstubrunner.idsToServiceIds.fraudDetectionServer=
* someNameThatShouldMapFraudDetectionServer (4)
*
* (1) - we tell Stub Runner where all the stubs reside (2) - we don't want the default
* behaviour where the discovery service is stubbed. That's why the stub registration will
* be picked (3) - we provide a list of stubs to download (4) - we provide a list of
* artifactId to serviceId mapping // end::stubrunnereureka_args[]
*
*
* -Dstubrunner.cloud.eureka.enabled=true
* -Dstubrunner.repositoryRoot=classpath:m2repo/repository/
* -Dstubrunner.camel.enabled=false -Dspring.cloud.zookeeper.enabled=false
* -Dspring.cloud.zookeeper.discovery.enabled=false -Ddebug=true
*
*/

View File

@@ -20,8 +20,10 @@ import com.github.tomakehurst.wiremock.core.WireMockConfiguration
import groovy.transform.CompileStatic
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import spock.lang.Issue
import spock.lang.Specification
import org.assertj.core.api.BDDAssertions
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
@@ -44,131 +46,137 @@ import org.springframework.test.context.ActiveProfiles
// Not necessary if Spring Cloud is used. TODO: make it work without this.
// tag::test[]
@SpringBootTest(classes = Config, properties = [" stubrunner.cloud.enabled=false",
'foo=${stubrunner.runningstubs.fraudDetectionServer.port}',
'fooWithGroup=${stubrunner.runningstubs.org.springframework.cloud.contract.verifier.stubs.fraudDetectionServer.port}'])
'foo=${stubrunner.runningstubs.fraudDetectionServer.port}',
'fooWithGroup=${stubrunner.runningstubs.org.springframework.cloud.contract.verifier.stubs.fraudDetectionServer.port}'])
// tag::annotation[]
@AutoConfigureStubRunner(mappingsOutputFolder = "target/outputmappings/",
httpServerStubConfigurer = HttpsForFraudDetection)
httpServerStubConfigurer = HttpsForFraudDetection)
// end::annotation[]
@ActiveProfiles("test")
class StubRunnerConfigurationSpec extends Specification {
class StubRunnerConfigurationSpec {
@Autowired
StubFinder stubFinder
@Autowired
Environment environment
@StubRunnerPort("fraudDetectionServer")
int fraudDetectionServerPort
@StubRunnerPort("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")
int fraudDetectionServerPortWithGroupId
@Value('${foo}')
Integer foo
@Autowired
StubFinder stubFinder
@Autowired
Environment environment
@StubRunnerPort("fraudDetectionServer")
int fraudDetectionServerPort
@StubRunnerPort("org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer")
int fraudDetectionServerPortWithGroupId
@Value('${foo}')
Integer foo
void setupSpec() {
System.clearProperty("stubrunner.repository.root")
System.clearProperty("stubrunner.classifier")
WireMockHttpServerStubAccessor.clear()
}
@BeforeAll
static void setupSpec() {
System.clearProperty("stubrunner.repository.root")
System.clearProperty("stubrunner.classifier")
WireMockHttpServerStubAccessor.clear()
}
void cleanupSpec() {
setupSpec()
}
@AfterAll
static void cleanupSpec() {
setupSpec()
}
def 'should mark all ports as random'() {
expect:
WireMockHttpServerStubAccessor.everyPortRandom()
}
@Test
void 'should mark all ports as random'() {
expect:
WireMockHttpServerStubAccessor.everyPortRandom()
}
def 'should start WireMock servers'() {
expect: 'WireMocks are running'
stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') != null
stubFinder.findStubUrl('loanIssuance') != null
stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance')
stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance')
stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs')
stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') != null
and:
stubFinder.findAllRunningStubs().isPresent('loanIssuance')
stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs', 'fraudDetectionServer')
stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer')
and: 'Stubs were registered'
"${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
"${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
and: 'Fraud Detection is an HTTPS endpoint'
stubFinder.findStubUrl('fraudDetectionServer').toString().startsWith("https")
}
@Test
void 'should start WireMock servers'() {
expect: 'WireMocks are running'
assert stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance') != null
assert stubFinder.findStubUrl('loanIssuance') != null
assert stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs', 'loanIssuance')
assert stubFinder.findStubUrl('loanIssuance') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance')
assert stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT') == stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs')
assert stubFinder.findStubUrl('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer') != null
and:
assert stubFinder.findAllRunningStubs().isPresent('loanIssuance')
assert stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs', 'fraudDetectionServer')
assert stubFinder.findAllRunningStubs().isPresent('org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer')
and: 'Stubs were registered'
assert "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
assert "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
and: 'Fraud Detection is an HTTPS endpoint'
assert stubFinder.findStubUrl('fraudDetectionServer').toString().startsWith("https")
}
def 'should throw an exception when stub is not found'() {
when:
stubFinder.findStubUrl('nonExistingService')
then:
thrown(StubNotFoundException)
when:
stubFinder.findStubUrl('nonExistingGroupId', 'nonExistingArtifactId')
then:
thrown(StubNotFoundException)
}
@Test
void 'should throw an exception when stub is not found'() {
when:
BDDAssertions.thenThrownBy(() -> stubFinder.findStubUrl('nonExistingService')).isInstanceOf(StubNotFoundException)
when:
BDDAssertions.thenThrownBy(() -> stubFinder.findStubUrl('nonExistingGroupId', 'nonExistingArtifactId'))
.isInstanceOf(StubNotFoundException)
}
def 'should register started servers as environment variables'() {
expect:
environment.getProperty("stubrunner.runningstubs.loanIssuance.port") != null
stubFinder.findAllRunningStubs().getPort("loanIssuance") == (environment.getProperty("stubrunner.runningstubs.loanIssuance.port") as Integer)
and:
environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") != null
stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") == (environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") as Integer)
and:
environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") != null
stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") == (environment.getProperty("stubrunner.runningstubs.org.springframework.cloud.contract.verifier.stubs.fraudDetectionServer.port") as Integer)
}
@Test
void 'should register started servers as environment variables'() {
expect:
assert environment.getProperty("stubrunner.runningstubs.loanIssuance.port") != null
assert stubFinder.findAllRunningStubs().getPort("loanIssuance") == (environment.getProperty("stubrunner.runningstubs.loanIssuance.port") as Integer)
and:
assert environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") != null
assert stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") == (environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") as Integer)
and:
assert environment.getProperty("stubrunner.runningstubs.fraudDetectionServer.port") != null
assert stubFinder.findAllRunningStubs().getPort("fraudDetectionServer") == (environment.getProperty("stubrunner.runningstubs.org.springframework.cloud.contract.verifier.stubs.fraudDetectionServer.port") as Integer)
}
def 'should be able to interpolate a running stub in the passed test property'() {
given:
int fraudPort = stubFinder.findAllRunningStubs().getPort("fraudDetectionServer")
expect:
fraudPort > 0
environment.getProperty("foo", Integer) == fraudPort
environment.getProperty("fooWithGroup", Integer) == fraudPort
foo == fraudPort
}
@Test
void 'should be able to interpolate a running stub in the passed test property'() {
given:
int fraudPort = stubFinder.findAllRunningStubs().getPort("fraudDetectionServer")
expect:
assert fraudPort > 0
assert environment.getProperty("foo", Integer) == fraudPort
assert environment.getProperty("fooWithGroup", Integer) == fraudPort
assert foo == fraudPort
}
@Issue("#573")
def 'should be able to retrieve the port of a running stub via an annotation'() {
given:
int fraudPort = stubFinder.findAllRunningStubs().getPort("fraudDetectionServer")
expect:
fraudPort > 0
fraudDetectionServerPort == fraudPort
fraudDetectionServerPortWithGroupId == fraudPort
}
// @Issue("#573")
@Test
void 'should be able to retrieve the port of a running stub via an annotation'() {
given:
int fraudPort = stubFinder.findAllRunningStubs().getPort("fraudDetectionServer")
expect:
assert fraudPort > 0
assert fraudDetectionServerPort == fraudPort
assert fraudDetectionServerPortWithGroupId == fraudPort
}
def 'should dump all mappings to a file'() {
when:
def url = stubFinder.findStubUrl("fraudDetectionServer")
then:
new File("target/outputmappings/", "fraudDetectionServer_${url.port}").exists()
}
@Test
void 'should dump all mappings to a file'() {
when:
def url = stubFinder.findStubUrl("fraudDetectionServer")
then:
assert new File("target/outputmappings/", "fraudDetectionServer_${url.port}").exists()
}
@Configuration
@EnableAutoConfiguration
static class Config {}
@Configuration
@EnableAutoConfiguration
static class Config {}
// tag::wireMockHttpServerStubConfigurer[]
@CompileStatic
static class HttpsForFraudDetection extends WireMockHttpServerStubConfigurer {
// tag::wireMockHttpServerStubConfigurer[]
@CompileStatic
static class HttpsForFraudDetection extends WireMockHttpServerStubConfigurer {
private static final Log log = LogFactory.getLog(HttpsForFraudDetection)
private static final Log log = LogFactory.getLog(HttpsForFraudDetection)
@Override
WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) {
if (httpServerStubConfiguration.stubConfiguration.artifactId == "fraudDetectionServer") {
int httpsPort = TestSocketUtils.findAvailableTcpPort()
log.info("Will set HTTPs port [" + httpsPort + "] for fraud detection server")
return httpStubConfiguration
.httpsPort(httpsPort)
}
return httpStubConfiguration
}
}
// end::wireMockHttpServerStubConfigurer[]
@Override
WireMockConfiguration configure(WireMockConfiguration httpStubConfiguration, HttpServerStubConfiguration httpServerStubConfiguration) {
if (httpServerStubConfiguration.stubConfiguration.artifactId == "fraudDetectionServer") {
int httpsPort = TestSocketUtils.findAvailableTcpPort()
log.info("Will set HTTPs port [" + httpsPort + "] for fraud detection server")
return httpStubConfiguration
.httpsPort(httpsPort)
}
return httpStubConfiguration
}
}
// end::wireMockHttpServerStubConfigurer[]
}
// end::test[]

View File

@@ -16,7 +16,7 @@
package org.springframework.cloud.contract.stubrunner.spring
import spock.lang.Specification
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
@@ -30,7 +30,7 @@ import org.springframework.test.context.ActiveProfiles
@SpringBootTest(classes = Config, properties = ['some.property1=org.springframework.cloud.contract.verifier.stubs:loanIssuance'])
@AutoConfigureStubRunner
@ActiveProfiles("test-with-placeholders")
class StubRunnerOptionsBuilderSpec extends Specification {
class StubRunnerOptionsBuilderSpec {
@StubRunnerPort("fraudDetectionServer")
int fraudDetectionServerPort
@@ -41,12 +41,13 @@ class StubRunnerOptionsBuilderSpec extends Specification {
@Value('${stub.port}')
int stubPort
def 'should resolve placeholders'() {
@Test
void 'should resolve placeholders'() {
expect:
fraudDetectionServerPort > 1000
loanIssuancePort > 1000
assert fraudDetectionServerPort > 1000
assert loanIssuancePort > 1000
and:
stubPort == fraudDetectionServerPort
assert stubPort == fraudDetectionServerPort
}
@Configuration

View File

@@ -0,0 +1,103 @@
/*
* 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.spring.cloud
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.client.loadbalancer.LoadBalanced
import org.springframework.cloud.consul.ConsulAutoConfiguration
import org.springframework.cloud.contract.stubrunner.StubFinder
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties
import org.springframework.cloud.contract.stubrunner.spring.cloud.loadbalancer.StubRunnerLoadBalancerClientFactory
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory
import org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration
import org.springframework.cloud.zookeeper.ZookeeperAutoConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.test.context.ActiveProfiles
import org.springframework.web.client.RestTemplate
/**
* @author Marcin Grzejszczak
*/
@SpringBootTest(classes = Config)
@ActiveProfiles("cloudtest")
// tag::autoconfigure[]
@AutoConfigureStubRunner(
ids = ["org.springframework.cloud.contract.verifier.stubs:loanIssuance",
"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer",
"org.springframework.cloud.contract.verifier.stubs:bootService"],
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
repositoryRoot = "classpath:m2repo/repository/")
// end::autoconfigure[]
class StubRunnerSpringCloudAutoConfigurationSpec {
@Autowired
StubFinder stubFinder
@Autowired
@LoadBalanced
RestTemplate restTemplate
@Autowired
LoadBalancerClientFactory loadBalancerClientFactory;
@BeforeAll
static void setupSpec() {
System.clearProperty("stubrunner.repository.root")
System.clearProperty("stubrunner.classifier")
}
@AfterAll
static void cleanupSpec() {
setupSpec()
}
@BeforeEach
void setup() {
assert loadBalancerClientFactory.getClass().getSimpleName() == "StubRunnerLoadBalancerClientFactory"
}
// tag::test[]
@Test
void 'should make service discovery work'() {
expect: 'WireMocks are running'
assert "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
assert "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
and: 'Stubs can be reached via load service discovery'
assert restTemplate.getForObject('http://loanIssuance/name', String) == 'loanIssuance'
assert restTemplate.getForObject('http://someNameThatShouldMapFraudDetectionServer/name', String) == 'fraudDetectionServer'
}
// end::test[]
@Configuration
@EnableAutoConfiguration(exclude = [EurekaClientAutoConfiguration,
ConsulAutoConfiguration, ZookeeperAutoConfiguration])
static class Config {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate()
}
}
}

View File

@@ -0,0 +1,100 @@
/*
* 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.spring.cloud
import org.junit.AfterClass
import org.junit.BeforeClass
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.client.ServiceInstance
import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient
import org.springframework.cloud.client.loadbalancer.LoadBalanced
import org.springframework.cloud.consul.ConsulAutoConfiguration
import org.springframework.cloud.contract.stubrunner.StubFinder
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties
import org.springframework.cloud.contract.stubrunner.spring.cloud.loadbalancer.StubRunnerLoadBalancerClientFactory
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory
import org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration
import org.springframework.cloud.zookeeper.ZookeeperAutoConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.test.context.ActiveProfiles
import org.springframework.web.client.RestTemplate
/**
* @author Marcin Grzejszczak
*/
@SpringBootTest(classes = Config)
@ActiveProfiles("cloudtest")
@AutoConfigureStubRunner(
ids = ["org.springframework.cloud.contract.verifier.stubs:loanIssuance",
"org.springframework.cloud.contract.verifier.stubs:fraudDetectionServer",
"org.springframework.cloud.contract.verifier.stubs:bootService"],
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
repositoryRoot = "classpath:m2repo/repository/")
class StubRunnerSpringCloudReactiveAutoConfigurationSpec {
@Autowired
StubFinder stubFinder
@Autowired
ReactiveDiscoveryClient reactiveDiscoveryClient;
@Autowired
LoadBalancerClientFactory loadBalancerClientFactory;
RestTemplate restTemplate = new RestTemplate()
@BeforeClass
@AfterClass
static void setupProps() {
System.clearProperty("stubrunner.repository.root")
System.clearProperty("stubrunner.classifier")
}
@BeforeEach
void setup() {
assert loadBalancerClientFactory.getClass().getSimpleName() == "StubRunnerLoadBalancerClientFactory"
}
// tag::test[]
@Test
void 'should make service discovery work'() {
expect: 'WireMocks are running'
assert "${stubFinder.findStubUrl('loanIssuance').toString()}/name".toURL().text == 'loanIssuance'
assert "${stubFinder.findStubUrl('fraudDetectionServer').toString()}/name".toURL().text == 'fraudDetectionServer'
and: 'Stubs can be reached via load service discovery'
ServiceInstance loanIssuance = reactiveDiscoveryClient.getInstances('loanIssuance').blockFirst()
assert restTemplate.getForObject(loanIssuance.uri.toString() + '/name', String) == 'loanIssuance'
ServiceInstance fraudDetection = reactiveDiscoveryClient.getInstances('someNameThatShouldMapFraudDetectionServer').blockFirst()
assert restTemplate.getForObject(fraudDetection.uri.toString() + '/name', String) == 'fraudDetectionServer'
}
// end::test[]
@Configuration
@EnableAutoConfiguration(exclude = [EurekaClientAutoConfiguration,
ConsulAutoConfiguration, ZookeeperAutoConfiguration])
static class Config {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate()
}
}
}

View File

@@ -18,12 +18,12 @@ package org.springframework.cloud.contract.stubrunner.spring.cloud
import java.util.function.Function
import spock.lang.Specification
import org.assertj.core.api.BDDAssertions
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.autoconfigure.ImportAutoConfiguration
import org.springframework.boot.test.context.SpringBootContextLoader
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.cloud.contract.stubrunner.StubFinder
@@ -37,18 +37,18 @@ import org.springframework.core.env.Environment
import org.springframework.http.ResponseEntity
import org.springframework.messaging.Message
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.ContextConfiguration
/**
* @author Marcin Grzejszczak
*/
// tag::test[]
@SpringBootTest(classes = Config, properties = ["spring.application.name=bar-consumer"])
@AutoConfigureStubRunner(ids = "org.springframework.cloud.contract.verifier.stubs:producerWithMultipleConsumers",
repositoryRoot = "classpath:m2repo/repository/",
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
stubsPerConsumer = true)
repositoryRoot = "classpath:m2repo/repository/",
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
stubsPerConsumer = true)
@ActiveProfiles("streamconsumer")
class StubRunnerStubsPerConsumerSpec extends Specification {
class StubRunnerStubsPerConsumerSpec {
// end::test[]
@Autowired
@@ -59,36 +59,36 @@ class StubRunnerStubsPerConsumerSpec extends Specification {
MessageVerifier<Message<?>> messaging
TestRestTemplate template = new TestRestTemplate()
def 'should start http stub servers for bar-consumer only'() {
@Test
void 'should start http stub servers for bar-consumer only'() {
given:
URL stubUrl = stubFinder.findStubUrl('producerWithMultipleConsumers')
URL stubUrl = stubFinder.findStubUrl('producerWithMultipleConsumers')
when:
ResponseEntity entity = template.getForEntity("${stubUrl}/bar-consumer", String)
ResponseEntity entity = template.getForEntity("${stubUrl}/bar-consumer", String)
then:
entity.statusCode.value() == 200
assert entity.statusCode.value() == 200
when:
entity = template.getForEntity("${stubUrl}/foo-consumer", String)
entity = template.getForEntity("${stubUrl}/foo-consumer", String)
then:
entity.statusCode.value() == 404
assert entity.statusCode.value() == 404
}
def 'should trigger a message by label from proper consumer'() {
@Test
void 'should trigger a message by label from proper consumer'() {
when:
stubFinder.trigger('return_book_for_bar')
stubFinder.trigger('return_book_for_bar')
then:
Message<?> receivedMessage = messaging.receive('output')
Message<?> receivedMessage = messaging.receive('output')
and:
receivedMessage != null
receivedMessage.payload == '''{"bookName":"foo_for_bar"}'''.bytes
receivedMessage.headers.get('BOOK-NAME') == 'foo_for_bar'
assert receivedMessage != null
assert receivedMessage.payload == '''{"bookName":"foo_for_bar"}'''.bytes
assert receivedMessage.headers.get('BOOK-NAME') == 'foo_for_bar'
}
def 'should not trigger a message by the not matching consumer'() {
@Test
void 'should not trigger a message by the not matching consumer'() {
when:
stubFinder.trigger('return_book_for_foo')
then:
IllegalArgumentException e = thrown(IllegalArgumentException)
e.message.contains("No label with name [return_book_for_foo] was found")
BDDAssertions.thenThrownBy(() -> stubFinder.trigger('return_book_for_foo')).isInstanceOf(IllegalArgumentException).hasMessageContaining("No label with name [return_book_for_foo] was found")
}
@Configuration
@@ -97,7 +97,7 @@ class StubRunnerStubsPerConsumerSpec extends Specification {
static class Config {
@Bean
Function output() {
return { Object o ->
return { Object o ->
println(o)
return o
}

View File

@@ -18,6 +18,8 @@ package org.springframework.cloud.contract.stubrunner.spring.cloud
import java.util.function.Function
import org.assertj.core.api.BDDAssertions
import org.junit.jupiter.api.Test
import spock.lang.Specification
import org.springframework.beans.factory.annotation.Autowired
@@ -47,7 +49,7 @@ import org.springframework.test.context.ActiveProfiles
stubsMode = StubRunnerProperties.StubsMode.REMOTE,
stubsPerConsumer = true)
@ActiveProfiles("streamconsumer")
class StubRunnerStubsPerConsumerWithConsumerNameSpec extends Specification {
class StubRunnerStubsPerConsumerWithConsumerNameSpec {
// end::test[]
@Autowired
@@ -59,36 +61,37 @@ class StubRunnerStubsPerConsumerWithConsumerNameSpec extends Specification {
TestRestTemplate template = new TestRestTemplate()
def 'should start http stub servers for foo-consumer only'() {
@Test
void 'should start http stub servers for foo-consumer only'() {
given:
URL stubUrl = stubFinder.findStubUrl('producerWithMultipleConsumers')
when:
ResponseEntity entity = template.getForEntity("${stubUrl}/foo-consumer", String)
then:
entity.statusCode.value() == 200
assert entity.statusCode.value() == 200
when:
entity = template.getForEntity("${stubUrl}/bar-consumer", String)
then:
entity.statusCode.value() == 404
assert entity.statusCode.value() == 404
}
def 'should trigger a message by label from proper consumer'() {
@Test
void 'should trigger a message by label from proper consumer'() {
when:
stubFinder.trigger('return_book_for_foo')
then:
Message<?> receivedMessage = messaging.receive('output')
and:
receivedMessage != null
receivedMessage.payload == '''{"bookName":"foo_for_foo"}'''.bytes
receivedMessage.headers.get('BOOK-NAME') == 'foo_for_foo'
assert receivedMessage != null
assert receivedMessage.payload == '''{"bookName":"foo_for_foo"}'''.bytes
assert receivedMessage.headers.get('BOOK-NAME') == 'foo_for_foo'
}
def 'should not trigger a message by the not matching consumer'() {
@Test
void 'should not trigger a message by the not matching consumer'() {
when:
stubFinder.trigger('return_book_for_bar')
then:
IllegalArgumentException e = thrown(IllegalArgumentException)
e.message.contains("No label with name [return_book_for_bar] was found")
BDDAssertions.thenThrownBy(() -> stubFinder.trigger('return_book_for_bar')).isInstanceOf(IllegalArgumentException)
.hasMessageContaining("No label with name [return_book_for_bar] was found")
}
@Configuration

View File

@@ -19,12 +19,13 @@ package org.springframework.cloud.contract.stubrunner.spring.cloud.consul
import com.ecwid.consul.v1.ConsulClient
import com.ecwid.consul.v1.agent.model.NewService
import groovy.transform.CompileStatic
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.mockito.ArgumentMatcher
import spock.lang.Specification
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.test.context.SpringBootContextLoader
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cloud.client.discovery.EnableDiscoveryClient
import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties
@@ -32,7 +33,6 @@ import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRun
import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.test.context.ContextConfiguration
import static org.mockito.BDDMockito.then
import static org.mockito.Matchers.argThat
@@ -56,30 +56,35 @@ import static org.mockito.Mockito.mock
stubsMode = StubRunnerProperties.StubsMode.REMOTE ,
repositoryRoot = "classpath:m2repo/repository/" )
// end::autoconfigure[]
class StubRunnerSpringCloudConsulAutoConfigurationSpec extends Specification {
class StubRunnerSpringCloudConsulAutoConfigurationSpec {
@Autowired
ConsulClient client
void setupSpec() {
@BeforeAll
static void setupSpec() {
System.clearProperty("stubrunner.stubs.repository.root")
System.clearProperty("stubrunner.stubs.classifier")
}
void cleanupSpec() {
@AfterAll
static void cleanupSpec() {
setupSpec()
}
def 'should make service discovery work for #serviceName'() {
@Test
void 'should make service discovery work for #serviceName'() {
given:
final String expectedId = serviceName.split(':')[0]
final String expectedName = serviceName.split(':')[1]
when: 'Consul registration took place for 3 stubs'
then(client).should().agentServiceRegister(argThat(new NewServiceMatcher(expectedId, expectedName)))
then:
noExceptionThrown()
where:
serviceName << ['loanIssuance:loanIssuance', 'bootService:bootService', 'fraudDetectionServer:someNameThatShouldMapFraudDetectionServer']
def serviceName = ['loanIssuance:loanIssuance', 'bootService:bootService', 'fraudDetectionServer:someNameThatShouldMapFraudDetectionServer']
when:
serviceName.each {
and:
final String expectedId = it.split(':')[0]
final String expectedName = it.split(':')[1]
then: 'Consul registration took place for 3 stubs'
then(client).should().agentServiceRegister(argThat(new NewServiceMatcher(expectedId, expectedName)))
}
}
private static class NewServiceMatcher implements ArgumentMatcher<NewService> {

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
wiremockVersion=2.30.1
wiremockVersion=2.35.0
jsonAssertVersion=0.6.2
verifierVersion=4.0.0-SNAPSHOT
groovyVersion=2.4.17

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
wiremockVersion=2.30.1
wiremockVersion=2.35.0
jsonAssertVersion=0.6.2
verifierVersion=4.0.0-SNAPSHOT
bootVersion=3.0.0-SNAPSHOT

View File

@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
wiremockVersion=2.30.1
wiremockVersion=2.35.0
jsonAssertVersion=0.6.2
verifierVersion=4.0.0-SNAPSHOT
bootVersion=3.0.0-SNAPSHOT

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,43 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package contracts
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
label 'some_label'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('jms:output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}
]

View File

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

View File

@@ -1,35 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package contracts
import org.springframework.cloud.contract.spec.Contract
[
Contract.make {
label 'some_label'
input {
messageFrom('jms:delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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