Added support for Spring Kafka
fixes gh-877
This commit is contained in:
@@ -58,6 +58,7 @@
|
||||
<module>spring-cloud-contract-stub-runner-stream</module>
|
||||
<module>spring-cloud-contract-stub-runner-amqp</module>
|
||||
<module>spring-cloud-contract-stub-runner-jms</module>
|
||||
<module>spring-cloud-contract-stub-runner-kafka</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
@@ -83,6 +84,7 @@
|
||||
<module>spring-cloud-contract-stub-runner-stream</module>
|
||||
<module>spring-cloud-contract-stub-runner-amqp</module>
|
||||
<module>spring-cloud-contract-stub-runner-jms</module>
|
||||
<module>spring-cloud-contract-stub-runner-kafka</module>
|
||||
</modules>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
82
tests/spring-cloud-contract-stub-runner-kafka/pom.xml
Normal file
82
tests/spring-cloud-contract-stub-runner-kafka/pom.xml
Normal file
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-contract-tests</artifactId>
|
||||
<version>2.2.0.BUILD-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
<artifactId>spring-cloud-contract-stub-runner-kafka</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<name>Spring Cloud Contract Stub Runner JMS</name>
|
||||
<description>Spring Cloud Contract Stub Runner JMS</description>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-contract-stub-runner</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-contract-stub-runner-jetty</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
<artifactId>spring-kafka</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
<artifactId>spring-kafka-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.codehaus.groovy</groupId>
|
||||
<artifactId>groovy</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.spockframework</groupId>
|
||||
<artifactId>spock-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.spockframework</groupId>
|
||||
<artifactId>spock-spring</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>info.solidsoft.spock</groupId>
|
||||
<artifactId>spock-global-unroll</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.gmavenplus</groupId>
|
||||
<artifactId>gmavenplus-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2013-2019 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 groovy.transform.CompileStatic
|
||||
import groovy.transform.EqualsAndHashCode
|
||||
|
||||
@CompileStatic
|
||||
@EqualsAndHashCode
|
||||
class BookReturned implements Serializable {
|
||||
String bookName
|
||||
|
||||
BookReturned(String bookName) {
|
||||
this.bookName = bookName
|
||||
}
|
||||
|
||||
BookReturned() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
* Copyright 2013-2019 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.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
import groovy.json.JsonOutput
|
||||
import groovy.json.JsonSlurper
|
||||
import spock.lang.IgnoreIf
|
||||
import spock.lang.Specification
|
||||
import spock.util.concurrent.PollingConditions
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
|
||||
import org.springframework.boot.test.context.SpringBootContextLoader
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.cloud.contract.spec.Contract
|
||||
import org.springframework.cloud.contract.stubrunner.StubFinder
|
||||
import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.kafka.annotation.EnableKafka
|
||||
import org.springframework.kafka.annotation.KafkaListener
|
||||
import org.springframework.kafka.core.KafkaTemplate
|
||||
import org.springframework.kafka.support.DefaultKafkaHeaderMapper
|
||||
import org.springframework.kafka.test.EmbeddedKafkaBroker
|
||||
import org.springframework.kafka.test.context.EmbeddedKafka
|
||||
import org.springframework.messaging.Message
|
||||
import org.springframework.messaging.MessageHeaders
|
||||
import org.springframework.messaging.support.MessageBuilder
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
|
||||
@SpringBootTest(properties = ["debug=true"])
|
||||
@AutoConfigureStubRunner
|
||||
@IgnoreIf({ os.windows })
|
||||
@EmbeddedKafka(topics = ["input", "output", "delete"])
|
||||
class KafkaStubRunnerSpec extends Specification {
|
||||
|
||||
@Autowired
|
||||
StubFinder stubFinder
|
||||
@Autowired
|
||||
KafkaTemplate kafkaTemplate
|
||||
@Autowired
|
||||
EmbeddedKafkaBroker broker
|
||||
@Value('${spring.embedded.kafka.brokers}')
|
||||
String brokers
|
||||
@Autowired
|
||||
MyMessageListener myMessageListener
|
||||
PollingConditions await = new PollingConditions()
|
||||
|
||||
def setup() {
|
||||
this.myMessageListener.clear()
|
||||
}
|
||||
|
||||
private Message receiveFromOutput() {
|
||||
this.myMessageListener.await()
|
||||
return this.myMessageListener.output
|
||||
}
|
||||
|
||||
def 'should download the stub and register a route for it'() {
|
||||
when:
|
||||
// tag::client_send[]
|
||||
Message message = MessageBuilder.createMessage(new BookReturned('foo'), new MessageHeaders([sample: "header",]))
|
||||
kafkaTemplate.setDefaultTopic('input')
|
||||
kafkaTemplate.send(message)
|
||||
// end::client_send[]
|
||||
then:
|
||||
// tag::client_receive[]
|
||||
Message receivedMessage = receiveFromOutput()
|
||||
// end::client_receive[]
|
||||
and:
|
||||
await.eventually {
|
||||
// tag::client_receive_message[]
|
||||
assert receivedMessage != null
|
||||
assert assertThatBodyContainsBookNameFoo(receivedMessage.getPayload())
|
||||
assert receivedMessage.getHeaders().get('BOOK-NAME') == 'foo'
|
||||
// end::client_receive_message[]
|
||||
}
|
||||
}
|
||||
|
||||
def 'should trigger a message by label'() {
|
||||
when:
|
||||
// tag::client_trigger[]
|
||||
stubFinder.trigger('return_book_1')
|
||||
// end::client_trigger[]
|
||||
then:
|
||||
// tag::client_trigger_receive[]
|
||||
Message receivedMessage = receiveFromOutput()
|
||||
// end::client_trigger_receive[]
|
||||
and:
|
||||
await.eventually {
|
||||
// tag::client_trigger_message[]
|
||||
assert receivedMessage != null
|
||||
assert assertThatBodyContainsBookNameFoo(receivedMessage.getPayload())
|
||||
assert receivedMessage.getHeaders().get('BOOK-NAME') == 'foo'
|
||||
// end::client_trigger_message[]
|
||||
}
|
||||
}
|
||||
|
||||
def 'should trigger a label for the existing groupId:artifactId'() {
|
||||
when:
|
||||
// tag::trigger_group_artifact[]
|
||||
stubFinder.
|
||||
trigger('my:stubs', 'return_book_1')
|
||||
// end::trigger_group_artifact[]
|
||||
then:
|
||||
Message receivedMessage = receiveFromOutput()
|
||||
and:
|
||||
await.eventually {
|
||||
assert receivedMessage != null
|
||||
assert assertThatBodyContainsBookNameFoo(receivedMessage.getPayload())
|
||||
assert receivedMessage.getHeaders().get('BOOK-NAME') == 'foo'
|
||||
}
|
||||
}
|
||||
|
||||
def 'should trigger a label for the existing artifactId'() {
|
||||
when:
|
||||
// tag::trigger_artifact[]
|
||||
stubFinder.trigger('stubs', 'return_book_1')
|
||||
// end::trigger_artifact[]
|
||||
then:
|
||||
Message receivedMessage = receiveFromOutput()
|
||||
and:
|
||||
await.eventually {
|
||||
assert receivedMessage != null
|
||||
assert assertThatBodyContainsBookNameFoo(receivedMessage.getPayload())
|
||||
assert receivedMessage.getHeaders().get('BOOK-NAME') == 'foo'
|
||||
}
|
||||
}
|
||||
|
||||
def 'should throw an exception when missing label is passed'() {
|
||||
when:
|
||||
stubFinder.trigger('missing label')
|
||||
then:
|
||||
thrown(IllegalArgumentException)
|
||||
}
|
||||
|
||||
def 'should throw an exception when missing label and artifactid is passed'() {
|
||||
when:
|
||||
stubFinder.trigger('some:service', 'return_book_1')
|
||||
then:
|
||||
thrown(IllegalArgumentException)
|
||||
}
|
||||
|
||||
def 'should trigger messages by running all triggers'() {
|
||||
when:
|
||||
// tag::trigger_all[]
|
||||
stubFinder.trigger()
|
||||
// end::trigger_all[]
|
||||
then:
|
||||
Message receivedMessage = receiveFromOutput()
|
||||
and:
|
||||
await.eventually {
|
||||
assert receivedMessage != null
|
||||
assert assertThatBodyContainsBookNameFoo(receivedMessage.getPayload())
|
||||
assert receivedMessage.getHeaders().get('BOOK-NAME') == 'foo'
|
||||
}
|
||||
}
|
||||
|
||||
def 'should trigger a label with no output message'() {
|
||||
when:
|
||||
// tag::trigger_no_output[]
|
||||
Message message = MessageBuilder.createMessage(new BookReturned('foo'), new MessageHeaders([sample: "header",]))
|
||||
kafkaTemplate.setDefaultTopic('delete')
|
||||
kafkaTemplate.send(message)
|
||||
// end::trigger_no_output[]
|
||||
then:
|
||||
noExceptionThrown()
|
||||
}
|
||||
|
||||
def 'should not trigger a message that does not match input'() {
|
||||
when:
|
||||
Message message = MessageBuilder.createMessage(new BookReturned('notmatching'), new MessageHeaders([wrong: "header",]))
|
||||
kafkaTemplate.setDefaultTopic('input')
|
||||
kafkaTemplate.send(message)
|
||||
then:
|
||||
Message receivedMessage = receiveFromOutput()
|
||||
and:
|
||||
receivedMessage == null
|
||||
}
|
||||
|
||||
private boolean assertThatBodyContainsBookNameFoo(Object payload) {
|
||||
println "Got payload [" + payload + "]"
|
||||
String objectAsString = payload instanceof String ? payload :
|
||||
JsonOutput.toJson(payload)
|
||||
def json = new JsonSlurper().parseText(objectAsString)
|
||||
if (objectAsString.contains("payload")) {
|
||||
return json.payload.bookName == 'foo'
|
||||
}
|
||||
return json.bookName == 'foo'
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@ComponentScan
|
||||
@EnableAutoConfiguration
|
||||
@EnableKafka
|
||||
static class Config {
|
||||
|
||||
@Bean
|
||||
DefaultKafkaHeaderMapper headerMapper() {
|
||||
return new DefaultKafkaHeaderMapper();
|
||||
}
|
||||
|
||||
@Bean
|
||||
MyMessageListener myMessageListener() {
|
||||
return new MyMessageListener();
|
||||
}
|
||||
}
|
||||
|
||||
static class MyMessageListener {
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1)
|
||||
|
||||
Message output
|
||||
|
||||
@KafkaListener(topics = ["output"])
|
||||
void output(Message message) {
|
||||
println "I got the message [${message}]"
|
||||
this.output = message
|
||||
this.latch.countDown()
|
||||
}
|
||||
|
||||
void clear() {
|
||||
this.output = null
|
||||
this.latch = new CountDownLatch(1)
|
||||
}
|
||||
|
||||
void await() {
|
||||
this.latch.await(5, TimeUnit.SECONDS)
|
||||
}
|
||||
}
|
||||
|
||||
Contract dsl =
|
||||
// tag::sample_dsl[]
|
||||
Contract.make {
|
||||
label 'return_book_1'
|
||||
input {
|
||||
triggeredBy('bookReturnedTriggered()')
|
||||
}
|
||||
outputMessage {
|
||||
sentTo('output')
|
||||
body('''{ "bookName" : "foo" }''')
|
||||
headers {
|
||||
header('BOOK-NAME', 'foo')
|
||||
}
|
||||
}
|
||||
}
|
||||
// end::sample_dsl[]
|
||||
|
||||
Contract dsl2 =
|
||||
// tag::sample_dsl_2[]
|
||||
Contract.make {
|
||||
label 'return_book_2'
|
||||
input {
|
||||
messageFrom('input')
|
||||
messageBody([
|
||||
bookName: 'foo'
|
||||
])
|
||||
messageHeaders {
|
||||
header('sample', 'header')
|
||||
}
|
||||
}
|
||||
outputMessage {
|
||||
sentTo('output')
|
||||
body([
|
||||
bookName: 'foo'
|
||||
])
|
||||
headers {
|
||||
header('BOOK-NAME', 'foo')
|
||||
}
|
||||
}
|
||||
}
|
||||
// end::sample_dsl_2[]
|
||||
|
||||
Contract dsl3 =
|
||||
// tag::sample_dsl_3[]
|
||||
Contract.make {
|
||||
label 'delete_book'
|
||||
input {
|
||||
messageFrom('delete')
|
||||
messageBody([
|
||||
bookName: 'foo'
|
||||
])
|
||||
messageHeaders {
|
||||
header('sample', 'header')
|
||||
}
|
||||
assertThat('bookWasDeleted()')
|
||||
}
|
||||
}
|
||||
// end::sample_dsl_3[]
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
stubrunner:
|
||||
repository-root: stubs:classpath:/stubs/
|
||||
ids: my:stubs
|
||||
stubs-mode: remote
|
||||
spring:
|
||||
kafka:
|
||||
bootstrap-servers: ${spring.embedded.kafka.brokers}
|
||||
producer:
|
||||
properties:
|
||||
"value.serializer": "org.springframework.kafka.support.serializer.JsonSerializer"
|
||||
"spring.json.trusted.packages": "*"
|
||||
consumer:
|
||||
properties:
|
||||
"value.deserializer": "org.springframework.kafka.support.serializer.JsonDeserializer"
|
||||
"value.serializer": "org.springframework.kafka.support.serializer.JsonSerializer"
|
||||
"spring.json.trusted.packages": "*"
|
||||
group-id: groupId
|
||||
server:
|
||||
port: 0
|
||||
debug: true
|
||||
logging.level.org.springframework.cloud.contract: debug
|
||||
@@ -0,0 +1,13 @@
|
||||
org.springframework.cloud.contract.spec.Contract.make {
|
||||
label 'delete_book'
|
||||
input {
|
||||
messageFrom('delete')
|
||||
messageBody([
|
||||
bookName: 'foo'
|
||||
])
|
||||
messageHeaders {
|
||||
header('sample', 'header')
|
||||
}
|
||||
assertThat('bookWasDeleted()')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
org.springframework.cloud.contract.spec.Contract.make {
|
||||
label 'return_book_1'
|
||||
input {
|
||||
triggeredBy('bookReturnedTriggered()')
|
||||
}
|
||||
outputMessage {
|
||||
sentTo('output')
|
||||
body('''{ "bookName" : "foo" }''')
|
||||
headers {
|
||||
header('BOOK-NAME', 'foo')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
org.springframework.cloud.contract.spec.Contract.make {
|
||||
label 'return_book_2'
|
||||
input {
|
||||
messageFrom('input')
|
||||
messageBody([
|
||||
bookName: 'foo'
|
||||
])
|
||||
messageHeaders {
|
||||
header('sample', 'header')
|
||||
}
|
||||
}
|
||||
outputMessage {
|
||||
sentTo('output')
|
||||
body([
|
||||
bookName: 'foo'
|
||||
])
|
||||
headers {
|
||||
header('BOOK-NAME', 'foo')
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user