@@ -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 )"
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -120,9 +120,6 @@ public abstract class ContractTestsBase {
|
||||
AmqpMetadata amqpMetadata = AmqpMetadata.fromMetadata(contract.metadata);
|
||||
if (isMessagingType("rabbit") && hasDeclaredOutputQueue(amqpMetadata) || isMessagingType("kafka")) {
|
||||
log.info("First will try to receive a message to setup the connection with the broker");
|
||||
if (contract.input != null && StringUtils.hasText(contract.input.messageFrom)) {
|
||||
setupConnection(contract.input.messageFrom, contract);
|
||||
}
|
||||
if (contract.outputMessage != null && StringUtils.hasText(contract.outputMessage.sentTo)){
|
||||
setupConnection(contract.outputMessage.sentTo, contract);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.contract.verifier.converter.YamlContract;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver;
|
||||
import org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender;
|
||||
import org.springframework.cloud.contract.verifier.messaging.amqp.AmqpMetadata;
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessageMetadata;
|
||||
@@ -68,8 +70,8 @@ public class MessagingAutoConfig {
|
||||
|
||||
@Bean
|
||||
public ContractVerifierMessaging<Message> contractVerifierMessaging(
|
||||
MessageVerifier<Message> exchange) {
|
||||
return new ContractVerifierCamelHelper(exchange);
|
||||
MessageVerifierSender<Message> sender, MessageVerifierReceiver<Message> receiver) {
|
||||
return new ContractVerifierCamelHelper(sender, receiver);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -185,8 +187,8 @@ public class MessagingAutoConfig {
|
||||
|
||||
class ContractVerifierCamelHelper extends ContractVerifierMessaging<Message> {
|
||||
|
||||
ContractVerifierCamelHelper(MessageVerifier<Message> exchange) {
|
||||
super(exchange);
|
||||
ContractVerifierCamelHelper(MessageVerifierSender<Message> sender, MessageVerifierReceiver<Message> receiver) {
|
||||
super(sender, receiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -197,4 +199,4 @@ class ContractVerifierCamelHelper extends ContractVerifierMessaging<Message> {
|
||||
return new ContractVerifierMessage(receive.getBody(), receive.getHeaders());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -13,7 +13,6 @@ The DSL for messaging looks a little bit different than the one that focuses on
|
||||
following sections explain the differences:
|
||||
|
||||
* <<contract-dsl-output-triggered-method>>
|
||||
* <<contract-dsl-output-triggered-message>>
|
||||
* <<contract-dsl-consumer-producer>>
|
||||
* <<contract-dsl-messaging-common>>
|
||||
|
||||
@@ -42,31 +41,6 @@ In the previous example case, the output message is sent to `output` if a method
|
||||
test that calls that method to trigger the message. On the consumer side, you can use
|
||||
`some_label` to trigger the message.
|
||||
|
||||
[[contract-dsl-output-triggered-message]]
|
||||
==== Output Triggered by a Message
|
||||
|
||||
The output message can be triggered by receiving a message, as shown in the following
|
||||
example:
|
||||
|
||||
====
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Groovy
|
||||
----
|
||||
include::{tests_path}/samples-messaging-integration/src/test/groovy/com/example/IntegrationMessagingApplicationSpec.groovy[tags=message_trigger,indent=0]
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.YAML
|
||||
----
|
||||
include::{verifier_core_path}/src/test/resources/yml/contract_message_input_message.yml[indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
In the preceding example, the output message is sent to `output` if a proper message is
|
||||
received on the `input` destination. On the message publisher's side, the engine
|
||||
generates a test that sends the input message to the defined destination. On the
|
||||
consumer side, you can either send a message to the input destination or use a label
|
||||
(`some_label` in the example) to trigger the message.
|
||||
|
||||
[[contract-dsl-consumer-producer]]
|
||||
==== Consumer/Producer
|
||||
@@ -75,16 +49,9 @@ IMPORTANT: This section is valid only for the Groovy DSL.
|
||||
|
||||
In HTTP, you have a notion of `client`/`stub and `server`/`test` notation. You can also
|
||||
use those paradigms in messaging. In addition, Spring Cloud Contract Verifier also
|
||||
provides the `consumer` and `producer` methods, as presented in the following example
|
||||
provides the `consumer` and `producer` methods
|
||||
(note that you can use either `$` or `value` methods to provide `consumer` and `producer`
|
||||
parts):
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=consumer_producer]
|
||||
----
|
||||
====
|
||||
parts).
|
||||
|
||||
[[contract-dsl-messaging-common]]
|
||||
==== Common
|
||||
@@ -97,14 +64,12 @@ in the generated test.
|
||||
[[features-messaging-integrations]]
|
||||
=== Integrations
|
||||
|
||||
You can use one of the following four integration configurations:
|
||||
You can use one of the following integration configurations:
|
||||
|
||||
* Apache Camel
|
||||
* Spring Integration
|
||||
* Spring Cloud Stream
|
||||
* Spring AMQP
|
||||
* Spring JMS (requires embedded broker)
|
||||
* Spring Kafka (requires embedded broker)
|
||||
* Spring JMS
|
||||
|
||||
Since we use Spring Boot, if you have added one of these libraries to the classpath, all
|
||||
the messaging configuration is automatically set up.
|
||||
@@ -143,8 +108,8 @@ testImplementation(group: 'org.springframework.cloud', name: 'spring-cloud-strea
|
||||
==== Manual Integration Testing
|
||||
|
||||
The main interface used by the tests is
|
||||
`org.springframework.cloud.contract.verifier.messaging.MessageVerifier`.
|
||||
It defines how to send and receive messages. You can create your own implementation to
|
||||
`org.springframework.cloud.contract.verifier.messaging.MessageVerifierSender` and `org.springframework.cloud.contract.verifier.messaging.MessageVerifierReceiver`.
|
||||
It defines how to send and receive messages. If you need both sender and receiver capabilities you can use `org.springframework.cloud.contract.verifier.messaging.MessageVerifier` interface. You can create your own implementation to
|
||||
achieve the same goal.
|
||||
|
||||
In a test, you can inject a `ContractVerifierMessageExchange` to send and receive
|
||||
@@ -176,22 +141,12 @@ Having the `input` or `outputMessage` sections in your DSL results in creation o
|
||||
on the publisher's side. By default, JUnit 4 tests are created. However, there is also a
|
||||
possibility to create JUnit 5, TestNG, or Spock tests.
|
||||
|
||||
There are three main scenarios that we should take into consideration:
|
||||
|
||||
* Scenario 1: There is no input message that produces an output message. The output
|
||||
message is triggered by a component inside the application (for example, a scheduler).
|
||||
* Scenario 2: The input message triggers an output message.
|
||||
* Scenario 3: The input message is consumed, and there is no output message.
|
||||
|
||||
IMPORTANT: The destination passed to `messageFrom` or `sentTo` can have different
|
||||
meanings for different messaging implementations. For Stream and Integration, it is
|
||||
first resolved as a `destination` of a channel. Then, if there is no such `destination`,
|
||||
it is resolved as a channel name. For Camel, that's a certain component (for example,
|
||||
`jms`).
|
||||
|
||||
[[features-messaging-scenario1]]
|
||||
==== Scenario 1: No Input Message
|
||||
|
||||
Consider the following contract:
|
||||
|
||||
=====
|
||||
@@ -225,76 +180,6 @@ include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-scenario2]]
|
||||
==== Scenario 2: Output Triggered by Input
|
||||
|
||||
Consider the following contract:
|
||||
|
||||
=====
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Groovy
|
||||
----
|
||||
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_message_dsl]
|
||||
----
|
||||
|
||||
[source,yml,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.YAML
|
||||
----
|
||||
include::{verifier_core_path}/src/test/resources/yml/contract_message_scenario2.yml[indent=0]
|
||||
----
|
||||
=====
|
||||
|
||||
For the preceding contract, the following test would be created:
|
||||
|
||||
====
|
||||
[source,java,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.JUnit
|
||||
----
|
||||
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_message_junit]
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Spock
|
||||
----
|
||||
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_message_spock]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-scenario3]]
|
||||
==== Scenario 3: No Output Message
|
||||
|
||||
Consider the following contract:
|
||||
|
||||
====
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.Groovy
|
||||
----
|
||||
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_no_output_dsl]
|
||||
----
|
||||
|
||||
[source,yml,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.YAML
|
||||
----
|
||||
include::{verifier_core_path}/src/test/resources/yml/contract_message_scenario3.yml[indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
For the preceding contract, the following test would be created:
|
||||
|
||||
====
|
||||
[source,java,indent=0,subs="verbatim,attributes",role="primary"]
|
||||
.JUnit
|
||||
----
|
||||
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_no_output_junit]
|
||||
----
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||
.Spock
|
||||
----
|
||||
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_no_output_spock]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-consumer]]
|
||||
=== Consumer Stub Generation
|
||||
|
||||
@@ -418,7 +303,7 @@ Assume that we have the following Maven repository with deployed stubs for the
|
||||
----
|
||||
====
|
||||
|
||||
Further assume that the stubs contain the following structure:
|
||||
Further, assume that the stubs contain the following structure:
|
||||
|
||||
====
|
||||
[source,bash,indent=0]
|
||||
@@ -427,107 +312,33 @@ Further assume that the stubs contain the following structure:
|
||||
│ └── MANIFEST.MF
|
||||
└── repository
|
||||
├── accurest
|
||||
│ ├── bookDeleted.groovy
|
||||
│ ├── bookReturned1.groovy
|
||||
│ └── bookReturned2.groovy
|
||||
│ └── bookReturned1.groovy
|
||||
└── mappings
|
||||
----
|
||||
====
|
||||
|
||||
Now consider the following contracts (we number them 1 and 2):
|
||||
Now consider the following contract:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=sample_dsl,indent=0]
|
||||
----
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0]
|
||||
include::{tests_path}/samples-messaging-camel/src/test/groovy/com/example/CamelMessagingApplicationSpec.groovy[tags=sample_dsl,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
These examples lend themselves to three scenarios:
|
||||
|
||||
. <<features-messaging-stub-runner-camel-scenario1>>
|
||||
. <<features-messaging-stub-runner-camel-scenario2>>
|
||||
. <<features-messaging-stub-runner-camel-scenario3>>
|
||||
|
||||
[[features-messaging-stub-runner-camel-scenario1]]
|
||||
===== Scenario 1 (No Input Message)
|
||||
|
||||
To trigger a message from the `return_book_1` label, we use the `StubTrigger` interface, as follows:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_trigger,indent=0]
|
||||
stubFinder.trigger("return_book_1")
|
||||
----
|
||||
====
|
||||
|
||||
Next, we want to listen to the output of the message sent to `{output_name}`:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The received message would then pass the following assertions:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_trigger_message,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-camel-scenario2]]
|
||||
===== Scenario 2 (Output Triggered by Input)
|
||||
|
||||
Since the route is set for you, you can send a message to the `{output_name}` destination.
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_send,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
Next, we want to listen to the output of the message sent to `{output_name}`, as follows:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_receive,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The received message would pass the following assertions:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=client_receive_message,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-camel-scenario3]]
|
||||
===== Scenario 3 (Input with No Output)
|
||||
|
||||
Since the route is set for you, you can send a message to the `{output_name}` destination, as follows:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy[tags=trigger_no_output,indent=0]
|
||||
----
|
||||
That will send out a message to the destination described in the output message of the contract.
|
||||
|
||||
:input_name: input
|
||||
:output_name: output
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-integration]]
|
||||
=== Consumer Side Messaging with Spring Integration
|
||||
@@ -581,25 +392,18 @@ Further assume the stubs contain the following structure:
|
||||
│ └── MANIFEST.MF
|
||||
└── repository
|
||||
├── accurest
|
||||
│ ├── bookDeleted.groovy
|
||||
│ ├── bookReturned1.groovy
|
||||
│ └── bookReturned2.groovy
|
||||
│ └── bookReturned1.groovy
|
||||
└── mappings
|
||||
----
|
||||
====
|
||||
|
||||
Consider the following contracts (numbered 1 and 2):
|
||||
Consider the following contract:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=sample_dsl,indent=0]
|
||||
----
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
Now consider the following Spring Integration Route:
|
||||
@@ -611,15 +415,6 @@ include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/res
|
||||
----
|
||||
====
|
||||
|
||||
These examples lend themselves to three scenarios:
|
||||
|
||||
. <<features-messaging-stub-runner-integration-scenario1>>
|
||||
. <<features-messaging-stub-runner-integration-scenario2>>
|
||||
. <<features-messaging-stub-runner-integration-scenario3>>
|
||||
|
||||
[[features-messaging-stub-runner-integration-scenario1]]
|
||||
===== Scenario 1 (No Input Message)
|
||||
|
||||
To trigger a message from the `return_book_1` label, use the `StubTrigger` interface, as
|
||||
follows:
|
||||
|
||||
@@ -630,66 +425,7 @@ include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/gro
|
||||
----
|
||||
====
|
||||
|
||||
The following listing shows how to listen to the output of the message sent to `{output_name}`:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The received message would pass the following assertions:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_trigger_message,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-integration-scenario2]]
|
||||
===== Scenario 2 (Output Triggered by Input)
|
||||
|
||||
Since the route is set for you, you can send a message to the `{output_name}`
|
||||
destination, as follows:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_send,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The following listing shows how to listen to the output of the message sent to `{output_name}`:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_receive,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The received message passes the following assertions:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=client_receive_message,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-integration-scenario3]]
|
||||
===== Scenario 3 (Input with No Output)
|
||||
|
||||
Since the route is set for you, you can send a message to the `{input_name}` destination, as follows:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-integration/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/integration/IntegrationStubRunnerSpec.groovy[tags=trigger_no_output,indent=0]
|
||||
----
|
||||
====
|
||||
That will send out a message to the destination described in the output message of the contract.
|
||||
|
||||
[[features-messaging-stub-runner-stream]]
|
||||
=== Consumer Side Messaging With Spring Cloud Stream
|
||||
@@ -773,24 +509,26 @@ Further assume the stubs contain the following structure:
|
||||
│ └── MANIFEST.MF
|
||||
└── repository
|
||||
├── accurest
|
||||
│ ├── bookDeleted.groovy
|
||||
│ ├── bookReturned1.groovy
|
||||
│ └── bookReturned2.groovy
|
||||
│ └── bookReturned1.groovy
|
||||
└── mappings
|
||||
----
|
||||
====
|
||||
|
||||
Consider the following contracts (numbered 1 and 2):
|
||||
Consider the following contract:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=sample_dsl,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[source,groovy]
|
||||
Now consider the following Spring Cloud Stream function configuration:
|
||||
|
||||
====
|
||||
[source,yaml]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0]
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=setup,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
@@ -803,15 +541,6 @@ include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/resource
|
||||
----
|
||||
====
|
||||
|
||||
These examples lend themselves to three scenarios:
|
||||
|
||||
* <<features-messaging-stub-runner-stream-scenario1>>
|
||||
* <<features-messaging-stub-runner-stream-scenario2>>
|
||||
* <<features-messaging-stub-runner-stream-scenario3>>
|
||||
|
||||
[[features-messaging-stub-runner-stream-scenario1]]
|
||||
===== Scenario 1 (No Input Message)
|
||||
|
||||
To trigger a message from the `return_book_1` label, use the `StubTrigger` interface as
|
||||
follows:
|
||||
|
||||
@@ -822,220 +551,8 @@ include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/o
|
||||
----
|
||||
====
|
||||
|
||||
The following example shows how to listen to the output of the message sent to a channel whose `destination` is
|
||||
`returnBook`:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The received message passes the following assertions:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_trigger_message,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-stream-scenario2]]
|
||||
===== Scenario 2 (Output Triggered by Input)
|
||||
|
||||
Since the route is set for you, you can send a message to the `bookStorage`
|
||||
`destination`, as follows:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_send,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The following example shows how to listen to the output of the message sent to `returnBook`:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_receive,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The received message passes the following assertions:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=client_receive_message,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-stream-scenario3]]
|
||||
===== Scenario 3 (Input with No Output)
|
||||
|
||||
Since the route is set for you, you can send a message to the `{output_name}`
|
||||
destination, as follows:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-stream/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/stream/StreamStubRunnerSpec.groovy[tags=trigger_no_output,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-amqp]]
|
||||
=== Consumer Side Messaging With Spring AMQP
|
||||
|
||||
Spring Cloud Contract Stub Runner's messaging module provides an easy way to
|
||||
integrate with Spring AMQP's Rabbit Template. For the provided artifacts, it
|
||||
automatically downloads the stubs and registers the required routes.
|
||||
|
||||
The integration tries to work standalone (that is, without interaction with a running
|
||||
RabbitMQ message broker). It expects a `RabbitTemplate` on the application context and
|
||||
uses it as a Spring Boot test named `@SpyBean`. As a result, it can use the Mockito spy
|
||||
functionality to verify and inspect messages sent by the application.
|
||||
|
||||
On the message consumer side, the stub runner considers all `@RabbitListener`-annotated
|
||||
endpoints and all `SimpleMessageListenerContainer` objects on the application context.
|
||||
|
||||
As messages are usually sent to exchanges in AMQP, the message contract contains the
|
||||
exchange name as the destination. Message listeners on the other side are bound to
|
||||
queues. Bindings connect an exchange to a queue. If message contracts are triggered, the
|
||||
Spring AMQP stub runner integration looks for bindings on the application context that
|
||||
matches this exchange. Then it collects the queues from the Spring exchanges and tries to
|
||||
find message listeners bound to these queues. The message is triggered for all matching
|
||||
message listeners.
|
||||
|
||||
If you need to work with routing keys, you can pass them by using the `amqp_receivedRoutingKey`
|
||||
messaging header.
|
||||
|
||||
[[features-messaging-stub-runner-amqp-adding]]
|
||||
==== Adding the Runner to the Project
|
||||
|
||||
You can have both Spring AMQP and Spring Cloud Contract Stub Runner on the classpath and
|
||||
set the property `stubrunner.amqp.enabled=true`. Remember to annotate your test class
|
||||
with `@AutoConfigureStubRunner`.
|
||||
|
||||
IMPORTANT: If you already have Stream and Integration on the classpath, you need
|
||||
to explicitly disable them by setting the `stubrunner.stream.enabled=false` and
|
||||
`stubrunner.integration.enabled=false` properties.
|
||||
|
||||
[[features-messaging-stub-runner-amqp-example]]
|
||||
==== Examples
|
||||
|
||||
Assume that you have the following Maven repository with a deployed stubs for the
|
||||
`spring-cloud-contract-amqp-test` application:
|
||||
|
||||
====
|
||||
[source,bash,indent=0]
|
||||
----
|
||||
└── .m2
|
||||
└── repository
|
||||
└── com
|
||||
└── example
|
||||
└── spring-cloud-contract-amqp-test
|
||||
├── 0.4.0-SNAPSHOT
|
||||
│ ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT.pom
|
||||
│ ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT-stubs.jar
|
||||
│ └── maven-metadata-local.xml
|
||||
└── maven-metadata-local.xml
|
||||
----
|
||||
====
|
||||
|
||||
Further assume that the stubs contain the following structure:
|
||||
|
||||
====
|
||||
[source,bash,indent=0]
|
||||
----
|
||||
├── META-INF
|
||||
│ └── MANIFEST.MF
|
||||
└── contracts
|
||||
└── shouldProduceValidPersonData.groovy
|
||||
----
|
||||
====
|
||||
|
||||
Then consider the following contract:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-amqp/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpStubRunnerSpec.groovy[tags=amqp_contract,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
Now consider the following Spring configuration:
|
||||
|
||||
====
|
||||
[source,yaml]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-amqp/src/test/resources/application.yml[]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-amqp-triggering]]
|
||||
===== Triggering the Message
|
||||
|
||||
To trigger a message using the contract in the preceding section, use the `StubTrigger` interface, as
|
||||
follows:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-amqp/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpStubRunnerSpec.groovy[tags=client_trigger,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The message has a destination of `contract-test.exchange`, so the Spring AMQP stub runner
|
||||
integration looks for bindings related to this exchange, as the following example shows:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpMessagingApplication.java[tags=amqp_binding,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The binding definition binds the queue called `test.queue`. As a result, the following listener
|
||||
definition is matched and invoked with the contract message:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/AmqpMessagingApplication.java[tags=amqp_listener,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
Also, the following annotated listener matches and is invoked:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-amqp/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/amqp/MessageSubscriberRabbitListener.java[tags=amqp_annotated_listener,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: The message is directly handed over to the `onMessage` method of the
|
||||
`MessageListener` associated with the matching `SimpleMessageListenerContainer`.
|
||||
|
||||
[[features-messaging-stub-runner-amqp-configuration]]
|
||||
===== Spring AMQP Test Configuration
|
||||
|
||||
To avoid Spring AMQP trying to connect to a running broker during our tests, we
|
||||
configure a mock `ConnectionFactory`.
|
||||
|
||||
To disable the mocked `ConnectionFactory`, set the following property:
|
||||
`stubrunner.amqp.mockConnection=false`, as follows:
|
||||
|
||||
====
|
||||
[source,yaml]
|
||||
----
|
||||
stubrunner:
|
||||
amqp:
|
||||
mockConnection: false
|
||||
----
|
||||
====
|
||||
That will send out a message to the destination described in the output message of the contract.
|
||||
|
||||
[[features-messaging-stub-runner-jms]]
|
||||
=== Consumer Side Messaging With Spring JMS
|
||||
@@ -1043,7 +560,7 @@ stubrunner:
|
||||
Spring Cloud Contract Stub Runner's messaging module provides an easy way to
|
||||
integrate with Spring JMS.
|
||||
|
||||
The integration assumes that you have a running instance of a JMS broker (such as an `activemq` embedded broker).
|
||||
The integration assumes that you have a running instance of a JMS broker.
|
||||
|
||||
[[features-messaging-stub-runner-jms-adding]]
|
||||
==== Adding the Runner to the Project
|
||||
@@ -1063,9 +580,7 @@ Assume that the stub structure looks as follows:
|
||||
[source,bash,indent=0]
|
||||
----
|
||||
├── stubs
|
||||
├── bookDeleted.groovy
|
||||
├── bookReturned1.groovy
|
||||
└── bookReturned2.groovy
|
||||
└── bookReturned1.groovy
|
||||
|
||||
----
|
||||
====
|
||||
@@ -1088,23 +603,15 @@ spring:
|
||||
----
|
||||
====
|
||||
|
||||
Now consider the following contracts (we number them 1 and 2):
|
||||
Now consider the following contract:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=sample_dsl,indent=0]
|
||||
----
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-jms-scenario1]]
|
||||
===== Scenario 1 (No Input Message)
|
||||
|
||||
To trigger a message from the `return_book_1` label, we use the `StubTrigger` interface, as follows:
|
||||
|
||||
====
|
||||
@@ -1114,213 +621,4 @@ include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/
|
||||
----
|
||||
====
|
||||
|
||||
Next, we want to listen to the output of the message sent to `{output_name}`:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The received message would then pass the following assertions:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_trigger_message,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-jms-scenario2]]
|
||||
===== Scenario 2 (Output Triggered by Input)
|
||||
|
||||
Since the route is set for you, you can send a message to the `{output_name}` destination.
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_send,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
Next, we want to listen to the output of the message sent to `{output_name}`, as follows:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_receive,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The received message would pass the following assertions:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_receive_message,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-jms-scenario3]]
|
||||
===== Scenario 3 (Input with No Output)
|
||||
|
||||
Since the route is set for you, you can send a message to the `{output_name}` destination, as follows:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=trigger_no_output,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-kafka]]
|
||||
=== Consumer Side Messaging With Spring Kafka
|
||||
|
||||
Spring Cloud Contract Stub Runner's messaging module provides an easy way to
|
||||
integrate with Spring Kafka.
|
||||
|
||||
The integration assumes that you have a running instance of an embedded Kafka broker (through the `spring-kafka-test` dependency).
|
||||
|
||||
[[features-messaging-stub-runner-kafka-adding]]
|
||||
==== Adding the Runner to the Project
|
||||
|
||||
You need to have Spring Kafka, Spring Kafka Test (to run the `@EmbeddedBroker`), and Spring Cloud Contract Stub Runner on the classpath. Remember to annotate your test class
|
||||
with `@AutoConfigureStubRunner`.
|
||||
|
||||
With Kafka integration, in order to poll for a single message, we need to register a consumer upon Spring context startup. That may lead to a situation that, when you are on the consumer side, Stub Runner can register an additional consumer for the same group ID and topic. That could lead to a situation that only one of the components would actually poll for the message. Since, on the consumer side, you have both the Spring Cloud Contract Stub Runner and Spring Cloud Contract Verifier classpath, we need to be able to switch off such behavior. That is done automatically through the `stubrunner.kafka.initializer.enabled` flag, which disables the Contact Verifier consumer registration. If your application is both the consumer and the producer of a Kafka message, you might need to manually toggle that property to `false` in the base class of your generated tests.
|
||||
|
||||
If you have multiple `KafkaTemplate` beans, you can provide your own bean of `Supplier<KafkaTemplate>` type that returns the `KafkaTemplate` of your chosing.
|
||||
|
||||
:input_name: input
|
||||
:output_name: output
|
||||
|
||||
[[features-messaging-stub-runner-kafka-example]]
|
||||
==== Examples
|
||||
|
||||
Assume that the stub structure looks as follows:
|
||||
|
||||
====
|
||||
[source,bash,indent=0]
|
||||
----
|
||||
├── stubs
|
||||
├── bookDeleted.groovy
|
||||
├── bookReturned1.groovy
|
||||
└── bookReturned2.groovy
|
||||
|
||||
----
|
||||
====
|
||||
|
||||
Further assume the following test configuration (notice the `spring.kafka.bootstrap-servers` pointing to the embedded broker's IP via `${spring.embedded.kafka.brokers}`):
|
||||
|
||||
====
|
||||
[source,yml,indent=0]
|
||||
----
|
||||
stubrunner:
|
||||
repository-root: stubs:classpath:/stubs/
|
||||
ids: my:stubs
|
||||
stubs-mode: remote
|
||||
spring:
|
||||
kafka:
|
||||
bootstrap-servers: ${spring.embedded.kafka.brokers}
|
||||
producer:
|
||||
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
|
||||
properties:
|
||||
"spring.json.trusted.packages": "*"
|
||||
consumer:
|
||||
value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
|
||||
properties:
|
||||
"spring.json.trusted.packages": "*"
|
||||
group-id: groupId
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: If your application uses non-integer record keys you will need to set the `spring.kafka.producer.key-serializer`
|
||||
and `spring.kafka.consumer.key-deserializer` properties accordingly because the Kafka de/serialization expects non-null
|
||||
record keys to be of integer type.
|
||||
|
||||
Now consider the following contracts (we number them 1 and 2):
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=sample_dsl,indent=0]
|
||||
----
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-kafka-scenario1]]
|
||||
===== Scenario 1 (No Input Message)
|
||||
|
||||
To trigger a message from the `return_book_1` label, we use the `StubTrigger` interface, as follows:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=client_trigger,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
Next, we want to listen to the output of the message sent to `{output_name}`:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The received message would then pass the following assertions:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=client_trigger_message,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-kafka-scenario2]]
|
||||
===== Scenario 2 (Output Triggered by Input)
|
||||
|
||||
Since the route is set for you, you can send a message to the `{output_name}` destination.
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=client_send,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
Next, we want to listen to the output of the message sent to `{output_name}`, as follows:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=client_receive,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
The received message would pass the following assertions:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=client_receive_message,indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
[[features-messaging-stub-runner-kafka-scenario3]]
|
||||
===== Scenario 3 (Input with No Output)
|
||||
|
||||
Since the route is set for you, you can send a message to the `{output_name}` destination, as follows:
|
||||
|
||||
====
|
||||
[source,groovy]
|
||||
----
|
||||
include::{tests_path}/spring-cloud-contract-stub-runner-kafka/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/kafka/KafkaStubRunnerSpec.groovy[tags=trigger_no_output,indent=0]
|
||||
----
|
||||
====
|
||||
That will send out a message to the destination described in the output message of the contract.
|
||||
|
||||
@@ -446,13 +446,13 @@ The following example shows a Camel messaging contract:
|
||||
[source,groovy,indent=0,role="primary"]
|
||||
.groovy
|
||||
----
|
||||
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_no_output_dsl]
|
||||
include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_method_dsl]
|
||||
----
|
||||
|
||||
[source,yaml,indent=0,role="secondary"]
|
||||
.yaml
|
||||
----
|
||||
include::{verifier_core_path}/src/test/resources/yml/contract_message_scenario3.yml[indent=0]
|
||||
include::{verifier_core_path}/src/test/resources/yml/contract_message_scenario1.yml[indent=0]
|
||||
----
|
||||
====
|
||||
|
||||
|
||||
94
pom.xml
94
pom.xml
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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[] -->
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
|
||||
<properties>
|
||||
<spring-cloud-contract.version>4.0.0-SNAPSHOT</spring-cloud-contract.version>
|
||||
<skipTests>true</skipTests>
|
||||
</properties>
|
||||
|
||||
<modules>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.spec.internal;
|
||||
|
||||
class ClientInput extends Input {
|
||||
|
||||
ClientInput(Input request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,14 +17,8 @@
|
||||
package org.springframework.cloud.contract.spec.internal;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import groovy.lang.Closure;
|
||||
import groovy.lang.DelegatesTo;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
/**
|
||||
* Represents an input for messaging. The input can be a message or some action inside the
|
||||
* application.
|
||||
@@ -35,49 +29,12 @@ import org.apache.commons.logging.LogFactory;
|
||||
*/
|
||||
public class Input extends Common implements RegexCreatingProperty<ClientDslProperty> {
|
||||
|
||||
private static final Log log = LogFactory.getLog(Input.class);
|
||||
|
||||
private ClientPatternValueDslProperty property = new ClientPatternValueDslProperty();
|
||||
|
||||
private DslProperty<String> messageFrom;
|
||||
|
||||
private ExecutionProperty triggeredBy;
|
||||
|
||||
private Headers messageHeaders = new Headers();
|
||||
|
||||
private BodyType messageBody;
|
||||
|
||||
private ExecutionProperty assertThat;
|
||||
|
||||
private BodyMatchers bodyMatchers;
|
||||
|
||||
public Input() {
|
||||
}
|
||||
|
||||
public Input(Input input) {
|
||||
this.messageFrom = input.getMessageFrom();
|
||||
this.messageHeaders = input.getMessageHeaders();
|
||||
this.messageBody = input.getMessageBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of a destination from which message would come to trigger action in the
|
||||
* system.
|
||||
* @param messageFrom message destination
|
||||
*/
|
||||
public void messageFrom(String messageFrom) {
|
||||
this.messageFrom = new DslProperty<>(messageFrom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Name of a destination from which message would come to trigger action in the
|
||||
* system.
|
||||
* @param messageFrom message destination
|
||||
*/
|
||||
public void messageFrom(DslProperty messageFrom) {
|
||||
this.messageFrom = messageFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that needs to be executed to trigger action in the system.
|
||||
* @param triggeredBy method name that triggers the message
|
||||
@@ -86,11 +43,6 @@ public class Input extends Common implements RegexCreatingProperty<ClientDslProp
|
||||
this.triggeredBy = new ExecutionProperty(triggeredBy);
|
||||
}
|
||||
|
||||
public BodyType messageBody(Object bodyAsValue) {
|
||||
this.messageBody = new BodyType(bodyAsValue);
|
||||
return this.messageBody;
|
||||
}
|
||||
|
||||
public DslProperty value(ClientDslProperty client) {
|
||||
Object dynamicValue = client.getClientValue();
|
||||
Object concreteValue = client.getServerValue();
|
||||
@@ -129,14 +81,6 @@ public class Input extends Common implements RegexCreatingProperty<ClientDslProp
|
||||
this.property = property;
|
||||
}
|
||||
|
||||
public DslProperty<String> getMessageFrom() {
|
||||
return messageFrom;
|
||||
}
|
||||
|
||||
public void setMessageFrom(DslProperty<String> messageFrom) {
|
||||
this.messageFrom = messageFrom;
|
||||
}
|
||||
|
||||
public ExecutionProperty getTriggeredBy() {
|
||||
return triggeredBy;
|
||||
}
|
||||
@@ -145,22 +89,6 @@ public class Input extends Common implements RegexCreatingProperty<ClientDslProp
|
||||
this.triggeredBy = triggeredBy;
|
||||
}
|
||||
|
||||
public Headers getMessageHeaders() {
|
||||
return messageHeaders;
|
||||
}
|
||||
|
||||
public void setMessageHeaders(Headers messageHeaders) {
|
||||
this.messageHeaders = messageHeaders;
|
||||
}
|
||||
|
||||
public BodyType getMessageBody() {
|
||||
return messageBody;
|
||||
}
|
||||
|
||||
public void setMessageBody(BodyType messageBody) {
|
||||
this.messageBody = messageBody;
|
||||
}
|
||||
|
||||
public ExecutionProperty getAssertThat() {
|
||||
return assertThat;
|
||||
}
|
||||
@@ -169,14 +97,6 @@ public class Input extends Common implements RegexCreatingProperty<ClientDslProp
|
||||
this.assertThat = assertThat;
|
||||
}
|
||||
|
||||
public BodyMatchers getBodyMatchers() {
|
||||
return bodyMatchers;
|
||||
}
|
||||
|
||||
public void setBodyMatchers(BodyMatchers bodyMatchers) {
|
||||
this.bodyMatchers = bodyMatchers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientDslProperty anyAlphaUnicode() {
|
||||
return property.anyAlphaUnicode();
|
||||
@@ -282,44 +202,6 @@ public class Input extends Common implements RegexCreatingProperty<ClientDslProp
|
||||
return property.anyOf(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* The message headers part of the contract.
|
||||
* @param consumer function to manipulate the message headers
|
||||
*/
|
||||
public void messageHeaders(Consumer<Headers> consumer) {
|
||||
this.messageHeaders = new Headers();
|
||||
consumer.accept(this.messageHeaders);
|
||||
}
|
||||
|
||||
/**
|
||||
* The stub matchers part of the contract.
|
||||
* @param consumer function to manipulate the message headers
|
||||
*/
|
||||
public void bodyMatchers(Consumer<BodyMatchers> consumer) {
|
||||
this.bodyMatchers = new BodyMatchers();
|
||||
consumer.accept(this.bodyMatchers);
|
||||
}
|
||||
|
||||
/**
|
||||
* The message headers part of the contract.
|
||||
* @param consumer function to manipulate the message headers
|
||||
*/
|
||||
public void messageHeaders(@DelegatesTo(Headers.class) Closure consumer) {
|
||||
this.messageHeaders = new Headers();
|
||||
consumer.setDelegate(this.messageHeaders);
|
||||
consumer.call();
|
||||
}
|
||||
|
||||
/**
|
||||
* The stub matchers part of the contract.
|
||||
* @param consumer function to manipulate the message headers
|
||||
*/
|
||||
public void bodyMatchers(@DelegatesTo(BodyMatchers.class) Closure consumer) {
|
||||
this.bodyMatchers = new BodyMatchers();
|
||||
consumer.setDelegate(this.bodyMatchers);
|
||||
consumer.call();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
@@ -329,34 +211,18 @@ public class Input extends Common implements RegexCreatingProperty<ClientDslProp
|
||||
return false;
|
||||
}
|
||||
Input input = (Input) o;
|
||||
return Objects.equals(messageFrom, input.messageFrom) && Objects.equals(triggeredBy, input.triggeredBy)
|
||||
&& Objects.equals(messageHeaders, input.messageHeaders)
|
||||
&& Objects.equals(messageBody, input.messageBody) && Objects.equals(assertThat, input.assertThat)
|
||||
&& Objects.equals(bodyMatchers, input.bodyMatchers);
|
||||
return Objects.equals(triggeredBy, input.triggeredBy) && Objects.equals(assertThat, input.assertThat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(messageFrom, triggeredBy, messageHeaders, messageBody, assertThat, bodyMatchers);
|
||||
return Objects.hash(triggeredBy, assertThat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Input{\n\tmessageFrom=" + messageFrom + ", \n\ttriggeredBy=" + triggeredBy + ", \n\tmessageHeaders="
|
||||
+ messageHeaders + ", \n\tmessageBody=" + messageBody + ", \n\tassertThat=" + assertThat
|
||||
+ ", \n\tbodyMatchers=" + bodyMatchers + "} \n\t" + super.toString();
|
||||
}
|
||||
|
||||
public static class BodyType extends DslProperty {
|
||||
|
||||
public BodyType(Object clientValue, Object serverValue) {
|
||||
super(clientValue, serverValue);
|
||||
}
|
||||
|
||||
public BodyType(Object singleValue) {
|
||||
super(singleValue);
|
||||
}
|
||||
|
||||
return "Input{\n\t" + ", \n\ttriggeredBy=" + triggeredBy + ", \n\tassertThat=" + assertThat + "} \n\t"
|
||||
+ super.toString();
|
||||
}
|
||||
|
||||
private class ClientPatternValueDslProperty extends PatternValueDslProperty<ClientDslProperty> {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.spec.internal;
|
||||
|
||||
class ServerInput extends Input {
|
||||
|
||||
ServerInput(Input request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,53 +31,16 @@ class InputDsl : CommonDsl() {
|
||||
|
||||
private val delegate = Input()
|
||||
|
||||
/**
|
||||
* Name of a destination from which message would come to trigger action in the
|
||||
* system.
|
||||
*/
|
||||
var messageFrom: DslProperty<String>? = null
|
||||
|
||||
/**
|
||||
* Function that needs to be executed to trigger action in the system.
|
||||
*/
|
||||
var triggeredBy: String? = null
|
||||
|
||||
/**
|
||||
* The message headers part of the contract.
|
||||
*/
|
||||
var headers: Headers? = null
|
||||
|
||||
/**
|
||||
* The contents of the incoming message.
|
||||
*/
|
||||
var messageBody: Input.BodyType? = null
|
||||
|
||||
/**
|
||||
* Function that needs to be executed after the message has been received/processed by the system.
|
||||
*/
|
||||
var assertThat: String? = null
|
||||
|
||||
/**
|
||||
* The body matchers part of the contract.
|
||||
*/
|
||||
var bodyMatchers: BodyMatchers? = null
|
||||
|
||||
fun messageFrom(messageFrom: String) = messageFrom.toDslProperty()
|
||||
|
||||
fun headers(headers: HeadersDsl.() -> Unit) {
|
||||
this.headers = HeadersDsl().apply(headers).get()
|
||||
}
|
||||
|
||||
fun messageBody(vararg pairs: Pair<String, Any>) = Input.BodyType(pairs.toMap())
|
||||
|
||||
fun messageBody(pair: Pair<String, Any>) = Input.BodyType(mapOf(pair))
|
||||
|
||||
fun messageBody(value: String) = Input.BodyType(value)
|
||||
|
||||
fun bodyMatchers(configurer: BodyMatchersDsl.() -> Unit) {
|
||||
this.bodyMatchers = BodyMatchersDsl().apply(configurer).get()
|
||||
}
|
||||
|
||||
/* HELPER VARIABLES */
|
||||
|
||||
val anyAlphaUnicode: ClientDslProperty
|
||||
@@ -170,12 +133,8 @@ class InputDsl : CommonDsl() {
|
||||
|
||||
internal fun get(): Input {
|
||||
val input = Input()
|
||||
messageFrom?.also { input.messageFrom = messageFrom }
|
||||
triggeredBy?.also { input.triggeredBy(triggeredBy) }
|
||||
headers?.also { input.messageHeaders = headers }
|
||||
messageBody?.also { input.messageBody = messageBody }
|
||||
assertThat?.also { input.assertThat(assertThat) }
|
||||
bodyMatchers?.also { input.bodyMatchers = bodyMatchers }
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,90 +142,6 @@ class ContractTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should work for messaging`() {
|
||||
val contract = contract {
|
||||
input {
|
||||
messageFrom = messageFrom("input")
|
||||
messageBody = messageBody("foo" to "bar")
|
||||
headers {
|
||||
header("foo", "bar")
|
||||
header("X-Custom-Header", value(consumer(regex("^.*2134.*\$")), producer("121345")))
|
||||
}
|
||||
}
|
||||
outputMessage {
|
||||
sentTo = sentTo("output")
|
||||
body = body("foo" to "bar", "foo2" to "bar2")
|
||||
headers {
|
||||
header("foo2", "bar")
|
||||
header("X-Custom-Header", value(consumer("121345"), producer(regex("^.*2134.*\$"))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertDoesNotThrow {
|
||||
Contract.assertContract(contract)
|
||||
}.also {
|
||||
val input = contract.input
|
||||
assertThat(input.messageFrom.clientValue).isEqualTo("input")
|
||||
assertThat(input.messageFrom.serverValue).isEqualTo("input")
|
||||
assertThat(input.messageBody.clientValue).isEqualTo(mapOf("foo" to "bar"))
|
||||
assertThat(input.messageBody.serverValue).isEqualTo(mapOf("foo" to "bar"))
|
||||
val headers = input.messageHeaders.entries
|
||||
assertThat(headers).hasSize(2)
|
||||
assertThat(headers.elementAt(0).name).isEqualTo("foo")
|
||||
assertThat(headers.elementAt(0).clientValue).isEqualTo("bar")
|
||||
assertThat(headers.elementAt(0).serverValue).isEqualTo("bar")
|
||||
assertThat(headers.elementAt(1).name).isEqualTo("X-Custom-Header")
|
||||
assertThat(headers.elementAt(1).clientValue).isInstanceOf(RegexProperty::class.java)
|
||||
assertThat((headers.elementAt(1).clientValue as RegexProperty).pattern()).isEqualTo("^.*2134.*\$")
|
||||
assertThat(headers.elementAt(1).serverValue).isEqualTo("121345")
|
||||
}.also {
|
||||
val output = contract.outputMessage
|
||||
assertThat(output.sentTo.clientValue).isEqualTo("output")
|
||||
assertThat(output.sentTo.serverValue).isEqualTo("output")
|
||||
assertThat(output.body.clientValue).isEqualTo(mapOf("foo" to "bar",
|
||||
"foo2" to "bar2"
|
||||
))
|
||||
assertThat(output.body.serverValue).isEqualTo(mapOf("foo" to "bar",
|
||||
"foo2" to "bar2"
|
||||
))
|
||||
val headers = output.headers.entries
|
||||
assertThat(headers).hasSize(2)
|
||||
assertThat(headers.elementAt(0).name).isEqualTo("foo2")
|
||||
assertThat(headers.elementAt(0).clientValue).isEqualTo("bar")
|
||||
assertThat(headers.elementAt(0).serverValue).isEqualTo("bar")
|
||||
assertThat(headers.elementAt(1).name).isEqualTo("X-Custom-Header")
|
||||
assertThat(headers.elementAt(1).clientValue).isEqualTo("121345")
|
||||
assertThat(headers.elementAt(1).serverValue).isInstanceOf(RegexProperty::class.java)
|
||||
assertThat((headers.elementAt(1).serverValue as RegexProperty).pattern()).isEqualTo("^.*2134.*\$")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should work for messaging with pattern properties`() {
|
||||
val contract = contract {
|
||||
input {
|
||||
messageFrom("input")
|
||||
messageBody("foo" to anyNonBlankString)
|
||||
headers {
|
||||
header("foo", anyNumber)
|
||||
}
|
||||
}
|
||||
outputMessage {
|
||||
sentTo("output")
|
||||
body("foo2" to anyNonEmptyString)
|
||||
headers {
|
||||
header("foo2", anyIpAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertDoesNotThrow {
|
||||
Contract.assertContract(contract)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should set a description`() {
|
||||
val contract =
|
||||
@@ -888,4 +804,4 @@ then:
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,65 +101,6 @@ class ContractSpec extends Specification {
|
||||
ex.message.contains("Status is missing for HTTP contract")
|
||||
}
|
||||
|
||||
def 'should work for messaging'() {
|
||||
when:
|
||||
Contract.make {
|
||||
input {
|
||||
messageFrom('input')
|
||||
messageBody([
|
||||
foo: 'bar'
|
||||
])
|
||||
messageHeaders {
|
||||
header([
|
||||
foo: 'bar'
|
||||
])
|
||||
}
|
||||
}
|
||||
outputMessage {
|
||||
sentTo('output')
|
||||
body([
|
||||
foo2: 'bar'
|
||||
])
|
||||
headers {
|
||||
header([
|
||||
foo2: 'bar'
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
then:
|
||||
noExceptionThrown()
|
||||
}
|
||||
|
||||
def 'should work for messaging with pattern properties'() {
|
||||
when:
|
||||
Contract.make {
|
||||
input {
|
||||
messageFrom('input')
|
||||
messageBody([
|
||||
foo: anyNonBlankString()
|
||||
])
|
||||
messageHeaders {
|
||||
header([
|
||||
foo: anyNumber()
|
||||
])
|
||||
}
|
||||
}
|
||||
outputMessage {
|
||||
sentTo('output')
|
||||
body([
|
||||
foo2: anyNonEmptyString()
|
||||
])
|
||||
headers {
|
||||
header([
|
||||
foo2: anyIpAddress()
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
then:
|
||||
noExceptionThrown()
|
||||
}
|
||||
|
||||
def 'should set a description'() {
|
||||
given:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging;
|
||||
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration;
|
||||
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Supports
|
||||
* {@link org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner} by
|
||||
* loading in AutoConfigurations related to Stream and Integration only if the relevant
|
||||
* jars are in classpath.
|
||||
*
|
||||
* @author Biju Kunjummen
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class StubRunnerStreamsIntegrationAutoConfiguration {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(TestChannelBinderConfiguration.class)
|
||||
@ImportAutoConfiguration(classes = TestChannelBinderConfiguration.class)
|
||||
static class StreamsRelatedAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(IntegrationAutoConfiguration.class)
|
||||
@ImportAutoConfiguration(classes = { IntegrationAutoConfiguration.class })
|
||||
static class IntegrationRelatedAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.camel;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.camel.Exchange;
|
||||
import org.apache.camel.Processor;
|
||||
import org.apache.camel.RoutesBuilder;
|
||||
import org.apache.camel.builder.RouteBuilder;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.stubrunner.BatchStubRunner;
|
||||
import org.springframework.cloud.contract.stubrunner.StubConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Camel configuration that iterates over the downloaded Groovy DSLs and registers a route
|
||||
* for each DSL.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(RoutesBuilder.class)
|
||||
@ConditionalOnProperty(name = "stubrunner.camel.enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class StubRunnerCamelConfiguration {
|
||||
|
||||
static final String STUBRUNNER_DESTINATION_URL_HEADER_NAME = "STUBRUNNER_DESTINATION_URL";
|
||||
|
||||
@Bean
|
||||
public RoutesBuilder myRouter(final BatchStubRunner batchStubRunner) {
|
||||
return new RouteBuilder() {
|
||||
@Override
|
||||
public void configure() throws Exception {
|
||||
Map<StubConfiguration, Collection<Contract>> contracts = batchStubRunner.getContracts();
|
||||
for (Map.Entry<StubConfiguration, Collection<Contract>> entry : contracts.entrySet()) {
|
||||
Collection<Contract> value = entry.getValue();
|
||||
MultiValueMap<String, Contract> map = new LinkedMultiValueMap<>();
|
||||
for (Contract dsl : value) {
|
||||
if (dsl == null) {
|
||||
continue;
|
||||
}
|
||||
if (dsl.getInput() != null && dsl.getInput().getMessageFrom() != null
|
||||
&& StringUtils.hasText(dsl.getInput().getMessageFrom().getClientValue())) {
|
||||
String from = dsl.getInput().getMessageFrom().getClientValue();
|
||||
map.add(from, dsl);
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, List<Contract>> entries : map.entrySet()) {
|
||||
from(entries.getKey()).filter(new StubRunnerCamelPredicate(entries.getValue()))
|
||||
.process(new StubRunnerCamelProcessor()).dynamicRouter(
|
||||
header(StubRunnerCamelConfiguration.STUBRUNNER_DESTINATION_URL_HEADER_NAME));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
DummyProcessor dummyStubRunnerProcessor() {
|
||||
return new DummyProcessor();
|
||||
}
|
||||
|
||||
private static class DummyProcessor implements Processor {
|
||||
|
||||
private static final Log log = LogFactory.getLog(DummyProcessor.class);
|
||||
|
||||
@Override
|
||||
public void process(Exchange exchange) {
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Got exchange [" + exchange + "]");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.camel;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerCamelPayload {
|
||||
|
||||
final Object payload;
|
||||
|
||||
final Contract contract;
|
||||
|
||||
StubRunnerCamelPayload(Contract contract) {
|
||||
this.contract = contract;
|
||||
this.payload = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.camel;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.toomuchcoding.jsonassert.JsonAssertion;
|
||||
import org.apache.camel.Exchange;
|
||||
import org.apache.camel.Message;
|
||||
import org.apache.camel.Predicate;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.BodyMatcher;
|
||||
import org.springframework.cloud.contract.spec.internal.BodyMatchers;
|
||||
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.Header;
|
||||
import org.springframework.cloud.contract.spec.internal.RegexProperty;
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
|
||||
import org.springframework.cloud.contract.verifier.util.ContentType;
|
||||
import org.springframework.cloud.contract.verifier.util.ContentUtils;
|
||||
import org.springframework.cloud.contract.verifier.util.JsonPaths;
|
||||
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
|
||||
import org.springframework.cloud.contract.verifier.util.MapConverter;
|
||||
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
|
||||
|
||||
/**
|
||||
* Passes through a message that matches the one defined in the DSL.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerCamelPredicate implements Predicate {
|
||||
|
||||
private static final Log log = LogFactory.getLog(StubRunnerCamelPredicate.class);
|
||||
|
||||
private final List<Contract> groovyDsls;
|
||||
|
||||
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
|
||||
|
||||
StubRunnerCamelPredicate(List<Contract> groovyDsls) {
|
||||
this.groovyDsls = groovyDsls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(Exchange exchange) {
|
||||
Contract contract = getContract(exchange.getMessage());
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("For exchange [" + exchange + "] found contract [" + contract + "]");
|
||||
}
|
||||
if (contract == null) {
|
||||
return false;
|
||||
}
|
||||
exchange.getIn().setBody(new StubRunnerCamelPayload(contract));
|
||||
return true;
|
||||
}
|
||||
|
||||
private Contract getContract(Message message) {
|
||||
for (Contract groovyDsl : this.groovyDsls) {
|
||||
Contract contract = matchContract(message, groovyDsl);
|
||||
if (contract != null) {
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Contract matchContract(Message message, Contract groovyDsl) {
|
||||
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
|
||||
if (!unmatchedHeaders.isEmpty()) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Object inputMessage = message.getBody();
|
||||
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
|
||||
if (dslBody instanceof FromFileProperty) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Will compare file content");
|
||||
}
|
||||
FromFileProperty property = (FromFileProperty) dslBody;
|
||||
if (property.isString()) {
|
||||
// continue processing as if body was pure string
|
||||
dslBody = property.asString();
|
||||
}
|
||||
else if (!(inputMessage instanceof byte[])) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract provided byte comparison, but the input message is of type ["
|
||||
+ inputMessage.getClass() + "]. Can't compare the two.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
|
||||
if (log.isDebugEnabled() && !matches) {
|
||||
log.debug("Contract provided byte comparison, but the byte arrays don't match");
|
||||
}
|
||||
return matches ? groovyDsl : null;
|
||||
}
|
||||
}
|
||||
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
|
||||
return groovyDsl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
|
||||
boolean matches;
|
||||
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
|
||||
if (type == ContentType.JSON) {
|
||||
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
|
||||
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
|
||||
}
|
||||
else if (dslBody instanceof RegexProperty && inputMessage instanceof String) {
|
||||
Pattern pattern = ((RegexProperty) dslBody).getPattern();
|
||||
matches = pattern.matcher((String) inputMessage).matches();
|
||||
bodyUnmatchedLog(dslBody, matches, pattern);
|
||||
}
|
||||
else {
|
||||
matches = dslBody.equals(inputMessage);
|
||||
bodyUnmatchedLog(dslBody, matches, inputMessage);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
|
||||
if (log.isDebugEnabled() && !matches) {
|
||||
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
|
||||
+ "]");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
|
||||
Object dslBody) {
|
||||
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
|
||||
JsonPaths jsonPaths = JsonToJsonPathsConverter
|
||||
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
|
||||
DocumentContext parsedJson;
|
||||
try {
|
||||
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("Cannot serialize to JSON", e);
|
||||
}
|
||||
List<String> unmatchedJsonPath = new ArrayList<>();
|
||||
boolean matches = true;
|
||||
for (MethodBufferingJsonVerifiable path : jsonPaths) {
|
||||
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
|
||||
}
|
||||
if (matchers != null && matchers.hasMatchers()) {
|
||||
for (BodyMatcher matcher : matchers.matchers()) {
|
||||
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
|
||||
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
|
||||
}
|
||||
}
|
||||
if (!unmatchedJsonPath.isEmpty()) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
|
||||
try {
|
||||
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e) {
|
||||
unmatchedJsonPath.add(e.getLocalizedMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> headersMatch(Message message, Contract groovyDsl) {
|
||||
List<String> unmatchedHeaders = new ArrayList<>();
|
||||
Map<String, Object> headers = message.getHeaders();
|
||||
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
|
||||
String name = it.getName();
|
||||
Object value = it.getClientValue();
|
||||
Object valueInHeader = headers.get(name);
|
||||
boolean matches;
|
||||
if (value instanceof RegexProperty) {
|
||||
Pattern pattern = ((RegexProperty) value).getPattern();
|
||||
matches = pattern.matcher(valueInHeader.toString()).matches();
|
||||
}
|
||||
else {
|
||||
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
|
||||
}
|
||||
if (!matches) {
|
||||
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
|
||||
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
|
||||
}
|
||||
}
|
||||
return unmatchedHeaders;
|
||||
}
|
||||
|
||||
private String unmatchedText(Object expectedValue) {
|
||||
return expectedValue instanceof RegexProperty
|
||||
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
|
||||
: "be equal to [" + expectedValue + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.camel;
|
||||
|
||||
import org.apache.camel.Exchange;
|
||||
import org.apache.camel.Message;
|
||||
import org.apache.camel.Processor;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.Header;
|
||||
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
|
||||
|
||||
/**
|
||||
* Sends forward a message defined in the DSL. Also removes headers from the input message
|
||||
* and provides the headers from the DSL.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerCamelProcessor implements Processor {
|
||||
|
||||
private static final Log log = LogFactory.getLog(StubRunnerCamelProcessor.class);
|
||||
|
||||
private static final String DUMMY_BEAN_URL = "bean:dummyStubRunnerProcessor";
|
||||
|
||||
@Override
|
||||
public void process(Exchange exchange) {
|
||||
Message input = exchange.getIn();
|
||||
StubRunnerCamelPayload body = input.getBody(StubRunnerCamelPayload.class);
|
||||
Contract groovyDsl = body.contract;
|
||||
setStubRunnerDestinationHeader(exchange, body);
|
||||
if (groovyDsl.getInput().getMessageHeaders() != null) {
|
||||
for (Header entry : groovyDsl.getInput().getMessageHeaders().getEntries()) {
|
||||
input.removeHeader(entry.getName());
|
||||
}
|
||||
}
|
||||
if (groovyDsl.getOutputMessage() == null) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("No output message provided, will not modify the body");
|
||||
}
|
||||
return;
|
||||
}
|
||||
input.setBody(outputBody(groovyDsl));
|
||||
if (groovyDsl.getOutputMessage().getHeaders() != null) {
|
||||
for (Header entry : groovyDsl.getOutputMessage().getHeaders().getEntries()) {
|
||||
input.setHeader(entry.getName(), entry.getClientValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Object outputBody(Contract groovyDsl) {
|
||||
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
|
||||
if (outputBody instanceof FromFileProperty) {
|
||||
FromFileProperty property = (FromFileProperty) outputBody;
|
||||
return property.asBytes();
|
||||
}
|
||||
return BodyExtractor.extractStubValueFrom(outputBody);
|
||||
}
|
||||
|
||||
private void setStubRunnerDestinationHeader(Exchange exchange, StubRunnerCamelPayload body) {
|
||||
boolean outputPart = body.contract.getOutputMessage() != null;
|
||||
String url = DUMMY_BEAN_URL;
|
||||
if (outputPart && body.contract.getOutputMessage().getSentTo() != null) {
|
||||
url = body.contract.getOutputMessage().getSentTo().getClientValue();
|
||||
}
|
||||
exchange.getIn().setHeader(StubRunnerCamelConfiguration.STUBRUNNER_DESTINATION_URL_HEADER_NAME, url);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Set stub runner destination header to [" + url + "]");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.integration;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.stubrunner.BatchStubRunner;
|
||||
import org.springframework.cloud.contract.stubrunner.StubConfiguration;
|
||||
import org.springframework.context.Lifecycle;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.integration.dsl.FilterEndpointSpec;
|
||||
import org.springframework.integration.dsl.IntegrationFlowBuilder;
|
||||
import org.springframework.integration.dsl.IntegrationFlows;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Spring Integration configuration that iterates over the downloaded Groovy DSLs and
|
||||
* registers a flow for each DSL.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(IntegrationFlowBuilder.class)
|
||||
@ConditionalOnProperty(name = "stubrunner.integration.enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class StubRunnerIntegrationConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "stubFlowRegistrar")
|
||||
public FlowRegistrar stubFlowRegistrar(AutowireCapableBeanFactory beanFactory, BatchStubRunner batchStubRunner) {
|
||||
Map<StubConfiguration, Collection<Contract>> contracts = batchStubRunner.getContracts();
|
||||
IntegrationFlowBuilder dummyBuilder = IntegrationFlows.from(DummyMessageHandler.CHANNEL_NAME)
|
||||
.handle(new DummyMessageHandler(), "handle");
|
||||
beanFactory.initializeBean(dummyBuilder.get(), DummyMessageHandler.CHANNEL_NAME + ".flow");
|
||||
for (Entry<StubConfiguration, Collection<Contract>> entry : contracts.entrySet()) {
|
||||
StubConfiguration key = entry.getKey();
|
||||
Collection<Contract> value = entry.getValue();
|
||||
String name = key.getGroupId() + "_" + key.getArtifactId();
|
||||
MultiValueMap<String, Contract> map = new LinkedMultiValueMap<>();
|
||||
for (Contract dsl : value) {
|
||||
if (dsl == null) {
|
||||
continue;
|
||||
}
|
||||
if (dsl.getInput() != null && dsl.getInput().getMessageFrom() != null
|
||||
&& StringUtils.hasText(dsl.getInput().getMessageFrom().getClientValue())) {
|
||||
String from = dsl.getInput().getMessageFrom().getClientValue();
|
||||
map.add(from, dsl);
|
||||
}
|
||||
}
|
||||
for (Entry<String, List<Contract>> entries : map.entrySet()) {
|
||||
final String flowName = name + "_" + entries.getKey() + "_" + entries.getValue().hashCode();
|
||||
IntegrationFlowBuilder builder = IntegrationFlows.from(entries.getKey())
|
||||
.filter(new StubRunnerIntegrationMessageSelector(entries.getValue()),
|
||||
new Consumer<FilterEndpointSpec>() {
|
||||
@Override
|
||||
public void accept(FilterEndpointSpec e) {
|
||||
e.id(flowName + ".filter");
|
||||
}
|
||||
})
|
||||
.transform(new StubRunnerIntegrationTransformer(entries.getValue()))
|
||||
.route(new StubRunnerIntegrationRouter(entries.getValue(), beanFactory));
|
||||
beanFactory.initializeBean(builder.get(), flowName);
|
||||
beanFactory.getBean(flowName + ".filter", Lifecycle.class).start();
|
||||
}
|
||||
|
||||
}
|
||||
return new FlowRegistrar();
|
||||
}
|
||||
|
||||
static class DummyMessageHandler {
|
||||
|
||||
static String CHANNEL_NAME = "stub_runner_dummy_channel";
|
||||
|
||||
public void handle(Message<?> message) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class FlowRegistrar {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.integration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.toomuchcoding.jsonassert.JsonAssertion;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.BodyMatcher;
|
||||
import org.springframework.cloud.contract.spec.internal.BodyMatchers;
|
||||
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.Header;
|
||||
import org.springframework.cloud.contract.spec.internal.RegexProperty;
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
|
||||
import org.springframework.cloud.contract.verifier.util.ContentType;
|
||||
import org.springframework.cloud.contract.verifier.util.ContentUtils;
|
||||
import org.springframework.cloud.contract.verifier.util.JsonPaths;
|
||||
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
|
||||
import org.springframework.cloud.contract.verifier.util.MapConverter;
|
||||
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
|
||||
import org.springframework.integration.core.MessageSelector;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
* Passes through a message that matches the one defined in the DSL.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Tim Ysewyn
|
||||
*/
|
||||
class StubRunnerIntegrationMessageSelector implements MessageSelector {
|
||||
|
||||
private static final Map<Message, Contract> CACHE = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private static final Log log = LogFactory.getLog(StubRunnerIntegrationMessageSelector.class);
|
||||
|
||||
private final List<Contract> groovyDsls;
|
||||
|
||||
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
|
||||
|
||||
StubRunnerIntegrationMessageSelector(Contract groovyDsl) {
|
||||
this(Collections.singletonList(groovyDsl));
|
||||
}
|
||||
|
||||
StubRunnerIntegrationMessageSelector(List<Contract> groovyDsls) {
|
||||
this.groovyDsls = groovyDsls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(Message<?> message) {
|
||||
return matchingContract(message) != null;
|
||||
}
|
||||
|
||||
Contract matchingContract(Message<?> message) {
|
||||
if (CACHE.containsKey(message)) {
|
||||
return CACHE.get(message);
|
||||
}
|
||||
Contract contract = getContract(message);
|
||||
if (contract != null) {
|
||||
CACHE.put(message, contract);
|
||||
}
|
||||
return contract;
|
||||
}
|
||||
|
||||
void updateCache(Message<?> message, Contract contract) {
|
||||
CACHE.put(message, contract);
|
||||
}
|
||||
|
||||
private Contract getContract(Message<?> message) {
|
||||
for (Contract groovyDsl : this.groovyDsls) {
|
||||
Contract contract = matchContract(message, groovyDsl);
|
||||
if (contract != null) {
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Contract matchContract(Message<?> message, Contract groovyDsl) {
|
||||
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
|
||||
if (!unmatchedHeaders.isEmpty()) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Object inputMessage = message.getPayload();
|
||||
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
|
||||
if (dslBody instanceof FromFileProperty) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Will compare file content");
|
||||
}
|
||||
FromFileProperty property = (FromFileProperty) dslBody;
|
||||
if (property.isString()) {
|
||||
// continue processing as if body was pure string
|
||||
dslBody = property.asString();
|
||||
}
|
||||
else if (!(inputMessage instanceof byte[])) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract provided byte comparison, but the input message is of type ["
|
||||
+ inputMessage.getClass() + "]. Can't compare the two.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
|
||||
if (log.isDebugEnabled() && !matches) {
|
||||
log.debug("Contract provided byte comparison, but the byte arrays don't match");
|
||||
}
|
||||
return matches ? groovyDsl : null;
|
||||
}
|
||||
}
|
||||
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
|
||||
return groovyDsl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
|
||||
boolean matches;
|
||||
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
|
||||
if (type == ContentType.JSON) {
|
||||
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
|
||||
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
|
||||
}
|
||||
else if (dslBody instanceof RegexProperty && inputMessage instanceof String) {
|
||||
Pattern pattern = ((RegexProperty) dslBody).getPattern();
|
||||
matches = pattern.matcher((String) inputMessage).matches();
|
||||
bodyUnmatchedLog(dslBody, matches, pattern);
|
||||
}
|
||||
else {
|
||||
matches = dslBody.equals(inputMessage);
|
||||
bodyUnmatchedLog(dslBody, matches, inputMessage);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
|
||||
if (log.isDebugEnabled() && !matches) {
|
||||
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
|
||||
+ "]");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
|
||||
Object dslBody) {
|
||||
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
|
||||
JsonPaths jsonPaths = JsonToJsonPathsConverter
|
||||
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
|
||||
DocumentContext parsedJson;
|
||||
try {
|
||||
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("Cannot serialize to JSON", e);
|
||||
}
|
||||
List<String> unmatchedJsonPath = new ArrayList<>();
|
||||
boolean matches = true;
|
||||
for (MethodBufferingJsonVerifiable path : jsonPaths) {
|
||||
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
|
||||
}
|
||||
if (matchers != null && matchers.hasMatchers()) {
|
||||
for (BodyMatcher matcher : matchers.matchers()) {
|
||||
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
|
||||
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
|
||||
}
|
||||
}
|
||||
if (!unmatchedJsonPath.isEmpty()) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
|
||||
try {
|
||||
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e) {
|
||||
unmatchedJsonPath.add(e.getLocalizedMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> headersMatch(Message<?> message, Contract groovyDsl) {
|
||||
List<String> unmatchedHeaders = new ArrayList<>();
|
||||
Map<String, Object> headers = message.getHeaders();
|
||||
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
|
||||
String name = it.getName();
|
||||
Object value = it.getClientValue();
|
||||
Object valueInHeader = headers.get(name);
|
||||
boolean matches;
|
||||
if (value instanceof RegexProperty || value instanceof Pattern) {
|
||||
Pattern pattern = new RegexProperty(value).getPattern();
|
||||
matches = pattern.matcher(valueInHeader.toString()).matches();
|
||||
}
|
||||
else {
|
||||
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
|
||||
}
|
||||
if (!matches) {
|
||||
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
|
||||
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
|
||||
}
|
||||
}
|
||||
return unmatchedHeaders;
|
||||
}
|
||||
|
||||
private String unmatchedText(Object expectedValue) {
|
||||
return expectedValue instanceof RegexProperty
|
||||
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
|
||||
: "be equal to [" + expectedValue + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.integration;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.integration.router.AbstractMessageRouter;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerIntegrationRouter extends AbstractMessageRouter {
|
||||
|
||||
private final StubRunnerIntegrationMessageSelector selector;
|
||||
|
||||
private final BeanFactory beanFactory;
|
||||
|
||||
StubRunnerIntegrationRouter(List<Contract> groovyDsls, BeanFactory beanFactory) {
|
||||
this.selector = new StubRunnerIntegrationMessageSelector(groovyDsls);
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<MessageChannel> determineTargetChannels(Message<?> message) {
|
||||
Contract dsl = this.selector.matchingContract(message);
|
||||
if (dsl != null && dsl.getOutputMessage() != null && dsl.getOutputMessage().getSentTo() != null) {
|
||||
String channelName = dsl.getOutputMessage().getSentTo().getClientValue();
|
||||
return Collections.singleton((MessageChannel) this.beanFactory.getBean(channelName));
|
||||
}
|
||||
return Collections.singleton((MessageChannel) this.beanFactory
|
||||
.getBean(StubRunnerIntegrationConfiguration.DummyMessageHandler.CHANNEL_NAME));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.integration;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
|
||||
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
|
||||
/**
|
||||
* Sends forward a message defined in the DSL.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerIntegrationTransformer {
|
||||
|
||||
private final StubRunnerIntegrationMessageSelector selector;
|
||||
|
||||
StubRunnerIntegrationTransformer(Contract groovyDsl) {
|
||||
this(Collections.singletonList(groovyDsl));
|
||||
}
|
||||
|
||||
StubRunnerIntegrationTransformer(List<Contract> groovyDsls) {
|
||||
this.selector = new StubRunnerIntegrationMessageSelector(groovyDsls);
|
||||
}
|
||||
|
||||
public Message<?> transform(Message<?> source) {
|
||||
Contract groovyDsl = matchingContract(source);
|
||||
if (groovyDsl == null || groovyDsl.getOutputMessage() == null) {
|
||||
return source;
|
||||
}
|
||||
Object outputBody = outputBody(groovyDsl);
|
||||
Map<String, Object> headers = groovyDsl.getOutputMessage().getHeaders().asStubSideMap();
|
||||
MessageHeaders messageHeaders = new MessageHeaders(headers);
|
||||
Message<Object> message = MessageBuilder.createMessage(outputBody, messageHeaders);
|
||||
this.selector.updateCache(message, groovyDsl);
|
||||
return message;
|
||||
}
|
||||
|
||||
private Object outputBody(Contract groovyDsl) {
|
||||
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
|
||||
if (outputBody instanceof FromFileProperty) {
|
||||
FromFileProperty property = (FromFileProperty) outputBody;
|
||||
return property.asBytes();
|
||||
}
|
||||
return BodyExtractor.extractStubValueFrom(outputBody);
|
||||
}
|
||||
|
||||
Contract matchingContract(Message<?> source) {
|
||||
return this.selector.matchingContract(source);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.jms;
|
||||
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.jms.JMSException;
|
||||
import jakarta.jms.Message;
|
||||
import jakarta.jms.ObjectMessage;
|
||||
import jakarta.jms.StreamMessage;
|
||||
import jakarta.jms.TextMessage;
|
||||
|
||||
final class StubRunnerJmsAccessor {
|
||||
|
||||
private StubRunnerJmsAccessor() {
|
||||
throw new IllegalStateException("Can't instantiate an utility class");
|
||||
}
|
||||
|
||||
static Object getBody(Message message) {
|
||||
try {
|
||||
return getPayload(message);
|
||||
}
|
||||
catch (JMSException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static Map<String, Object> getHeaders(Message message) {
|
||||
try {
|
||||
return headers(message);
|
||||
}
|
||||
catch (JMSException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Object> headers(Message message) throws JMSException {
|
||||
Map<String, Object> headers = new HashMap<>();
|
||||
if (message == null) {
|
||||
return headers;
|
||||
}
|
||||
Enumeration enumeration = message.getPropertyNames();
|
||||
while (enumeration.hasMoreElements()) {
|
||||
Object element = enumeration.nextElement();
|
||||
String asString = element.toString();
|
||||
Object property = message.getObjectProperty(asString);
|
||||
headers.put(asString, property);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
private static Object getPayload(Message message) throws JMSException {
|
||||
if (message == null) {
|
||||
return null;
|
||||
}
|
||||
else if (message instanceof TextMessage) {
|
||||
return ((TextMessage) message).getText();
|
||||
}
|
||||
else if (message instanceof StreamMessage) {
|
||||
return ((StreamMessage) message).readObject();
|
||||
}
|
||||
else if (message instanceof ObjectMessage) {
|
||||
return ((ObjectMessage) message).getObject();
|
||||
}
|
||||
return message.getBody(Object.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.jms;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import jakarta.jms.ConnectionFactory;
|
||||
import jakarta.jms.MessageListener;
|
||||
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.stubrunner.BatchStubRunner;
|
||||
import org.springframework.cloud.contract.stubrunner.StubConfiguration;
|
||||
import org.springframework.cloud.contract.verifier.util.MapConverter;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jms.core.JmsTemplate;
|
||||
import org.springframework.jms.listener.DefaultMessageListenerContainer;
|
||||
import org.springframework.jms.listener.MessageListenerContainer;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Spring Integration configuration that iterates over the downloaded Groovy DSLs and
|
||||
* registers a flow for each DSL.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(JmsTemplate.class)
|
||||
@ConditionalOnProperty(name = "stubrunner.jms.enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class StubRunnerJmsConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "stubFlowRegistrar")
|
||||
public FlowRegistrar stubFlowRegistrar(ConfigurableListableBeanFactory beanFactory,
|
||||
BatchStubRunner batchStubRunner) {
|
||||
Map<StubConfiguration, Collection<Contract>> contracts = batchStubRunner.getContracts();
|
||||
for (Entry<StubConfiguration, Collection<Contract>> entry : contracts.entrySet()) {
|
||||
StubConfiguration key = entry.getKey();
|
||||
Collection<Contract> value = entry.getValue();
|
||||
String name = key.getGroupId() + "_" + key.getArtifactId();
|
||||
MultiValueMap<String, Contract> map = new LinkedMultiValueMap<>();
|
||||
for (Contract dsl : value) {
|
||||
if (dsl == null) {
|
||||
continue;
|
||||
}
|
||||
if (dsl.getInput() != null && dsl.getInput().getMessageFrom() != null
|
||||
&& StringUtils.hasText(dsl.getInput().getMessageFrom().getClientValue())) {
|
||||
String from = dsl.getInput().getMessageFrom().getClientValue();
|
||||
map.add(from, dsl);
|
||||
}
|
||||
}
|
||||
for (Entry<String, List<Contract>> entries : map.entrySet()) {
|
||||
List<Contract> matchingContracts = entries.getValue();
|
||||
final String flowName = name + "_" + entries.getKey() + "_" + Math.abs(matchingContracts.hashCode());
|
||||
// listener
|
||||
StubRunnerJmsRouter router = new StubRunnerJmsRouter(matchingContracts, beanFactory);
|
||||
StubRunnerJmsRouter listener = (StubRunnerJmsRouter) beanFactory.initializeBean(router, flowName);
|
||||
beanFactory.registerSingleton(flowName, listener);
|
||||
registerContainers(beanFactory, matchingContracts, flowName, listener);
|
||||
}
|
||||
|
||||
}
|
||||
return new FlowRegistrar();
|
||||
}
|
||||
|
||||
private void registerContainers(ConfigurableListableBeanFactory beanFactory, List<Contract> matchingContracts,
|
||||
String flowName, StubRunnerJmsRouter listener) {
|
||||
// listener's container
|
||||
ConnectionFactory connectionFactory = beanFactory.getBean(ConnectionFactory.class);
|
||||
for (Contract matchingContract : matchingContracts) {
|
||||
if (matchingContract.getInput() == null) {
|
||||
continue;
|
||||
}
|
||||
String destination = MapConverter.getStubSideValuesForNonBody(matchingContract.getInput().getMessageFrom())
|
||||
.toString();
|
||||
MessageListenerContainer container = listenerContainer(destination, connectionFactory, listener);
|
||||
String containerName = flowName + ".container";
|
||||
Object initializedContainer = beanFactory.initializeBean(container, containerName);
|
||||
beanFactory.registerSingleton(containerName, initializedContainer);
|
||||
}
|
||||
}
|
||||
|
||||
private MessageListenerContainer listenerContainer(String queueName, ConnectionFactory connectionFactory,
|
||||
MessageListener listener) {
|
||||
DefaultMessageListenerContainer container = new DefaultMessageListenerContainer();
|
||||
container.setConnectionFactory(connectionFactory);
|
||||
container.setDestinationName(queueName);
|
||||
container.setMessageListener(listener);
|
||||
return container;
|
||||
}
|
||||
|
||||
static class FlowRegistrar {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.jms;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.toomuchcoding.jsonassert.JsonAssertion;
|
||||
import jakarta.jms.Message;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.BodyMatcher;
|
||||
import org.springframework.cloud.contract.spec.internal.BodyMatchers;
|
||||
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.Header;
|
||||
import org.springframework.cloud.contract.spec.internal.RegexProperty;
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
|
||||
import org.springframework.cloud.contract.verifier.util.ContentType;
|
||||
import org.springframework.cloud.contract.verifier.util.ContentUtils;
|
||||
import org.springframework.cloud.contract.verifier.util.JsonPaths;
|
||||
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
|
||||
import org.springframework.cloud.contract.verifier.util.MapConverter;
|
||||
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
|
||||
|
||||
/**
|
||||
* Passes through a message that matches the one defined in the DSL.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Tim Ysewyn
|
||||
*/
|
||||
class StubRunnerJmsMessageSelector {
|
||||
|
||||
private static final Map<Message, Contract> CACHE = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private static final Log log = LogFactory.getLog(StubRunnerJmsMessageSelector.class);
|
||||
|
||||
private final List<Contract> groovyDsls;
|
||||
|
||||
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
|
||||
|
||||
StubRunnerJmsMessageSelector(List<Contract> groovyDsls) {
|
||||
this.groovyDsls = groovyDsls;
|
||||
}
|
||||
|
||||
Contract matchingContract(Message message) {
|
||||
if (CACHE.containsKey(message)) {
|
||||
return CACHE.get(message);
|
||||
}
|
||||
Contract contract = getContract(message);
|
||||
if (contract != null) {
|
||||
CACHE.put(message, contract);
|
||||
}
|
||||
return contract;
|
||||
}
|
||||
|
||||
void updateCache(Message message, Contract contract) {
|
||||
CACHE.put(message, contract);
|
||||
}
|
||||
|
||||
private Contract getContract(Message message) {
|
||||
for (Contract groovyDsl : this.groovyDsls) {
|
||||
Contract contract = matchContract(message, groovyDsl);
|
||||
if (contract != null) {
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Contract matchContract(Message message, Contract groovyDsl) {
|
||||
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
|
||||
if (!unmatchedHeaders.isEmpty()) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Object inputMessage = StubRunnerJmsAccessor.getBody(message);
|
||||
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
|
||||
if (dslBody instanceof FromFileProperty) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Will compare file content");
|
||||
}
|
||||
FromFileProperty property = (FromFileProperty) dslBody;
|
||||
if (property.isString()) {
|
||||
// continue processing as if body was pure string
|
||||
dslBody = property.asString();
|
||||
}
|
||||
else if (!(inputMessage instanceof byte[])) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract provided byte comparison, but the input message is of type ["
|
||||
+ inputMessage.getClass() + "]. Can't compare the two.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
|
||||
if (log.isDebugEnabled() && !matches) {
|
||||
log.debug("Contract provided byte comparison, but the byte arrays don't match");
|
||||
}
|
||||
return matches ? groovyDsl : null;
|
||||
}
|
||||
}
|
||||
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
|
||||
return groovyDsl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
|
||||
boolean matches;
|
||||
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
|
||||
if (type == ContentType.JSON) {
|
||||
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
|
||||
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
|
||||
}
|
||||
else if (dslBody instanceof RegexProperty && inputMessage instanceof String) {
|
||||
Pattern pattern = ((RegexProperty) dslBody).getPattern();
|
||||
matches = pattern.matcher((String) inputMessage).matches();
|
||||
bodyUnmatchedLog(dslBody, matches, pattern);
|
||||
}
|
||||
else {
|
||||
matches = dslBody.equals(inputMessage);
|
||||
bodyUnmatchedLog(dslBody, matches, inputMessage);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
|
||||
if (log.isDebugEnabled() && !matches) {
|
||||
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
|
||||
+ "]");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
|
||||
Object dslBody) {
|
||||
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
|
||||
JsonPaths jsonPaths = JsonToJsonPathsConverter
|
||||
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
|
||||
DocumentContext parsedJson;
|
||||
try {
|
||||
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("Cannot serialize to JSON", e);
|
||||
}
|
||||
List<String> unmatchedJsonPath = new ArrayList<>();
|
||||
boolean matches = true;
|
||||
for (MethodBufferingJsonVerifiable path : jsonPaths) {
|
||||
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
|
||||
}
|
||||
if (matchers != null && matchers.hasMatchers()) {
|
||||
for (BodyMatcher matcher : matchers.matchers()) {
|
||||
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
|
||||
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
|
||||
}
|
||||
}
|
||||
if (!unmatchedJsonPath.isEmpty()) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
|
||||
try {
|
||||
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e) {
|
||||
unmatchedJsonPath.add(e.getLocalizedMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> headersMatch(Message message, Contract groovyDsl) {
|
||||
List<String> unmatchedHeaders = new ArrayList<>();
|
||||
Map<String, Object> headers = StubRunnerJmsAccessor.getHeaders(message);
|
||||
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
|
||||
String name = it.getName();
|
||||
Object value = it.getClientValue();
|
||||
Object valueInHeader = headers.get(name);
|
||||
boolean matches;
|
||||
if (value instanceof RegexProperty) {
|
||||
Pattern pattern = ((RegexProperty) value).getPattern();
|
||||
matches = pattern.matcher(valueInHeader.toString()).matches();
|
||||
}
|
||||
else {
|
||||
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
|
||||
}
|
||||
if (!matches) {
|
||||
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
|
||||
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
|
||||
}
|
||||
}
|
||||
return unmatchedHeaders;
|
||||
}
|
||||
|
||||
private String unmatchedText(Object expectedValue) {
|
||||
return expectedValue instanceof RegexProperty
|
||||
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
|
||||
: "be equal to [" + expectedValue + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.jms;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.jms.JMSException;
|
||||
import jakarta.jms.Message;
|
||||
import jakarta.jms.MessageListener;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.jms.core.JmsTemplate;
|
||||
import org.springframework.jms.core.MessagePostProcessor;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerJmsRouter implements MessageListener {
|
||||
|
||||
private final StubRunnerJmsMessageSelector selector;
|
||||
|
||||
private final BeanFactory beanFactory;
|
||||
|
||||
private final List<Contract> contracts;
|
||||
|
||||
private JmsTemplate jmsTemplate;
|
||||
|
||||
StubRunnerJmsRouter(List<Contract> groovyDsls, BeanFactory beanFactory) {
|
||||
this.selector = new StubRunnerJmsMessageSelector(groovyDsls);
|
||||
this.beanFactory = beanFactory;
|
||||
this.contracts = groovyDsls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(jakarta.jms.Message message) {
|
||||
Contract dsl = this.selector.matchingContract(message);
|
||||
if (dsl != null && dsl.getOutputMessage() != null && dsl.getOutputMessage().getSentTo() != null) {
|
||||
String destination = dsl.getOutputMessage().getSentTo().getClientValue();
|
||||
jmsTemplate().send(destination,
|
||||
session -> new StubRunnerJmsTransformer(this.contracts).transform(session, dsl));
|
||||
}
|
||||
}
|
||||
|
||||
private JmsTemplate jmsTemplate() {
|
||||
if (this.jmsTemplate == null) {
|
||||
this.jmsTemplate = this.beanFactory.getBean(JmsTemplate.class);
|
||||
}
|
||||
return this.jmsTemplate;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ReplyToProcessor implements MessagePostProcessor {
|
||||
|
||||
@Override
|
||||
public jakarta.jms.Message postProcessMessage(Message message) throws JMSException {
|
||||
message.setStringProperty("requiresReply", "no");
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.jms;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.jms.BytesMessage;
|
||||
import jakarta.jms.JMSException;
|
||||
import jakarta.jms.Message;
|
||||
import jakarta.jms.Session;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
|
||||
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
|
||||
|
||||
/**
|
||||
* Sends forward a message defined in the DSL.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerJmsTransformer {
|
||||
|
||||
private final StubRunnerJmsMessageSelector selector;
|
||||
|
||||
StubRunnerJmsTransformer(List<Contract> groovyDsls) {
|
||||
this.selector = new StubRunnerJmsMessageSelector(groovyDsls);
|
||||
}
|
||||
|
||||
public Message transform(Session session, Contract groovyDsl) {
|
||||
Object outputBody = outputBody(groovyDsl);
|
||||
Map<String, Object> headers = groovyDsl.getOutputMessage().getHeaders().asStubSideMap();
|
||||
Message newMessage = createMessage(session, outputBody);
|
||||
setHeaders(newMessage, headers);
|
||||
this.selector.updateCache(newMessage, groovyDsl);
|
||||
return newMessage;
|
||||
}
|
||||
|
||||
private Object outputBody(Contract groovyDsl) {
|
||||
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
|
||||
if (outputBody instanceof FromFileProperty) {
|
||||
FromFileProperty property = (FromFileProperty) outputBody;
|
||||
return property.asBytes();
|
||||
}
|
||||
return BodyExtractor.extractStubValueFrom(outputBody);
|
||||
}
|
||||
|
||||
Contract matchingContract(Message source) {
|
||||
return this.selector.matchingContract(source);
|
||||
}
|
||||
|
||||
private Message createMessage(Session session, Object payload) {
|
||||
try {
|
||||
if (payload instanceof String) {
|
||||
return session.createTextMessage((String) payload);
|
||||
}
|
||||
else if (payload instanceof byte[]) {
|
||||
BytesMessage bytesMessage = session.createBytesMessage();
|
||||
bytesMessage.writeBytes((byte[]) payload);
|
||||
return bytesMessage;
|
||||
}
|
||||
else if (payload instanceof Serializable) {
|
||||
return session.createObjectMessage((Serializable) payload);
|
||||
}
|
||||
return session.createMessage();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void setHeaders(Message message, Map<String, Object> headers) {
|
||||
for (Map.Entry<String, Object> entry : headers.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
try {
|
||||
if (value instanceof String) {
|
||||
message.setStringProperty(key, (String) value);
|
||||
}
|
||||
else if (value instanceof Boolean) {
|
||||
message.setBooleanProperty(key, (Boolean) value);
|
||||
}
|
||||
else {
|
||||
message.setObjectProperty(key, value);
|
||||
}
|
||||
}
|
||||
catch (JMSException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.kafka;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.toomuchcoding.jsonassert.JsonAssertion;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.BodyMatcher;
|
||||
import org.springframework.cloud.contract.spec.internal.BodyMatchers;
|
||||
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.Header;
|
||||
import org.springframework.cloud.contract.spec.internal.RegexProperty;
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
|
||||
import org.springframework.cloud.contract.verifier.util.ContentType;
|
||||
import org.springframework.cloud.contract.verifier.util.ContentUtils;
|
||||
import org.springframework.cloud.contract.verifier.util.JsonPaths;
|
||||
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
|
||||
import org.springframework.cloud.contract.verifier.util.MapConverter;
|
||||
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
* Passes through a message that matches the one defined in the DSL.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerKafkaMessageSelector {
|
||||
|
||||
private static final Map<Message<?>, Contract> CACHE = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private static final Log log = LogFactory.getLog(StubRunnerKafkaMessageSelector.class);
|
||||
|
||||
private final List<Contract> groovyDsls;
|
||||
|
||||
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
|
||||
|
||||
StubRunnerKafkaMessageSelector(List<Contract> groovyDsls) {
|
||||
this.groovyDsls = groovyDsls;
|
||||
}
|
||||
|
||||
Contract matchingContract(Message<?> message) {
|
||||
if (CACHE.containsKey(message)) {
|
||||
return CACHE.get(message);
|
||||
}
|
||||
Contract contract = getContract(message);
|
||||
if (contract != null) {
|
||||
CACHE.put(message, contract);
|
||||
}
|
||||
return contract;
|
||||
}
|
||||
|
||||
void updateCache(Message<?> message, Contract contract) {
|
||||
CACHE.put(message, contract);
|
||||
}
|
||||
|
||||
private Contract getContract(Message<?> message) {
|
||||
for (Contract groovyDsl : this.groovyDsls) {
|
||||
Contract contract = matchContract(message, groovyDsl);
|
||||
if (contract != null) {
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Contract matchContract(Message<?> message, Contract groovyDsl) {
|
||||
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
|
||||
if (!unmatchedHeaders.isEmpty()) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Object inputMessage = message.getPayload();
|
||||
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
|
||||
if (dslBody instanceof FromFileProperty) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Will compare file content");
|
||||
}
|
||||
FromFileProperty property = (FromFileProperty) dslBody;
|
||||
if (property.isString()) {
|
||||
// continue processing as if body was pure string
|
||||
dslBody = property.asString();
|
||||
}
|
||||
else if (!(inputMessage instanceof byte[])) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract provided byte comparison, but the input message is of type ["
|
||||
+ inputMessage.getClass() + "]. Can't compare the two.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
|
||||
if (log.isDebugEnabled() && !matches) {
|
||||
log.debug("Contract provided byte comparison, but the byte arrays don't match");
|
||||
}
|
||||
return matches ? groovyDsl : null;
|
||||
}
|
||||
}
|
||||
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
|
||||
return groovyDsl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
|
||||
boolean matches;
|
||||
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
|
||||
if (type == ContentType.JSON) {
|
||||
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
|
||||
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
|
||||
}
|
||||
else if (dslBody instanceof RegexProperty && inputMessage instanceof String) {
|
||||
Pattern pattern = ((RegexProperty) dslBody).getPattern();
|
||||
matches = pattern.matcher((String) inputMessage).matches();
|
||||
bodyUnmatchedLog(dslBody, matches, pattern);
|
||||
}
|
||||
else {
|
||||
matches = dslBody.equals(inputMessage);
|
||||
bodyUnmatchedLog(dslBody, matches, inputMessage);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
|
||||
if (log.isDebugEnabled() && !matches) {
|
||||
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
|
||||
+ "]");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
|
||||
Object dslBody) {
|
||||
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
|
||||
JsonPaths jsonPaths = JsonToJsonPathsConverter
|
||||
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
|
||||
DocumentContext parsedJson;
|
||||
try {
|
||||
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("Cannot serialize to JSON", e);
|
||||
}
|
||||
List<String> unmatchedJsonPath = new ArrayList<>();
|
||||
boolean matches = true;
|
||||
for (MethodBufferingJsonVerifiable path : jsonPaths) {
|
||||
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
|
||||
}
|
||||
if (matchers != null && matchers.hasMatchers()) {
|
||||
for (BodyMatcher matcher : matchers.matchers()) {
|
||||
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
|
||||
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
|
||||
}
|
||||
}
|
||||
if (!unmatchedJsonPath.isEmpty()) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
|
||||
try {
|
||||
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e) {
|
||||
unmatchedJsonPath.add(e.getLocalizedMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> headersMatch(Message message, Contract groovyDsl) {
|
||||
List<String> unmatchedHeaders = new ArrayList<>();
|
||||
Map<String, Object> headers = message.getHeaders();
|
||||
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
|
||||
String name = it.getName();
|
||||
Object value = it.getClientValue();
|
||||
Object valueInHeader = headers.get(name);
|
||||
valueInHeader = valueInHeader instanceof byte[] ? fromByte((byte[]) valueInHeader) : valueInHeader;
|
||||
boolean matches;
|
||||
if (value instanceof RegexProperty) {
|
||||
Pattern pattern = ((RegexProperty) value).getPattern();
|
||||
matches = pattern.matcher(valueInHeader.toString()).matches();
|
||||
}
|
||||
else {
|
||||
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
|
||||
}
|
||||
if (!matches) {
|
||||
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
|
||||
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
|
||||
}
|
||||
}
|
||||
return unmatchedHeaders;
|
||||
}
|
||||
|
||||
private String fromByte(byte[] valueInHeader) {
|
||||
String string = new String(valueInHeader);
|
||||
if (string.startsWith("\"") && string.endsWith("\"")) {
|
||||
return string.substring(1, string.length() - 1);
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
private String unmatchedText(Object expectedValue) {
|
||||
return expectedValue instanceof RegexProperty
|
||||
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
|
||||
: "be equal to [" + expectedValue + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.kafka;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.kafka.listener.MessageListener;
|
||||
import org.springframework.kafka.support.Acknowledgment;
|
||||
import org.springframework.kafka.support.converter.MessagingMessageConverter;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerKafkaRouter implements MessageListener<Object, Object> {
|
||||
|
||||
private static final Log log = LogFactory.getLog(StubRunnerKafkaRouter.class);
|
||||
|
||||
private final MessagingMessageConverter messageConverter = new MessagingMessageConverter();
|
||||
|
||||
private final StubRunnerKafkaMessageSelector selector;
|
||||
|
||||
private final BeanFactory beanFactory;
|
||||
|
||||
private final List<Contract> contracts;
|
||||
|
||||
private KafkaTemplate kafkaTemplate;
|
||||
|
||||
StubRunnerKafkaRouter(List<Contract> groovyDsls, BeanFactory beanFactory) {
|
||||
this.selector = new StubRunnerKafkaMessageSelector(groovyDsls);
|
||||
this.beanFactory = beanFactory;
|
||||
this.contracts = groovyDsls;
|
||||
}
|
||||
|
||||
private KafkaTemplate kafkaTemplate() {
|
||||
if (this.kafkaTemplate == null) {
|
||||
this.kafkaTemplate = this.beanFactory.getBean(KafkaTemplate.class);
|
||||
}
|
||||
return this.kafkaTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(ConsumerRecord<Object, Object> data) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Received message [" + data + "]");
|
||||
}
|
||||
Message<?> message = messageConverter.toMessage(data, null, null, null);
|
||||
Contract dsl = this.selector.matchingContract(message);
|
||||
if (dsl != null && dsl.getOutputMessage() != null && dsl.getOutputMessage().getSentTo() != null) {
|
||||
String destination = dsl.getOutputMessage().getSentTo().getClientValue();
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Found a matching contract with an output message. Will send it to the [" + destination
|
||||
+ "] destination");
|
||||
}
|
||||
Message<?> transform = new StubRunnerKafkaTransformer(this.contracts).transform(dsl);
|
||||
String defaultTopic = kafkaTemplate().getDefaultTopic();
|
||||
try {
|
||||
kafkaTemplate().setDefaultTopic(destination);
|
||||
kafkaTemplate().send(transform);
|
||||
}
|
||||
finally {
|
||||
kafkaTemplate().setDefaultTopic(defaultTopic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(ConsumerRecord<Object, Object> data, Acknowledgment acknowledgment) {
|
||||
onMessage(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(ConsumerRecord<Object, Object> data, Consumer<?, ?> consumer) {
|
||||
onMessage(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(ConsumerRecord<Object, Object> data, Acknowledgment acknowledgment, Consumer<?, ?> consumer) {
|
||||
onMessage(data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.kafka;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
|
||||
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
|
||||
/**
|
||||
* Sends forward a message defined in the DSL.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerKafkaTransformer {
|
||||
|
||||
private final StubRunnerKafkaMessageSelector selector;
|
||||
|
||||
StubRunnerKafkaTransformer(List<Contract> groovyDsls) {
|
||||
this.selector = new StubRunnerKafkaMessageSelector(groovyDsls);
|
||||
}
|
||||
|
||||
public Message<?> transform(Contract groovyDsl) {
|
||||
Object outputBody = outputBody(groovyDsl);
|
||||
Map<String, Object> headers = groovyDsl.getOutputMessage().getHeaders().asStubSideMap();
|
||||
Message newMessage = MessageBuilder.createMessage(outputBody, new MessageHeaders(headers));
|
||||
this.selector.updateCache(newMessage, groovyDsl);
|
||||
return newMessage;
|
||||
}
|
||||
|
||||
private Object outputBody(Contract groovyDsl) {
|
||||
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
|
||||
if (outputBody instanceof FromFileProperty) {
|
||||
FromFileProperty property = (FromFileProperty) outputBody;
|
||||
return property.asBytes();
|
||||
}
|
||||
return BodyExtractor.extractStubValueFrom(outputBody);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.stream;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.integration.router.AbstractMessageRouter;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerMessageRouter extends AbstractMessageRouter {
|
||||
|
||||
private final StubRunnerStreamMessageSelector selector;
|
||||
|
||||
private final BeanFactory beanFactory;
|
||||
|
||||
StubRunnerMessageRouter(List<Contract> groovyDsls, BeanFactory beanFactory) {
|
||||
this.selector = new StubRunnerStreamMessageSelector(groovyDsls);
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Collection<MessageChannel> determineTargetChannels(Message<?> message) {
|
||||
Contract dsl = this.selector.matchingContract(message);
|
||||
if (dsl != null && dsl.getOutputMessage() != null && dsl.getOutputMessage().getSentTo() != null) {
|
||||
String channelName = StubRunnerStreamConfiguration.resolvedDestination(this.beanFactory,
|
||||
dsl.getOutputMessage().getSentTo().getClientValue());
|
||||
return Collections.singleton((MessageChannel) this.beanFactory.getBean(channelName));
|
||||
}
|
||||
return Collections.singleton((MessageChannel) this.beanFactory.getBean("nullChannel"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.stream;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.toomuchcoding.jsonassert.JsonAssertion;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.BodyMatcher;
|
||||
import org.springframework.cloud.contract.spec.internal.BodyMatchers;
|
||||
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.Header;
|
||||
import org.springframework.cloud.contract.spec.internal.RegexProperty;
|
||||
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
|
||||
import org.springframework.cloud.contract.verifier.util.ContentType;
|
||||
import org.springframework.cloud.contract.verifier.util.ContentUtils;
|
||||
import org.springframework.cloud.contract.verifier.util.JsonPaths;
|
||||
import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
|
||||
import org.springframework.cloud.contract.verifier.util.MapConverter;
|
||||
import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
|
||||
import org.springframework.integration.core.MessageSelector;
|
||||
import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
* Passes through a message that matches the one defined in the DSL.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Tim Ysewyn
|
||||
*/
|
||||
class StubRunnerStreamMessageSelector implements MessageSelector {
|
||||
|
||||
private static final Map<Message, Contract> CACHE = Collections.synchronizedMap(new WeakHashMap<>());
|
||||
|
||||
private static final Log log = LogFactory.getLog(StubRunnerStreamMessageSelector.class);
|
||||
|
||||
private final List<Contract> groovyDsls;
|
||||
|
||||
private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
|
||||
|
||||
StubRunnerStreamMessageSelector(Contract groovyDsl) {
|
||||
this(Collections.singletonList(groovyDsl));
|
||||
}
|
||||
|
||||
StubRunnerStreamMessageSelector(List<Contract> groovyDsls) {
|
||||
this.groovyDsls = groovyDsls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(Message<?> message) {
|
||||
return matchingContract(message) != null;
|
||||
}
|
||||
|
||||
Contract matchingContract(Message<?> message) {
|
||||
if (CACHE.containsKey(message)) {
|
||||
return CACHE.get(message);
|
||||
}
|
||||
Contract contract = getContract(message);
|
||||
if (contract != null) {
|
||||
CACHE.put(message, contract);
|
||||
}
|
||||
return contract;
|
||||
}
|
||||
|
||||
void updateCache(Message<?> message, Contract contract) {
|
||||
CACHE.put(message, contract);
|
||||
}
|
||||
|
||||
private Contract getContract(Message<?> message) {
|
||||
for (Contract groovyDsl : this.groovyDsls) {
|
||||
Contract contract = matchContract(message, groovyDsl);
|
||||
if (contract != null) {
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Contract matchContract(Message<?> message, Contract groovyDsl) {
|
||||
List<String> unmatchedHeaders = headersMatch(message, groovyDsl);
|
||||
if (!unmatchedHeaders.isEmpty()) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract [" + groovyDsl + "] hasn't matched the following headers " + unmatchedHeaders);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Object inputMessage = message.getPayload();
|
||||
Object dslBody = MapConverter.getStubSideValues(groovyDsl.getInput().getMessageBody());
|
||||
if (dslBody instanceof FromFileProperty) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Will compare file content");
|
||||
}
|
||||
FromFileProperty property = (FromFileProperty) dslBody;
|
||||
if (property.isString()) {
|
||||
// continue processing as if body was pure string
|
||||
dslBody = property.asString();
|
||||
}
|
||||
else if (!(inputMessage instanceof byte[])) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract provided byte comparison, but the input message is of type ["
|
||||
+ inputMessage.getClass() + "]. Can't compare the two.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
boolean matches = Arrays.equals(property.asBytes(), (byte[]) inputMessage);
|
||||
if (log.isDebugEnabled() && !matches) {
|
||||
log.debug("Contract provided byte comparison, but the byte arrays don't match");
|
||||
}
|
||||
return matches ? groovyDsl : null;
|
||||
}
|
||||
}
|
||||
if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
|
||||
return groovyDsl;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean matchViaContent(Contract groovyDsl, Object inputMessage, Object dslBody) {
|
||||
boolean matches;
|
||||
ContentType type = ContentUtils.getClientContentType(inputMessage, groovyDsl.getInput().getMessageHeaders());
|
||||
if (type == ContentType.JSON) {
|
||||
BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
|
||||
matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
|
||||
}
|
||||
else if ((dslBody instanceof RegexProperty || dslBody instanceof Pattern) && inputMessage instanceof String) {
|
||||
Pattern pattern = new RegexProperty(dslBody).getPattern();
|
||||
matches = pattern.matcher((String) inputMessage).matches();
|
||||
bodyUnmatchedLog(dslBody, matches, pattern);
|
||||
}
|
||||
else {
|
||||
matches = dslBody.equals(inputMessage);
|
||||
bodyUnmatchedLog(dslBody, matches, inputMessage);
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
|
||||
if (log.isDebugEnabled() && !matches) {
|
||||
log.debug("Body was supposed to " + unmatchedText(pattern) + " but the value is [" + dslBody.toString()
|
||||
+ "]");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage, BodyMatchers matchers,
|
||||
Object dslBody) {
|
||||
Object matchingInputMessage = JsonToJsonPathsConverter.removeMatchingJsonPaths(dslBody, matchers);
|
||||
JsonPaths jsonPaths = JsonToJsonPathsConverter
|
||||
.transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(matchingInputMessage);
|
||||
DocumentContext parsedJson;
|
||||
try {
|
||||
parsedJson = JsonPath.parse(this.objectMapper.writeValueAsString(inputMessage));
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("Cannot serialize to JSON", e);
|
||||
}
|
||||
List<String> unmatchedJsonPath = new ArrayList<>();
|
||||
boolean matches = true;
|
||||
for (MethodBufferingJsonVerifiable path : jsonPaths) {
|
||||
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
|
||||
}
|
||||
if (matchers != null && matchers.hasMatchers()) {
|
||||
for (BodyMatcher matcher : matchers.matchers()) {
|
||||
String jsonPath = JsonToJsonPathsConverter.convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
|
||||
matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
|
||||
}
|
||||
}
|
||||
if (!unmatchedJsonPath.isEmpty()) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Contract [" + groovyDsl + "] didn't match the body due to " + unmatchedJsonPath);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
private boolean matchesJsonPath(List<String> unmatchedJsonPath, DocumentContext parsedJson, String jsonPath) {
|
||||
try {
|
||||
JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e) {
|
||||
unmatchedJsonPath.add(e.getLocalizedMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> headersMatch(Message<?> message, Contract groovyDsl) {
|
||||
List<String> unmatchedHeaders = new ArrayList<>();
|
||||
Map<String, Object> headers = message.getHeaders();
|
||||
for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
|
||||
String name = it.getName();
|
||||
Object value = it.getClientValue();
|
||||
Object valueInHeader = headers.get(name);
|
||||
boolean matches;
|
||||
if (value instanceof RegexProperty || value instanceof Pattern) {
|
||||
Pattern pattern = new RegexProperty(value).getPattern();
|
||||
matches = pattern.matcher(valueInHeader.toString()).matches();
|
||||
}
|
||||
else {
|
||||
matches = valueInHeader != null && valueInHeader.toString().equals(value.toString());
|
||||
}
|
||||
if (!matches) {
|
||||
unmatchedHeaders.add("Header with name [" + name + "] was supposed to " + unmatchedText(value)
|
||||
+ " but the value is [" + (valueInHeader != null ? valueInHeader.toString() : "null") + "]");
|
||||
}
|
||||
}
|
||||
return unmatchedHeaders;
|
||||
}
|
||||
|
||||
private String unmatchedText(Object expectedValue) {
|
||||
return expectedValue instanceof RegexProperty
|
||||
? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
|
||||
: "be equal to [" + expectedValue + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.stream;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
|
||||
import org.springframework.cloud.contract.verifier.util.BodyExtractor;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
|
||||
/**
|
||||
* Sends forward a message defined in the DSL.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerStreamTransformer {
|
||||
|
||||
private final StubRunnerStreamMessageSelector selector;
|
||||
|
||||
StubRunnerStreamTransformer(Contract groovyDsl) {
|
||||
this(Collections.singletonList(groovyDsl));
|
||||
}
|
||||
|
||||
StubRunnerStreamTransformer(List<Contract> groovyDsls) {
|
||||
this.selector = new StubRunnerStreamMessageSelector(groovyDsls);
|
||||
}
|
||||
|
||||
public Message<?> transform(Message<?> source) {
|
||||
Contract groovyDsl = matchingContract(source);
|
||||
if (groovyDsl == null || groovyDsl.getOutputMessage() == null) {
|
||||
return source;
|
||||
}
|
||||
byte[] outputBody = outputBodyAsBytes(groovyDsl);
|
||||
Map<String, Object> headers = groovyDsl.getOutputMessage().getHeaders().asStubSideMap();
|
||||
MessageHeaders messageHeaders = new MessageHeaders(headers);
|
||||
Message<byte[]> message = MessageBuilder.createMessage(outputBody, messageHeaders);
|
||||
this.selector.updateCache(message, groovyDsl);
|
||||
return message;
|
||||
}
|
||||
|
||||
private byte[] outputBodyAsBytes(Contract groovyDsl) {
|
||||
Object outputBody = BodyExtractor.extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
|
||||
if (outputBody instanceof FromFileProperty) {
|
||||
FromFileProperty property = (FromFileProperty) outputBody;
|
||||
return property.asBytes();
|
||||
}
|
||||
return BodyExtractor.extractStubValueFrom(outputBody).getBytes();
|
||||
}
|
||||
|
||||
Contract matchingContract(Message<?> source) {
|
||||
return this.selector.matchingContract(source);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.camel
|
||||
|
||||
import org.apache.camel.Exchange
|
||||
import org.apache.camel.Message
|
||||
import org.apache.camel.impl.DefaultCamelContext
|
||||
import org.apache.camel.support.DefaultExchange
|
||||
import org.apache.camel.support.DefaultMessage
|
||||
import spock.lang.Specification
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerCamelPredicateSpec extends Specification {
|
||||
Exchange exchange = new DefaultExchange(new DefaultCamelContext())
|
||||
Message message = new DefaultMessage(exchange)
|
||||
|
||||
def "should return false if headers don't match"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageBody(foo: "bar")
|
||||
messageHeaders {
|
||||
header("foo", $(c(regex("[0-9]{3}")), p(123)))
|
||||
}
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
|
||||
exchange.in = message
|
||||
message.headers = [
|
||||
foo: "non matching stuff"
|
||||
]
|
||||
expect:
|
||||
!predicate.matches(exchange)
|
||||
}
|
||||
|
||||
def "should return false if headers match and body doesn't"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
}
|
||||
messageBody(foo: $(c(regex("[0-9]{3}")), p(123)))
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
|
||||
exchange.in = message
|
||||
message.headers = [
|
||||
foo: 123
|
||||
]
|
||||
message.body = [
|
||||
foo: "non matching stuff"
|
||||
]
|
||||
expect:
|
||||
!predicate.matches(exchange)
|
||||
}
|
||||
|
||||
def "should return false if headers match and body doesn't when it's using matchers"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
}
|
||||
messageBody(foo: "non matching stuff")
|
||||
bodyMatchers {
|
||||
jsonPath('$.foo', byRegex("[0-9]{3}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
|
||||
exchange.in >> message
|
||||
message.headers >> [
|
||||
foo: 123
|
||||
]
|
||||
message.body >> [
|
||||
foo: "non matching stuff"
|
||||
]
|
||||
expect:
|
||||
!predicate.matches(exchange)
|
||||
}
|
||||
|
||||
def "should return true if headers and body match"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
}
|
||||
messageBody(foo: $(c(regex("[0-9]{3}")), p(123)))
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
|
||||
exchange.in = message
|
||||
message.headers = [
|
||||
foo: 123
|
||||
]
|
||||
message.body = [
|
||||
foo: 123
|
||||
]
|
||||
expect:
|
||||
predicate.matches(exchange)
|
||||
}
|
||||
|
||||
def "should return true if headers and body using matchers match"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
}
|
||||
messageBody(foo: 123)
|
||||
bodyMatchers {
|
||||
jsonPath('$.foo', byRegex("[0-9]{3}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
|
||||
exchange.in = message
|
||||
message.headers = [
|
||||
foo: 123
|
||||
]
|
||||
message.body = [
|
||||
foo: 123
|
||||
]
|
||||
expect:
|
||||
predicate.matches(exchange)
|
||||
}
|
||||
|
||||
def "should return true if headers and byte body matches"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
messagingContentType(applicationOctetStream())
|
||||
}
|
||||
messageBody(fileAsBytes("request.pdf"))
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
|
||||
exchange.in = message
|
||||
message.headers = [
|
||||
foo : 123,
|
||||
contentType: "application/octet-stream"
|
||||
]
|
||||
message.body = StubRunnerCamelPredicate.getResource("/request.pdf").bytes
|
||||
expect:
|
||||
predicate.matches(exchange)
|
||||
}
|
||||
|
||||
def "should return false if byte body types don't match for binary"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
messagingContentType(applicationOctetStream())
|
||||
}
|
||||
messageBody(fileAsBytes("request.pdf"))
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerCamelPredicate predicate = new StubRunnerCamelPredicate([dsl])
|
||||
exchange.in = message
|
||||
message.headers = [
|
||||
foo : 123,
|
||||
contentType: "application/octet-stream"
|
||||
]
|
||||
message.body = "hello world"
|
||||
expect:
|
||||
!predicate.matches(exchange)
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.integration
|
||||
|
||||
import spock.lang.Issue
|
||||
import spock.lang.Specification
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
import org.springframework.messaging.Message
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerIntegrationMessageSelectorSpec extends Specification {
|
||||
Message message = Mock(Message)
|
||||
|
||||
def "should return false if headers don't match"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageBody(foo: "bar")
|
||||
messageHeaders {
|
||||
header("foo", $(c(regex("[0-9]{3}")), p(123)))
|
||||
}
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
|
||||
message.headers >> [
|
||||
foo: "non matching stuff"
|
||||
]
|
||||
expect:
|
||||
!predicate.accept(message)
|
||||
}
|
||||
|
||||
def "should return false if headers match and body doesn't"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
}
|
||||
messageBody(foo: $(c(regex("[0-9]{3}")), p(123)))
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
|
||||
message.headers >> [
|
||||
foo: 123
|
||||
]
|
||||
message.payload >> [
|
||||
foo: "non matching stuff"
|
||||
]
|
||||
expect:
|
||||
!predicate.accept(message)
|
||||
}
|
||||
|
||||
def "should return false if headers match and body doesn't when it's using matchers"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
}
|
||||
messageBody(foo: "non matching stuff")
|
||||
bodyMatchers {
|
||||
jsonPath('$.foo', byRegex("[0-9]{3}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
|
||||
message.headers >> [
|
||||
foo: 123
|
||||
]
|
||||
message.payload >> [
|
||||
foo: "non matching stuff"
|
||||
]
|
||||
expect:
|
||||
!predicate.accept(message)
|
||||
}
|
||||
|
||||
def "should return true if headers and byte body matches"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
messagingContentType(applicationOctetStream())
|
||||
}
|
||||
messageBody(fileAsBytes("request.pdf"))
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
|
||||
message.headers >> [
|
||||
foo : 123,
|
||||
contentType: "application/octet-stream"
|
||||
]
|
||||
message.payload >> StubRunnerIntegrationMessageSelector.getResource("/request.pdf").bytes
|
||||
expect:
|
||||
predicate.accept(message)
|
||||
}
|
||||
|
||||
def "should return false if byte body types don't match for binary"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
messagingContentType(applicationOctetStream())
|
||||
}
|
||||
messageBody(fileAsBytes("request.pdf"))
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
|
||||
message.headers >> [
|
||||
foo : 123,
|
||||
contentType: "application/octet-stream"
|
||||
]
|
||||
message.payload >> "hello world"
|
||||
expect:
|
||||
!predicate.accept(message)
|
||||
}
|
||||
|
||||
def "should return true if headers and body match"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
}
|
||||
messageBody(foo: $(c(regex("[0-9]{3}")), p(123)))
|
||||
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(dsl)
|
||||
message.headers >> [
|
||||
foo: 123
|
||||
]
|
||||
message.payload >> [
|
||||
foo: 123
|
||||
]
|
||||
expect:
|
||||
predicate.accept(message)
|
||||
}
|
||||
|
||||
def "should return true if headers and body using matchers match"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
}
|
||||
messageBody(foo: 123)
|
||||
bodyMatchers {
|
||||
jsonPath('$.foo', byRegex("[0-9]{3}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(
|
||||
dsl)
|
||||
message.headers >> [
|
||||
foo: 123
|
||||
]
|
||||
message.payload >> [
|
||||
foo: 123
|
||||
]
|
||||
expect:
|
||||
predicate.accept(message)
|
||||
}
|
||||
|
||||
@Issue("1382")
|
||||
def "should return true if header matches regex"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", $(anyUuid()))
|
||||
}
|
||||
messageBody(foo: 123)
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerIntegrationMessageSelector predicate = new StubRunnerIntegrationMessageSelector(
|
||||
dsl)
|
||||
message.headers >> [
|
||||
foo: "fbcc2ed3-dbac-47e7-9a4b-c2d55709792c"
|
||||
]
|
||||
message.payload >> [
|
||||
foo: 123
|
||||
]
|
||||
expect:
|
||||
predicate.accept(message)
|
||||
}
|
||||
}
|
||||
@@ -1,250 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.stubrunner.messaging.stream
|
||||
|
||||
import spock.lang.Issue
|
||||
import spock.lang.Specification
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.messaging.Message
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class StubRunnerStreamMessageSelectorSpec extends Specification {
|
||||
Message message = Mock(Message)
|
||||
|
||||
def "should return false if headers don't match"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageBody(foo: "bar")
|
||||
messageHeaders {
|
||||
header("foo", $(c(regex("[0-9]{3}")), p(123)))
|
||||
}
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
|
||||
message.headers >> [
|
||||
foo: "non matching stuff"
|
||||
]
|
||||
expect:
|
||||
!predicate.accept(message)
|
||||
}
|
||||
|
||||
def "should return false if headers match and body doesn't"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
}
|
||||
messageBody(foo: $(c(regex("[0-9]{3}")), p(123)))
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
|
||||
message.headers >> [
|
||||
foo: 123
|
||||
]
|
||||
message.payload >> [
|
||||
foo: "non matching stuff"
|
||||
]
|
||||
expect:
|
||||
!predicate.accept(message)
|
||||
}
|
||||
|
||||
def "should return false if headers match and body doesn't when it's using matchers"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
}
|
||||
messageBody(foo: "non matching stuff")
|
||||
bodyMatchers {
|
||||
jsonPath('$.foo', byRegex("[0-9]{3}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
|
||||
message.headers >> [
|
||||
foo: 123
|
||||
]
|
||||
message.payload >> [
|
||||
foo: "non matching stuff"
|
||||
]
|
||||
expect:
|
||||
!predicate.accept(message)
|
||||
}
|
||||
|
||||
def "should return true if headers and body match"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
header("bar", "bar")
|
||||
messagingContentType(applicationJsonUtf8())
|
||||
header("regex", regex("234"))
|
||||
}
|
||||
messageBody(foo: $(c(regex("[0-9]{3}")), p(123)))
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
|
||||
message.headers >> [
|
||||
foo : 123,
|
||||
bar : "bar",
|
||||
contentType: MediaType.APPLICATION_JSON_UTF8,
|
||||
regex : 234
|
||||
]
|
||||
message.payload >> [
|
||||
foo: 123
|
||||
]
|
||||
expect:
|
||||
predicate.accept(message)
|
||||
}
|
||||
|
||||
def "should return true if headers and text body matches"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
messagingContentType(textPlain())
|
||||
}
|
||||
messageBody($(c(regex("[0-9]{3}")), p("123")))
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
|
||||
message.headers >> [
|
||||
foo : 123,
|
||||
contentType: "text/plain"
|
||||
]
|
||||
message.payload >> "123"
|
||||
expect:
|
||||
predicate.accept(message)
|
||||
}
|
||||
|
||||
def "should return true if headers and byte body matches"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
messagingContentType(applicationOctetStream())
|
||||
}
|
||||
messageBody(fileAsBytes("request.pdf"))
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
|
||||
message.headers >> [
|
||||
foo : 123,
|
||||
contentType: "application/octet-stream"
|
||||
]
|
||||
message.payload >> StubRunnerStreamMessageSelectorSpec.getResource("/request.pdf").bytes
|
||||
expect:
|
||||
predicate.accept(message)
|
||||
}
|
||||
|
||||
def "should return false if byte body types don't match for binary"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
messagingContentType(applicationOctetStream())
|
||||
}
|
||||
messageBody(fileAsBytes("request.pdf"))
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(dsl)
|
||||
message.headers >> [
|
||||
foo : 123,
|
||||
contentType: "application/octet-stream"
|
||||
]
|
||||
message.payload >> "hello world"
|
||||
expect:
|
||||
!predicate.accept(message)
|
||||
}
|
||||
|
||||
def "should return true if headers and body using matchers match"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", 123)
|
||||
}
|
||||
messageBody(foo: 123)
|
||||
bodyMatchers {
|
||||
jsonPath('$.foo', byRegex("[0-9]{3}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(
|
||||
dsl)
|
||||
message.headers >> [
|
||||
foo: 123
|
||||
]
|
||||
message.payload >> [
|
||||
foo: 123
|
||||
]
|
||||
expect:
|
||||
predicate.accept(message)
|
||||
}
|
||||
|
||||
@Issue("1382")
|
||||
def "should return true if header matches regex"() {
|
||||
given:
|
||||
Contract dsl = Contract.make {
|
||||
input {
|
||||
messageFrom "foo"
|
||||
messageHeaders {
|
||||
header("foo", $(anyUuid()))
|
||||
}
|
||||
messageBody(foo: 123)
|
||||
}
|
||||
}
|
||||
and:
|
||||
StubRunnerStreamMessageSelector predicate = new StubRunnerStreamMessageSelector(
|
||||
dsl)
|
||||
message.headers >> [
|
||||
foo: "fbcc2ed3-dbac-47e7-9a4b-c2d55709792c"
|
||||
]
|
||||
message.payload >> [
|
||||
foo: 123
|
||||
]
|
||||
expect:
|
||||
predicate.accept(message)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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())
|
||||
])
|
||||
|
||||
@@ -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=")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*
|
||||
*/
|
||||
@@ -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[]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,13 +15,7 @@
|
||||
*/ org.springframework.cloud.contract.spec.Contract.make {
|
||||
label 'some_label'
|
||||
input {
|
||||
messageFrom('jms:input')
|
||||
messageBody([
|
||||
bookName: 'foo'
|
||||
])
|
||||
messageHeaders {
|
||||
header('sample', 'header')
|
||||
}
|
||||
triggeredBy("hashCode()")
|
||||
}
|
||||
outputMessage {
|
||||
sentTo('jms:output')
|
||||
|
||||
@@ -15,13 +15,7 @@
|
||||
*/ org.springframework.cloud.contract.spec.Contract.make {
|
||||
label 'some_label'
|
||||
input {
|
||||
messageFrom('jms:input')
|
||||
messageBody([
|
||||
bookName: 'foo'
|
||||
])
|
||||
messageHeaders {
|
||||
header('sample', 'header')
|
||||
}
|
||||
triggeredBy("hashCode()")
|
||||
}
|
||||
outputMessage {
|
||||
sentTo('jms:output')
|
||||
|
||||
@@ -15,13 +15,7 @@
|
||||
*/ org.springframework.cloud.contract.spec.Contract.make {
|
||||
label 'some_label'
|
||||
input {
|
||||
messageFrom('jms:input')
|
||||
messageBody([
|
||||
bookName: 'foo'
|
||||
])
|
||||
messageHeaders {
|
||||
header('sample', 'header')
|
||||
}
|
||||
triggeredBy("hashCode()")
|
||||
}
|
||||
outputMessage {
|
||||
sentTo('jms:output')
|
||||
|
||||
@@ -15,13 +15,7 @@
|
||||
*/ org.springframework.cloud.contract.spec.Contract.make {
|
||||
label 'some_label'
|
||||
input {
|
||||
messageFrom('jms:input')
|
||||
messageBody([
|
||||
bookName: 'foo'
|
||||
])
|
||||
messageHeaders {
|
||||
header('sample', 'header')
|
||||
}
|
||||
triggeredBy("hashCode()")
|
||||
}
|
||||
outputMessage {
|
||||
sentTo('jms:output')
|
||||
|
||||
@@ -80,9 +80,6 @@ class MessagePactCreator {
|
||||
if (input.getTriggeredBy() != null) {
|
||||
return input.getTriggeredBy().getExecutionCommand();
|
||||
}
|
||||
else if (input.getMessageFrom() != null) {
|
||||
return "received message from " + clientValueExtractor.apply(input.getMessageFrom());
|
||||
}
|
||||
else {
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package contracts
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
|
||||
[
|
||||
Contract.make {
|
||||
label 'some_label'
|
||||
input {
|
||||
messageFrom('jms:input')
|
||||
messageBody([
|
||||
bookName: 'foo'
|
||||
])
|
||||
messageHeaders {
|
||||
header('sample', 'header')
|
||||
}
|
||||
}
|
||||
outputMessage {
|
||||
sentTo('jms:output')
|
||||
body([
|
||||
bookName: 'foo'
|
||||
])
|
||||
headers {
|
||||
header('BOOK-NAME', 'foo')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"consumer": {
|
||||
"name": "Consumer"
|
||||
},
|
||||
"provider": {
|
||||
"name": "Provider"
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"description": "message sent to jms:output",
|
||||
"metaData": {
|
||||
"BOOK-NAME": "foo"
|
||||
},
|
||||
"contents": {
|
||||
"bookName": "foo"
|
||||
},
|
||||
"providerStates": [
|
||||
{
|
||||
"name": "received message from jms:input"
|
||||
}
|
||||
],
|
||||
"matchingRules": {
|
||||
"body": {
|
||||
"$.bookName": {
|
||||
"matchers": [
|
||||
{
|
||||
"match": "type"
|
||||
}
|
||||
],
|
||||
"combine": "AND"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"pactSpecification": {
|
||||
"version": "3.0.0"
|
||||
},
|
||||
"pact-jvm": {
|
||||
"version": "4.3.15"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package contracts
|
||||
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
|
||||
[
|
||||
Contract.make {
|
||||
label 'some_label'
|
||||
input {
|
||||
messageFrom('jms:delete')
|
||||
messageBody([
|
||||
bookName: 'foo'
|
||||
])
|
||||
messageHeaders {
|
||||
header('sample', 'header')
|
||||
}
|
||||
assertThat('bookWasDeleted()')
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"consumer": {
|
||||
"name": "Consumer"
|
||||
},
|
||||
"provider": {
|
||||
"name": "Provider"
|
||||
},
|
||||
"messages": [
|
||||
{
|
||||
"description": "assert that bookWasDeleted()",
|
||||
"metaData": {
|
||||
},
|
||||
"providerStates": [
|
||||
{
|
||||
"name": "received message from jms:delete"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"pactSpecification": {
|
||||
"version": "3.0.0"
|
||||
},
|
||||
"pact-jvm": {
|
||||
"version": "4.3.15"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.builder;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.config.TestFramework;
|
||||
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata;
|
||||
|
||||
class JavaMessagingGiven extends MessagingGiven {
|
||||
|
||||
private final GeneratedClassMetaData generatedClassMetaData;
|
||||
|
||||
JavaMessagingGiven(BlockBuilder blockBuilder, GeneratedClassMetaData generatedClassMetaData) {
|
||||
super(blockBuilder, generatedClassMetaData, JavaMessagingBodyParser.INSTANCE);
|
||||
this.generatedClassMetaData = generatedClassMetaData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(SingleContractMetadata metadata) {
|
||||
return super.accept(metadata)
|
||||
&& this.generatedClassMetaData.configProperties.getTestFramework() != TestFramework.SPOCK;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.builder;
|
||||
|
||||
import org.springframework.cloud.contract.spec.internal.FromFileProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.Input;
|
||||
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata;
|
||||
import org.springframework.cloud.contract.verifier.util.ContentType;
|
||||
|
||||
class MessagingBodyGiven implements Given, MethodVisitor<Given> {
|
||||
|
||||
private final BlockBuilder blockBuilder;
|
||||
|
||||
private final BodyReader bodyReader;
|
||||
|
||||
private final BodyParser bodyParser;
|
||||
|
||||
MessagingBodyGiven(BlockBuilder blockBuilder, BodyReader bodyReader, BodyParser bodyParser) {
|
||||
this.blockBuilder = blockBuilder;
|
||||
this.bodyReader = bodyReader;
|
||||
this.bodyParser = bodyParser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor<Given> apply(SingleContractMetadata metadata) {
|
||||
appendBodyGiven(metadata);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void appendBodyGiven(SingleContractMetadata metadata) {
|
||||
ContentType contentType = metadata.getInputTestContentType();
|
||||
Input inputMessage = metadata.getContract().getInput();
|
||||
Object bodyValue = this.bodyParser.extractServerValueFromBody(contentType,
|
||||
inputMessage.getMessageBody().getServerValue());
|
||||
if (bodyValue instanceof FromFileProperty) {
|
||||
FromFileProperty fileProperty = (FromFileProperty) bodyValue;
|
||||
String byteText = fileProperty.isByte()
|
||||
? this.bodyReader.readBytesFromFileString(metadata, fileProperty, CommunicationType.REQUEST)
|
||||
: this.bodyParser.quotedLongText(this.bodyReader.readStringFromFileString(metadata, fileProperty,
|
||||
CommunicationType.REQUEST));
|
||||
this.blockBuilder.addIndented(byteText);
|
||||
}
|
||||
else {
|
||||
String text = this.bodyParser.convertToJsonString(bodyValue);
|
||||
this.blockBuilder.addIndented(this.bodyParser.quotedEscapedLongText(text));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(SingleContractMetadata metadata) {
|
||||
return metadata.getContract().getInput().getMessageBody() != null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.builder;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata;
|
||||
|
||||
class MessagingBodyWhen implements When {
|
||||
|
||||
private final BlockBuilder blockBuilder;
|
||||
|
||||
private final BodyReader bodyReader;
|
||||
|
||||
MessagingBodyWhen(BlockBuilder blockBuilder, GeneratedClassMetaData metaData) {
|
||||
this.blockBuilder = blockBuilder;
|
||||
this.bodyReader = new BodyReader(metaData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor<When> apply(SingleContractMetadata metadata) {
|
||||
this.bodyReader.storeContractAsYaml(metadata);
|
||||
this.blockBuilder
|
||||
.addIndented("contractVerifierMessaging.send(inputMessage, \""
|
||||
+ metadata.getContract().getInput().getMessageFrom().getServerValue() + "\",")
|
||||
.addEmptyLine().indent().addIndented("contract(this, \"" + metadata.methodName() + ".yml\"))")
|
||||
.addEndingIfNotPresent().unindent();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(SingleContractMetadata metadata) {
|
||||
return metadata.getContract().getInput().getMessageFrom() != null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.builder;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata;
|
||||
|
||||
class MessagingGiven implements Given, MethodVisitor<Given>, BodyMethodVisitor {
|
||||
|
||||
private final BlockBuilder blockBuilder;
|
||||
|
||||
private final GeneratedClassMetaData generatedClassMetaData;
|
||||
|
||||
private final List<Given> givens = new LinkedList<>();
|
||||
|
||||
MessagingGiven(BlockBuilder blockBuilder, GeneratedClassMetaData generatedClassMetaData, BodyParser bodyParser) {
|
||||
this.blockBuilder = blockBuilder;
|
||||
this.generatedClassMetaData = generatedClassMetaData;
|
||||
this.givens.addAll(Arrays.asList(
|
||||
new MessagingBodyGiven(this.blockBuilder, new BodyReader(this.generatedClassMetaData), bodyParser),
|
||||
new MessagingHeadersGiven(this.blockBuilder)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor<Given> apply(SingleContractMetadata metadata) {
|
||||
startBodyBlock(this.blockBuilder, "given:");
|
||||
this.blockBuilder.addIndented("ContractVerifierMessage inputMessage = contractVerifierMessaging.create(")
|
||||
.addEmptyLine().indent();
|
||||
this.givens.stream().filter(given -> given.accept(metadata)).forEach(given -> {
|
||||
given.apply(metadata);
|
||||
this.blockBuilder.addEmptyLine();
|
||||
});
|
||||
this.blockBuilder.unindent().unindent().startBlock().addIndented(")").addEndingIfNotPresent().addEmptyLine()
|
||||
.endBlock();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(SingleContractMetadata metadata) {
|
||||
return metadata.isMessaging() && metadata.getContract().getInput().getTriggeredBy() == null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.builder;
|
||||
|
||||
import org.springframework.cloud.contract.spec.internal.Header;
|
||||
import org.springframework.cloud.contract.spec.internal.Input;
|
||||
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata;
|
||||
import org.springframework.cloud.contract.verifier.util.MapConverter;
|
||||
|
||||
class MessagingHeadersGiven implements Given, MethodVisitor<Given> {
|
||||
|
||||
private final BlockBuilder blockBuilder;
|
||||
|
||||
MessagingHeadersGiven(BlockBuilder blockBuilder) {
|
||||
this.blockBuilder = blockBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor<Given> apply(SingleContractMetadata metadata) {
|
||||
Input inputMessage = metadata.getContract().getInput();
|
||||
this.blockBuilder.startBlock().addIndented(", headers()").startBlock();
|
||||
inputMessage.getMessageHeaders().executeForEachHeader(header -> {
|
||||
this.blockBuilder.addEmptyLine().addIndented(getHeaderString(header));
|
||||
});
|
||||
this.blockBuilder.endBlock();
|
||||
return this;
|
||||
}
|
||||
|
||||
private String getHeaderString(Header header) {
|
||||
return ".header(" + getTestSideValue(header.getName()) + ", " + getTestSideValue(header.getServerValue()) + ")";
|
||||
}
|
||||
|
||||
private String getTestSideValue(Object object) {
|
||||
return '"' + MapConverter.getTestSideValues(object).toString() + '"';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(SingleContractMetadata metadata) {
|
||||
return metadata.getContract().getInput().getMessageHeaders() != null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,10 +28,9 @@ class MessagingWhen implements When, BodyMethodVisitor {
|
||||
|
||||
private final List<When> whens = new LinkedList<>();
|
||||
|
||||
MessagingWhen(BlockBuilder blockBuilder, GeneratedClassMetaData generatedClassMetaData) {
|
||||
MessagingWhen(BlockBuilder blockBuilder) {
|
||||
this.blockBuilder = blockBuilder;
|
||||
this.whens.addAll(Arrays.asList(new MessagingTriggeredByWhen(this.blockBuilder),
|
||||
new MessagingBodyWhen(this.blockBuilder, generatedClassMetaData),
|
||||
new MessagingAssertThatWhen(this.blockBuilder)));
|
||||
}
|
||||
|
||||
|
||||
@@ -117,9 +117,7 @@ class SingleMethodBuilder {
|
||||
|
||||
SingleMethodBuilder messaging() {
|
||||
// @formatter:off
|
||||
return given(new JavaMessagingGiven(this.blockBuilder, this.generatedClassMetaData))
|
||||
.given(new SpockMessagingGiven(this.blockBuilder, this.generatedClassMetaData))
|
||||
.when(new MessagingWhen(this.blockBuilder, this.generatedClassMetaData))
|
||||
return when(new MessagingWhen(this.blockBuilder))
|
||||
.then(new JavaMessagingWithBodyThen(this.blockBuilder,
|
||||
this.generatedClassMetaData))
|
||||
.then(new SpockMessagingWithBodyThen(this.blockBuilder,
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.contract.verifier.builder;
|
||||
|
||||
import org.springframework.cloud.contract.verifier.config.TestFramework;
|
||||
import org.springframework.cloud.contract.verifier.file.SingleContractMetadata;
|
||||
|
||||
class SpockMessagingGiven extends MessagingGiven {
|
||||
|
||||
private final GeneratedClassMetaData generatedClassMetaData;
|
||||
|
||||
SpockMessagingGiven(BlockBuilder blockBuilder, GeneratedClassMetaData generatedClassMetaData) {
|
||||
super(blockBuilder, generatedClassMetaData, SpockMessagingBodyParser.INSTANCE);
|
||||
this.generatedClassMetaData = generatedClassMetaData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean accept(SingleContractMetadata metadata) {
|
||||
return super.accept(metadata)
|
||||
&& this.generatedClassMetaData.configProperties.getTestFramework() == TestFramework.SPOCK;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -367,7 +367,6 @@ class ContractsToYaml {
|
||||
protected void input(Contract contract, YamlContract yamlContract) {
|
||||
Input input = contract.getInput();
|
||||
if (input != null) {
|
||||
ContentType contentType = evaluateClientSideContentType(input.getMessageHeaders(), input.getMessageBody());
|
||||
yamlContract.input = new YamlContract.Input();
|
||||
yamlContract.input.assertThat = Optional.ofNullable(input.getAssertThat())
|
||||
.map(assertThat -> MapConverter
|
||||
@@ -377,26 +376,6 @@ class ContractsToYaml {
|
||||
.map(triggeredBy -> MapConverter
|
||||
.getTestSideValues(triggeredBy.toString(), MapConverter.JSON_PARSING_FUNCTION).toString())
|
||||
.orElse(null);
|
||||
yamlContract.input.messageHeaders = input.getMessageHeaders().asTestSideMap();
|
||||
yamlContract.input.messageBody = MapConverter.getTestSideValues(input.getMessageBody(),
|
||||
MapConverter.JSON_PARSING_FUNCTION);
|
||||
yamlContract.input.messageFrom = Optional
|
||||
.ofNullable(input.getMessageFrom()).map(messageFrom -> MapConverter
|
||||
.getTestSideValues(messageFrom, MapConverter.JSON_PARSING_FUNCTION).toString())
|
||||
.orElse(null);
|
||||
Optional.ofNullable(input.getBodyMatchers()).map(BodyMatchers::matchers)
|
||||
.ifPresent(bodyMatchers -> bodyMatchers.forEach(bodyMatcher -> {
|
||||
YamlContract.BodyStubMatcher bodyStubMatcher = new YamlContract.BodyStubMatcher();
|
||||
bodyStubMatcher.path = bodyMatcher.path();
|
||||
bodyStubMatcher.type = stubMatcherType(bodyMatcher.matchingType());
|
||||
bodyStubMatcher.value = Optional.ofNullable(bodyMatcher.value()).map(Object::toString)
|
||||
.orElse(null);
|
||||
yamlContract.input.matchers.body.add(bodyStubMatcher);
|
||||
}));
|
||||
if (XML != contentType) {
|
||||
setInputBodyMatchers(input.getMessageBody(), yamlContract.input.matchers.body);
|
||||
}
|
||||
setInputHeadersMatchers(input.getMessageHeaders(), yamlContract.input.matchers.headers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -733,22 +733,10 @@ public class YamlContract {
|
||||
|
||||
public static class Input {
|
||||
|
||||
public String messageFrom;
|
||||
|
||||
public String triggeredBy;
|
||||
|
||||
public Map<String, Object> messageHeaders = new LinkedHashMap<String, Object>();
|
||||
|
||||
public Object messageBody;
|
||||
|
||||
public String messageBodyFromFile;
|
||||
|
||||
public String messageBodyFromFileAsBytes;
|
||||
|
||||
public String assertThat;
|
||||
|
||||
public StubMatchers matchers = new StubMatchers();
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
@@ -758,26 +746,17 @@ public class YamlContract {
|
||||
return false;
|
||||
}
|
||||
Input input = (Input) o;
|
||||
return Objects.equals(messageFrom, input.messageFrom) && Objects.equals(triggeredBy, input.triggeredBy)
|
||||
&& Objects.equals(messageHeaders, input.messageHeaders)
|
||||
&& Objects.equals(messageBody, input.messageBody)
|
||||
&& Objects.equals(messageBodyFromFile, input.messageBodyFromFile)
|
||||
&& Objects.equals(messageBodyFromFileAsBytes, input.messageBodyFromFileAsBytes)
|
||||
&& Objects.equals(assertThat, input.assertThat) && Objects.equals(matchers, input.matchers);
|
||||
return Objects.equals(triggeredBy, input.triggeredBy) && Objects.equals(assertThat, input.assertThat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(messageFrom, triggeredBy, messageHeaders, messageBody, messageBodyFromFile,
|
||||
messageBodyFromFileAsBytes, assertThat, matchers);
|
||||
return Objects.hash(triggeredBy, assertThat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Input{" + "messageFrom='" + messageFrom + '\'' + ", triggeredBy='" + triggeredBy + '\''
|
||||
+ ", messageHeaders=" + messageHeaders + ", messageBody=" + messageBody + ", messageBodyFromFile='"
|
||||
+ messageBodyFromFile + '\'' + ", messageBodyFromFileAsBytes='" + messageBodyFromFileAsBytes + '\''
|
||||
+ ", assertThat='" + assertThat + '\'' + ", matchers=" + matchers + '}';
|
||||
return "Input{" + "triggeredBy='" + triggeredBy + '\'' + ", assertThat='" + assertThat + '\'' + '}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -661,23 +661,13 @@ class YamlToContracts {
|
||||
YamlContract.Input yamlContractInput = yamlContract.input;
|
||||
if (yamlContractInput != null) {
|
||||
dslContract.input(dslContractInput -> {
|
||||
mapInputMessageFrom(yamlContractInput, dslContractInput);
|
||||
mapInputAssertThat(yamlContractInput, dslContractInput);
|
||||
mapInputTriggeredBy(yamlContractInput, dslContractInput);
|
||||
mapInputMessageHeaders(yamlContractInput, dslContractInput);
|
||||
mapInputMessageBody(yamlContractInput, dslContractInput);
|
||||
mapInputBodyMatchers(yamlContractInput, dslContractInput);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void mapInputMessageFrom(YamlContract.Input yamlContractInput, Input dslContractInput) {
|
||||
if (yamlContractInput.messageFrom != null) {
|
||||
dslContractInput.messageFrom(yamlContractInput.messageFrom);
|
||||
}
|
||||
}
|
||||
|
||||
private void mapInputAssertThat(YamlContract.Input yamlContractInput, Input dslContractInput) {
|
||||
if (yamlContractInput.assertThat != null) {
|
||||
dslContractInput.assertThat(yamlContractInput.assertThat);
|
||||
@@ -690,80 +680,6 @@ class YamlToContracts {
|
||||
}
|
||||
}
|
||||
|
||||
private void mapInputMessageHeaders(YamlContract.Input yamlContractInput, Input dslContractInput) {
|
||||
dslContractInput
|
||||
.messageHeaders(dslContractMessageHeaders -> Optional.ofNullable(yamlContractInput.messageHeaders)
|
||||
.ifPresent(yamlContractMessageHeaders -> yamlContractMessageHeaders.forEach((key, value) -> {
|
||||
YamlContract.KeyValueMatcher matcher = Optional.ofNullable(yamlContractInput.matchers)
|
||||
.map(yamlContractInputMatchers -> yamlContractInputMatchers.headers)
|
||||
.flatMap(yamlContractInputMatchersHeaders -> yamlContractInputMatchersHeaders
|
||||
.stream()
|
||||
.filter(yamlContractInputMatchersHeader -> yamlContractInputMatchersHeader.key
|
||||
.equals(key))
|
||||
.findFirst())
|
||||
.orElse(null);
|
||||
dslContractMessageHeaders.header(key, clientValue(value, matcher, key));
|
||||
})));
|
||||
}
|
||||
|
||||
private void mapInputMessageBody(YamlContract.Input yamlContractInput, Input dslContractInput) {
|
||||
if (yamlContractInput.messageBody != null) {
|
||||
dslContractInput.messageBody(yamlContractInput.messageBody);
|
||||
}
|
||||
if (yamlContractInput.messageBodyFromFile != null) {
|
||||
dslContractInput.messageBody(file(yamlContractInput.messageBodyFromFile));
|
||||
}
|
||||
if (yamlContractInput.messageBodyFromFileAsBytes != null) {
|
||||
dslContractInput.messageBody(dslContractInput.fileAsBytes(yamlContractInput.messageBodyFromFileAsBytes));
|
||||
}
|
||||
}
|
||||
|
||||
private void mapInputBodyMatchers(YamlContract.Input yamlContractInput, Input dslContractInput) {
|
||||
dslContractInput
|
||||
.bodyMatchers(dslContractInputBodyMatchers -> Optional.ofNullable(yamlContractInput.matchers.body)
|
||||
.ifPresent(yamlContractBodyStubMatchers -> yamlContractBodyStubMatchers
|
||||
.forEach(yamlContractBodyStubMatcher -> {
|
||||
ContentType contentType = evaluateClientSideContentType(
|
||||
yamlHeadersToContractHeaders(
|
||||
Optional.ofNullable(yamlContractInput.messageHeaders)
|
||||
.orElse(new HashMap<>())),
|
||||
Optional.ofNullable(yamlContractInput.messageBody).orElse(null));
|
||||
MatchingTypeValue value;
|
||||
switch (yamlContractBodyStubMatcher.type) {
|
||||
case by_date:
|
||||
value = dslContractInputBodyMatchers.byDate();
|
||||
break;
|
||||
case by_time:
|
||||
value = dslContractInputBodyMatchers.byTime();
|
||||
break;
|
||||
case by_timestamp:
|
||||
value = dslContractInputBodyMatchers.byTimestamp();
|
||||
break;
|
||||
case by_regex:
|
||||
String regex = yamlContractBodyStubMatcher.value;
|
||||
if (yamlContractBodyStubMatcher.predefined != null) {
|
||||
regex = predefinedToPattern(yamlContractBodyStubMatcher.predefined)
|
||||
.pattern();
|
||||
}
|
||||
value = dslContractInputBodyMatchers.byRegex(regex);
|
||||
break;
|
||||
case by_equality:
|
||||
value = dslContractInputBodyMatchers.byEquality();
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("The type " + "["
|
||||
+ yamlContractBodyStubMatcher.type + "] is unsupported. "
|
||||
+ "Hint: If you're using <predefined> remember to pass < type:by_regex > ");
|
||||
}
|
||||
if (XML == contentType) {
|
||||
dslContractInputBodyMatchers.xPath(yamlContractBodyStubMatcher.path, value);
|
||||
}
|
||||
else {
|
||||
dslContractInputBodyMatchers.jsonPath(yamlContractBodyStubMatcher.path, value);
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
private Headers yamlHeadersToContractHeaders(Map<String, Object> headers) {
|
||||
Set<Header> convertedHeaders = headers.keySet().stream()
|
||||
.map(header -> Header.build(header, headers.get(header))).collect(toSet());
|
||||
|
||||
@@ -29,7 +29,6 @@ import org.springframework.cloud.contract.spec.Contract;
|
||||
import org.springframework.cloud.contract.spec.internal.DslProperty;
|
||||
import org.springframework.cloud.contract.spec.internal.Header;
|
||||
import org.springframework.cloud.contract.spec.internal.Headers;
|
||||
import org.springframework.cloud.contract.spec.internal.Input;
|
||||
import org.springframework.cloud.contract.spec.internal.OutputMessage;
|
||||
import org.springframework.cloud.contract.spec.internal.Request;
|
||||
import org.springframework.cloud.contract.spec.internal.Response;
|
||||
@@ -183,12 +182,11 @@ public class SingleContractMetadata {
|
||||
|
||||
private DslProperty<?> inputBody(Contract contract) {
|
||||
return Optional.ofNullable(contract.getRequest()).map(Request::getBody).map(DslProperty.class::cast)
|
||||
.orElseGet(() -> Optional.ofNullable(contract.getInput()).map(Input::getMessageBody).orElse(null));
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private Headers inputHeaders(Contract contract) {
|
||||
return Optional.ofNullable(contract.getRequest()).map(Request::getHeaders)
|
||||
.orElseGet(() -> Optional.ofNullable(contract.getInput()).map(Input::getMessageHeaders).orElse(null));
|
||||
return Optional.ofNullable(contract.getRequest()).map(Request::getHeaders).orElse(null);
|
||||
}
|
||||
|
||||
private DslProperty<?> outputBody(Contract contract) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user