diff --git a/README.md b/README.md
index e69de29..162ad6a 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +1 @@
+pact-messaging-app
diff --git a/jenkins/cd/Jenkinsfile b/jenkins/cd/Jenkinsfile
new file mode 100644
index 0000000..af03390
--- /dev/null
+++ b/jenkins/cd/Jenkinsfile
@@ -0,0 +1,44 @@
+#!groovy
+pipeline {
+
+ agent any
+
+ environment {
+ BRANCH_NAME=env.GIT_BRANCH.replace("origin/", "")
+ }
+
+ stages {
+ stage('Build') {
+ steps {
+ dir('messaging-app') {
+ sh '../mvnw clean verify'
+ }
+ }
+ }
+ stage('Publish Pacts') {
+ steps {
+ dir('messaging-app') {
+ sh '../mvnw pact:publish -Dpact.consumer.version=${GIT_COMMIT} -Dpact.tag=${BRANCH_NAME}'
+ }
+ }
+ }
+ stage('Check Pact Verifications') {
+ steps {
+ sh 'curl -LO https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v1.88.3/pact-1.88.3-linux-x86_64.tar.gz'
+ sh 'tar xzf pact-1.88.3-linux-x86_64.tar.gz'
+ dir('pact/bin') {
+ sh "./pact-broker can-i-deploy --retry-while-unknown=12 --retry-interval=10 -a messaging-app -b http://pact_broker -e ${GIT_COMMIT}"
+ }
+ }
+ }
+ stage('Deploy') {
+ when {
+ branch 'junit5'
+ }
+ steps {
+ echo 'Deploying to prod now...'
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/jenkins/without-cd/Jenkinsfile-build b/jenkins/without-cd/Jenkinsfile-build
new file mode 100644
index 0000000..6a97182
--- /dev/null
+++ b/jenkins/without-cd/Jenkinsfile-build
@@ -0,0 +1,27 @@
+#!groovy
+pipeline {
+
+ agent any
+
+ environment {
+ BRANCH_NAME=env.GIT_BRANCH.replace("origin/", "")
+ }
+
+ stages {
+ stage('Build') {
+ steps {
+ dir('messaging-app') {
+ sh '../mvnw clean verify'
+ }
+ }
+ }
+ stage('Publish Pacts') {
+ steps {
+ dir('messaging-app') {
+ sh '../mvnw pact:publish -Dpact.consumer.version=${GIT_COMMIT} -Dpact.tag=${BRANCH_NAME}'
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/jenkins/without-cd/Jenkinsfile-deploy b/jenkins/without-cd/Jenkinsfile-deploy
new file mode 100644
index 0000000..c1ed3ca
--- /dev/null
+++ b/jenkins/without-cd/Jenkinsfile-deploy
@@ -0,0 +1,38 @@
+#!groovy
+pipeline {
+
+ agent any
+
+ parameters {
+ string(name: 'GIT_COMMIT', defaultValue: '', description: 'Version (a.k.a. git commit) to deploy')
+ }
+
+ options {
+ skipDefaultCheckout()
+ }
+
+ stages {
+ stage('Check Pact Verifications') {
+ steps {
+ sh 'curl -LO https://github.com/pact-foundation/pact-ruby-standalone/releases/download/v1.88.3/pact-1.88.3-linux-x86_64.tar.gz'
+ sh 'tar xzf pact-1.88.3-linux-x86_64.tar.gz'
+ dir('pact/bin') {
+ sh "./pact-broker can-i-deploy -a messaging-app -b http://pact_broker -e ${GIT_COMMIT} --to prod"
+ }
+ }
+ }
+ stage('Deploy') {
+ steps {
+ echo 'Deploying to prod now...'
+ }
+ }
+ stage('Tag Pact') {
+ steps {
+ dir('pact/bin') {
+ sh "./pact-broker create-version-tag -a messaging-app -b http://pact_broker -e ${GIT_COMMIT} -t prod"
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/lombok.config b/lombok.config
new file mode 100644
index 0000000..15d5dd7
--- /dev/null
+++ b/lombok.config
@@ -0,0 +1,2 @@
+config.stopBubbling = true
+lombok.noArgsConstructor.extraPrivate = true
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..0525d98
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,88 @@
+
+
+ 4.0.0
+
+ de.kreuzwerker.cdc
+ messaging-app
+ 1.0.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.4.RELEASE
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ 4.0.7
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ junit
+ junit
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit-jupiter.version}
+ test
+
+
+
+ au.com.dius
+ pact-jvm-consumer-java8
+ ${pact.version}
+ test
+
+
+ au.com.dius
+ pact-jvm-consumer-junit5
+ ${pact.version}
+ test
+
+
+
+
+
+
+ au.com.dius
+ pact-jvm-provider-maven
+ ${pact.version}
+
+
+ http://pact_broker
+ ${pact.consumer.version}
+
+ ${pact.tag}
+
+
+
+
+
+
+
+
diff --git a/src/main/java/de/kreuzwerker/cdc/messagingapp/Friend.java b/src/main/java/de/kreuzwerker/cdc/messagingapp/Friend.java
new file mode 100644
index 0000000..0e76c8e
--- /dev/null
+++ b/src/main/java/de/kreuzwerker/cdc/messagingapp/Friend.java
@@ -0,0 +1,11 @@
+package de.kreuzwerker.cdc.messagingapp;
+
+import lombok.Data;
+
+@Data
+public class Friend {
+
+ private String id;
+ private String name;
+
+}
diff --git a/src/main/java/de/kreuzwerker/cdc/messagingapp/MessagingApplication.java b/src/main/java/de/kreuzwerker/cdc/messagingapp/MessagingApplication.java
new file mode 100644
index 0000000..9b6f654
--- /dev/null
+++ b/src/main/java/de/kreuzwerker/cdc/messagingapp/MessagingApplication.java
@@ -0,0 +1,15 @@
+package de.kreuzwerker.cdc.messagingapp;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+@Slf4j
+public class MessagingApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(MessagingApplication.class, args);
+ }
+
+}
diff --git a/src/main/java/de/kreuzwerker/cdc/messagingapp/User.java b/src/main/java/de/kreuzwerker/cdc/messagingapp/User.java
new file mode 100644
index 0000000..592a28d
--- /dev/null
+++ b/src/main/java/de/kreuzwerker/cdc/messagingapp/User.java
@@ -0,0 +1,17 @@
+package de.kreuzwerker.cdc.messagingapp;
+
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+public class User {
+
+ private String name;
+ private String role;
+ private LocalDateTime lastLogin;
+ private List friends;
+
+
+}
diff --git a/src/main/java/de/kreuzwerker/cdc/messagingapp/UserServiceClient.java b/src/main/java/de/kreuzwerker/cdc/messagingapp/UserServiceClient.java
new file mode 100644
index 0000000..df7b67c
--- /dev/null
+++ b/src/main/java/de/kreuzwerker/cdc/messagingapp/UserServiceClient.java
@@ -0,0 +1,24 @@
+package de.kreuzwerker.cdc.messagingapp;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+import org.springframework.web.client.RestTemplate;
+
+@Component
+public class UserServiceClient {
+
+ private final RestTemplate restTemplate;
+
+ public UserServiceClient(@Value("${user-service.base-url}") String baseUrl) {
+ this.restTemplate = new RestTemplateBuilder().rootUri(baseUrl).build();
+ }
+
+ public User getUser(String id) {
+ final User user = restTemplate.getForObject("/users/" + id, User.class);
+ Assert.hasText(user.getName(), "Name is blank.");
+ return user;
+ }
+
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..4d30c5d
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,2 @@
+user-service:
+ base-url: "http://localhost:8090"
\ No newline at end of file
diff --git a/src/test/java/de/kreuzwerker/cdc/messagingapp/MessagingApplicationTests.java b/src/test/java/de/kreuzwerker/cdc/messagingapp/MessagingApplicationTests.java
new file mode 100644
index 0000000..ef7bbcc
--- /dev/null
+++ b/src/test/java/de/kreuzwerker/cdc/messagingapp/MessagingApplicationTests.java
@@ -0,0 +1,13 @@
+package de.kreuzwerker.cdc.messagingapp;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+public class MessagingApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}
diff --git a/src/test/java/de/kreuzwerker/cdc/messagingapp/UserServiceContractTest.java b/src/test/java/de/kreuzwerker/cdc/messagingapp/UserServiceContractTest.java
new file mode 100644
index 0000000..d60a9f2
--- /dev/null
+++ b/src/test/java/de/kreuzwerker/cdc/messagingapp/UserServiceContractTest.java
@@ -0,0 +1,103 @@
+package de.kreuzwerker.cdc.messagingapp;
+
+import au.com.dius.pact.consumer.dsl.DslPart;
+import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
+import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
+import au.com.dius.pact.consumer.junit5.PactTestFor;
+import au.com.dius.pact.core.model.RequestResponsePact;
+import au.com.dius.pact.core.model.annotations.Pact;
+import io.pactfoundation.consumer.dsl.LambdaDsl;
+import org.assertj.core.groups.Tuple;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpStatus;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.web.client.HttpClientErrorException;
+
+import java.sql.Date;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@ExtendWith(SpringExtension.class)
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
+ properties = "user-service.base-url:http://localhost:8080",
+ classes = UserServiceClient.class)
+@Disabled
+@ExtendWith(PactConsumerTestExt.class)
+@PactTestFor(providerName = "user-service", port = "8080")
+public class UserServiceContractTest {
+
+ private static final String NAME = "user name for CDC";
+ private static final LocalDateTime LAST_LOGIN = LocalDateTime.of(2018, 10, 16, 12, 34, 12);
+
+ @Autowired
+ private UserServiceClient userServiceClient;
+
+ @Pact(consumer = "messaging-app")
+ public RequestResponsePact pactUserExists(PactDslWithProvider builder) {
+
+ // See https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-consumer-junit#dsl-matching-methods
+ DslPart body = LambdaDsl.newJsonBody((o) -> o
+ .stringType("name", NAME)
+ .timestamp("lastLogin", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+ Date.from(LAST_LOGIN.atZone(ZoneId.systemDefault()).toInstant()))
+ .stringMatcher("role", "ADMIN|USER", "ADMIN")
+ .minArrayLike("friends", 0, 2, friend -> friend
+ .stringType("id", "2")
+ .stringType("name", "a friend")
+ )).build();
+
+ return builder.given(
+ "User 1 exists")
+ .uponReceiving("A request to /users/1")
+ .path("/users/1")
+ .method("GET")
+ .willRespondWith()
+ .status(200)
+ .body(body)
+ .toPact();
+
+ }
+
+ @Pact(consumer = "messaging-app")
+ public RequestResponsePact pactUserDoesNotExist(PactDslWithProvider builder) {
+
+ return builder.given(
+ "User 2 does not exist")
+ .uponReceiving("A request to /users/2")
+ .path("/users/2")
+ .method("GET")
+ .willRespondWith()
+ .status(404)
+ .toPact();
+ }
+
+ @PactTestFor(pactMethod = "pactUserExists")
+ @Test
+ public void userExists() {
+ final User user = userServiceClient.getUser("1");
+
+ assertThat(user.getName()).isEqualTo(NAME);
+ assertThat(user.getLastLogin()).isEqualTo(LAST_LOGIN);
+ assertThat(user.getRole()).isEqualTo("ADMIN");
+ assertThat(user.getFriends()).hasSize(2)
+ // currently not possible to define multiple values, s. https://github.com/DiUS/pact-jvm/issues/379
+ .extracting(Friend::getId, Friend::getName)
+ .containsExactly(Tuple.tuple("2", "a friend"), Tuple.tuple("2", "a friend"));
+ }
+
+ @PactTestFor(pactMethod = "pactUserDoesNotExist")
+ @Test
+ public void userDoesNotExist() {
+ HttpClientErrorException exception = assertThrows(HttpClientErrorException.class, () ->
+ userServiceClient.getUser("2"));
+ assertEquals(HttpStatus.NOT_FOUND, exception.getStatusCode());
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/kreuzwerker/cdc/messagingapp/UserServiceContractTestV1.java b/src/test/java/de/kreuzwerker/cdc/messagingapp/UserServiceContractTestV1.java
new file mode 100644
index 0000000..39b6443
--- /dev/null
+++ b/src/test/java/de/kreuzwerker/cdc/messagingapp/UserServiceContractTestV1.java
@@ -0,0 +1,50 @@
+package de.kreuzwerker.cdc.messagingapp;
+
+import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
+import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
+import au.com.dius.pact.consumer.junit5.PactTestFor;
+import au.com.dius.pact.core.model.RequestResponsePact;
+import au.com.dius.pact.core.model.annotations.Pact;
+import io.pactfoundation.consumer.dsl.LambdaDsl;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
+ properties = "user-service.base-url:http://localhost:8080",
+ classes = UserServiceClient.class)
+@Disabled
+@ExtendWith(PactConsumerTestExt.class)
+@PactTestFor(providerName = "user-service", port = "8080")
+public class UserServiceContractTestV1 {
+
+ @Autowired
+ private UserServiceClient userServiceClient;
+
+
+ @Pact(consumer = "messaging-app")
+ public RequestResponsePact pactUserExists(PactDslWithProvider builder) {
+ return builder.given(
+ "User 1 exists")
+ .uponReceiving("A request to /users/1")
+ .path("/users/1")
+ .method("GET")
+ .willRespondWith()
+ .status(200)
+ .body(LambdaDsl.newJsonBody((o) ->
+ o.stringType("name", "user name for CDC")
+ ).build()).toPact();
+ }
+
+ @PactTestFor(pactMethod = "pactUserExists")
+ @Test
+ public void userExists() {
+ User user = userServiceClient.getUser("1");
+
+ assertThat(user.getName()).isEqualTo("user name for CDC");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/de/kreuzwerker/cdc/messagingapp/UserServiceGenericStateWithParameterContractTest.java b/src/test/java/de/kreuzwerker/cdc/messagingapp/UserServiceGenericStateWithParameterContractTest.java
new file mode 100644
index 0000000..985bee1
--- /dev/null
+++ b/src/test/java/de/kreuzwerker/cdc/messagingapp/UserServiceGenericStateWithParameterContractTest.java
@@ -0,0 +1,100 @@
+package de.kreuzwerker.cdc.messagingapp;
+
+import au.com.dius.pact.consumer.dsl.DslPart;
+import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
+import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
+import au.com.dius.pact.consumer.junit5.PactTestFor;
+import au.com.dius.pact.core.model.RequestResponsePact;
+import au.com.dius.pact.core.model.annotations.Pact;
+import io.pactfoundation.consumer.dsl.LambdaDsl;
+import org.assertj.core.groups.Tuple;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.client.HttpClientErrorException;
+
+import java.sql.Date;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
+ properties = "user-service.base-url:http://localhost:9000",
+ classes = UserServiceClient.class)
+@ExtendWith(PactConsumerTestExt.class)
+@PactTestFor(providerName = "user-service", port = "9000")
+public class UserServiceGenericStateWithParameterContractTest {
+
+ private static final String NAME = "user name for CDC";
+ private static final LocalDateTime LAST_LOGIN = LocalDateTime.of(2018, 10, 16, 12, 34, 12);
+
+ @Autowired
+ private UserServiceClient userServiceClient;
+
+
+ @Pact(consumer = "messaging-app")
+ public RequestResponsePact pactUserExists(PactDslWithProvider builder) {
+
+ // See https://github.com/DiUS/pact-jvm/tree/master/pact-jvm-consumer-junit#dsl-matching-methods
+ DslPart body = LambdaDsl.newJsonBody((o) -> o
+ .stringType("id", "1")
+ .stringType("name", NAME)
+ .timestamp("lastLogin", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+ Date.from(LAST_LOGIN.atZone(ZoneId.systemDefault()).toInstant()))
+ .stringMatcher("role", "ADMIN|USER", "ADMIN")
+ .minArrayLike("friends", 0, 2, friend -> friend
+ .stringType("id", "2")
+ .stringType("name", "a friend")
+ )).build();
+
+ return builder.given("default", Collections.singletonMap("userExists", true))
+ .uponReceiving("A request for an existing user")
+ .path("/users/1")
+ .method("GET")
+ .willRespondWith()
+ .status(200)
+ .body(body)
+ .toPact();
+
+ }
+
+ @Pact(consumer = "messaging-app")
+ public RequestResponsePact pactUserDoesNotExist(PactDslWithProvider builder) {
+
+ return builder.given("default", Collections.singletonMap("userExists", false))
+ .uponReceiving("A request for a non-existing user")
+ .path("/users/1")
+ .method("GET")
+ .willRespondWith()
+ .status(404)
+ .toPact();
+ }
+
+ @PactTestFor(pactMethod = "pactUserExists")
+ @Test
+ public void userExists() {
+ final User user = userServiceClient.getUser("1");
+
+ assertThat(user.getName()).isEqualTo(NAME);
+ assertThat(user.getLastLogin()).isEqualTo(LAST_LOGIN);
+ assertThat(user.getRole()).isEqualTo("ADMIN");
+ assertThat(user.getFriends()).hasSize(2)
+ // currently not possible to define multiple values, s. https://github.com/DiUS/pact-jvm/issues/379
+ .extracting(Friend::getId, Friend::getName)
+ .containsExactly(Tuple.tuple("2", "a friend"), Tuple.tuple("2", "a friend"));
+ }
+
+ @PactTestFor(pactMethod = "pactUserDoesNotExist")
+ @Test
+ public void userDoesNotExist() {
+ HttpClientErrorException exception = assertThrows(HttpClientErrorException.class, () ->
+ userServiceClient.getUser("1"));
+ assertEquals(HttpStatus.NOT_FOUND, exception.getStatusCode());
+ }
+}
\ No newline at end of file