diff --git a/jenkins/Jenkinsfile-contract-tests b/jenkins/Jenkinsfile-contract-tests
new file mode 100644
index 0000000..2a2bf7b
--- /dev/null
+++ b/jenkins/Jenkinsfile-contract-tests
@@ -0,0 +1,42 @@
+#!groovy
+pipeline {
+
+ agent any
+
+ parameters {
+ string(name: 'pactConsumerTags', defaultValue: 'junit5', description: 'Tags to verify')
+ }
+
+ stages {
+ stage ('Get Latest Prod Version From Pact Broker') {
+ 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') {
+ script {
+ env.PROD_VERSION = sh(script: "./pact-broker describe-version -a user-service -b http://pact_broker -l prod | tail -1 | cut -f 1 -d \\|", returnStdout: true).trim()
+ }
+ }
+ echo "Current prod version: " + PROD_VERSION
+ }
+ }
+ stage("Checkout Latest Prod Version") {
+ steps {
+ sh "git checkout ${PROD_VERSION}"
+ }
+ }
+
+ stage ('Run Contract Tests') {
+ steps {
+ dir('user-service') {
+ sh "../mvnw clean test " +
+ "-Pcontract-tests " +
+ "-Dpact.provider.version=${PROD_VERSION} " +
+ "-Dpact.verifier.publishResults=true " +
+ "-Dpactbroker.tags=prod,${params.pactConsumerTags}"
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/jenkins/cd/Jenkinsfile b/jenkins/cd/Jenkinsfile
new file mode 100644
index 0000000..27a7c05
--- /dev/null
+++ b/jenkins/cd/Jenkinsfile
@@ -0,0 +1,35 @@
+#!groovy
+pipeline {
+
+ agent any
+
+ stages {
+ stage ('Build') {
+ steps {
+ dir('user-service') {
+ sh "../mvnw clean verify " +
+ "-Dpact.provider.version=${GIT_COMMIT} " +
+ "-Dpact.verifier.publishResults=true"
+ }
+ }
+ }
+ 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 user-service -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..438507f
--- /dev/null
+++ b/jenkins/without-cd/Jenkinsfile-build
@@ -0,0 +1,18 @@
+#!groovy
+pipeline {
+
+ agent any
+
+ stages {
+ stage('Build') {
+ steps {
+ dir('user-service') {
+ sh "../mvnw clean verify " +
+ "-Dpact.provider.version=${GIT_COMMIT} " +
+ "-Dpact.verifier.publishResults=true"
+ }
+ }
+ }
+ }
+
+}
\ 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..55b06eb
--- /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 user-service -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 user-service -b http://pact_broker -e ${GIT_COMMIT} -t prod"
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..bed5177
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,72 @@
+
+
+ 4.0.0
+
+ de.kreuzwerker.cdc
+ user-service
+ 1.0.0-SNAPSHOT
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.4.RELEASE
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ junit
+ junit
+
+
+
+
+
+ au.com.dius
+ pact-jvm-provider-junit5
+ 4.0.7
+ test
+
+
+
+
+
+ contract-tests
+
+
+
+ maven-surefire-plugin
+
+
+ **/*ContractTest.java
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/de/kreuzwerker/cdc/userservice/Friend.java b/src/main/java/de/kreuzwerker/cdc/userservice/Friend.java
new file mode 100644
index 0000000..f7b9f55
--- /dev/null
+++ b/src/main/java/de/kreuzwerker/cdc/userservice/Friend.java
@@ -0,0 +1,13 @@
+package de.kreuzwerker.cdc.userservice;
+
+import lombok.Builder;
+import lombok.Data;
+
+@Data
+@Builder
+public class Friend {
+
+ private String id;
+ private String name;
+
+}
diff --git a/src/main/java/de/kreuzwerker/cdc/userservice/GlobalControllerExceptionHandler.java b/src/main/java/de/kreuzwerker/cdc/userservice/GlobalControllerExceptionHandler.java
new file mode 100644
index 0000000..30cb31f
--- /dev/null
+++ b/src/main/java/de/kreuzwerker/cdc/userservice/GlobalControllerExceptionHandler.java
@@ -0,0 +1,15 @@
+package de.kreuzwerker.cdc.userservice;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ControllerAdvice
+public class GlobalControllerExceptionHandler {
+
+ @ExceptionHandler(NotFoundException.class)
+ @ResponseStatus(HttpStatus.NOT_FOUND)
+ public void handleNotFound() {
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/kreuzwerker/cdc/userservice/NotFoundException.java b/src/main/java/de/kreuzwerker/cdc/userservice/NotFoundException.java
new file mode 100644
index 0000000..14381ec
--- /dev/null
+++ b/src/main/java/de/kreuzwerker/cdc/userservice/NotFoundException.java
@@ -0,0 +1,5 @@
+package de.kreuzwerker.cdc.userservice;
+
+public class NotFoundException extends RuntimeException {
+
+}
diff --git a/src/main/java/de/kreuzwerker/cdc/userservice/User.java b/src/main/java/de/kreuzwerker/cdc/userservice/User.java
new file mode 100644
index 0000000..5e70cb4
--- /dev/null
+++ b/src/main/java/de/kreuzwerker/cdc/userservice/User.java
@@ -0,0 +1,22 @@
+package de.kreuzwerker.cdc.userservice;
+
+import java.util.Date;
+import java.util.List;
+import lombok.Builder;
+import lombok.Data;
+import lombok.Singular;
+
+@Data
+@Builder
+public class User {
+
+ private String id;
+ private String legacyId;
+ private String name;
+ private UserRole role;
+ private Date lastLogin;
+ @Singular
+ private List friends;
+
+
+}
diff --git a/src/main/java/de/kreuzwerker/cdc/userservice/UserController.java b/src/main/java/de/kreuzwerker/cdc/userservice/UserController.java
new file mode 100644
index 0000000..617655e
--- /dev/null
+++ b/src/main/java/de/kreuzwerker/cdc/userservice/UserController.java
@@ -0,0 +1,21 @@
+package de.kreuzwerker.cdc.userservice;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class UserController {
+
+ private final UserService userService;
+
+ public UserController(UserService userService) {
+ this.userService = userService;
+ }
+
+ @GetMapping("/users/{userId}")
+ public User getUser(@PathVariable String userId) {
+ return userService.findUser(userId);
+ }
+
+}
diff --git a/src/main/java/de/kreuzwerker/cdc/userservice/UserRole.java b/src/main/java/de/kreuzwerker/cdc/userservice/UserRole.java
new file mode 100644
index 0000000..66b6744
--- /dev/null
+++ b/src/main/java/de/kreuzwerker/cdc/userservice/UserRole.java
@@ -0,0 +1,8 @@
+package de.kreuzwerker.cdc.userservice;
+
+public enum UserRole {
+
+ ADMIN,
+
+ USER;
+}
diff --git a/src/main/java/de/kreuzwerker/cdc/userservice/UserService.java b/src/main/java/de/kreuzwerker/cdc/userservice/UserService.java
new file mode 100644
index 0000000..6e6fd87
--- /dev/null
+++ b/src/main/java/de/kreuzwerker/cdc/userservice/UserService.java
@@ -0,0 +1,21 @@
+package de.kreuzwerker.cdc.userservice;
+
+import java.util.Date;
+import java.util.UUID;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserService {
+
+ public User findUser(String userId) {
+ return User.builder()
+ .id(userId)
+ .legacyId(UUID.randomUUID().toString())
+ .name("Beth")
+ .role(UserRole.ADMIN)
+ .lastLogin(new Date())
+ .friend(Friend.builder().id("2").name("Ronald Smith").build())
+ .friend(Friend.builder().id("3").name("Matt Spencer").build())
+ .build();
+ }
+}
diff --git a/src/main/java/de/kreuzwerker/cdc/userservice/UserServiceApplication.java b/src/main/java/de/kreuzwerker/cdc/userservice/UserServiceApplication.java
new file mode 100644
index 0000000..ac568d2
--- /dev/null
+++ b/src/main/java/de/kreuzwerker/cdc/userservice/UserServiceApplication.java
@@ -0,0 +1,12 @@
+package de.kreuzwerker.cdc.userservice;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class UserServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(UserServiceApplication.class, args);
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..9620feb
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,8 @@
+server:
+ port: 8090
+
+spring:
+ jackson:
+ serialization:
+ write-dates-as-timestamps: false
+ date-format: yyyy-MM-dd'T'HH:mm:ss.SSS'Z'
\ No newline at end of file
diff --git a/src/test/java/de/kreuzwerker/cdc/userservice/ContractTest.java b/src/test/java/de/kreuzwerker/cdc/userservice/ContractTest.java
new file mode 100644
index 0000000..bedbab0
--- /dev/null
+++ b/src/test/java/de/kreuzwerker/cdc/userservice/ContractTest.java
@@ -0,0 +1,41 @@
+package de.kreuzwerker.cdc.userservice;
+
+import au.com.dius.pact.provider.junit.Provider;
+import au.com.dius.pact.provider.junit.State;
+import au.com.dius.pact.provider.junit.loader.PactFolder;
+import au.com.dius.pact.provider.junit5.HttpTestTarget;
+import au.com.dius.pact.provider.junit5.PactVerificationContext;
+import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.web.server.LocalServerPort;
+
+@Provider("user-service")
+@PactFolder("pacts")
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@Disabled
+public class ContractTest {
+
+ @LocalServerPort
+ private int port;
+
+ @BeforeEach
+ void before(PactVerificationContext context) {
+ context.setTarget(new HttpTestTarget("localhost", port));
+ }
+
+ @TestTemplate
+ @ExtendWith(PactVerificationInvocationContextProvider.class)
+ void pactVerificationTestTemplate(PactVerificationContext context) {
+ context.verifyInteraction();
+ }
+ @State("User 1 exists")
+ public void user1Exists() {
+ // nothing to do, real service is used
+ }
+
+}
+
diff --git a/src/test/java/de/kreuzwerker/cdc/userservice/GenericStateWithParameterContractTest.java b/src/test/java/de/kreuzwerker/cdc/userservice/GenericStateWithParameterContractTest.java
new file mode 100644
index 0000000..46dd4ce
--- /dev/null
+++ b/src/test/java/de/kreuzwerker/cdc/userservice/GenericStateWithParameterContractTest.java
@@ -0,0 +1,66 @@
+package de.kreuzwerker.cdc.userservice;
+
+import au.com.dius.pact.provider.junit.Provider;
+import au.com.dius.pact.provider.junit.State;
+import au.com.dius.pact.provider.junit.loader.PactBroker;
+import au.com.dius.pact.provider.junit5.HttpTestTarget;
+import au.com.dius.pact.provider.junit5.PactVerificationContext;
+import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.boot.web.server.LocalServerPort;
+
+import java.util.Date;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@Provider("user-service")
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+//pact_broker is the service name in docker-compose
+@PactBroker(host = "pact_broker", tags = "${pactbroker.tags:prod}")
+public class GenericStateWithParameterContractTest {
+
+ @LocalServerPort
+ private int port;
+
+ @BeforeEach
+ void before(PactVerificationContext context) {
+ context.setTarget(new HttpTestTarget("localhost", port));
+ }
+
+ @TestTemplate
+ @ExtendWith(PactVerificationInvocationContextProvider.class)
+ void pactVerificationTestTemplate(PactVerificationContext context) {
+ context.verifyInteraction();
+ }
+
+ @MockBean
+ private UserService userService;
+
+ @State("default")
+ public void toDefaultState(Map params) {
+ final boolean userExists = (boolean) params.get("userExists");
+ if (userExists) {
+ when(userService.findUser(any())).thenReturn(User.builder()
+ .id("1")
+ .legacyId(UUID.randomUUID().toString())
+ .name("Beth")
+ .role(UserRole.ADMIN)
+ .lastLogin(new Date())
+ .friend(Friend.builder().id("2").name("Ronald Smith").build())
+ .friend(Friend.builder().id("3").name("Matt Spencer").build())
+ .build());
+ } else {
+ when(userService.findUser(any())).thenThrow(NotFoundException.class);
+ }
+ }
+
+
+}
+
diff --git a/src/test/java/de/kreuzwerker/cdc/userservice/MockedUserServiceContractTest.java b/src/test/java/de/kreuzwerker/cdc/userservice/MockedUserServiceContractTest.java
new file mode 100644
index 0000000..3b3dac5
--- /dev/null
+++ b/src/test/java/de/kreuzwerker/cdc/userservice/MockedUserServiceContractTest.java
@@ -0,0 +1,56 @@
+package de.kreuzwerker.cdc.userservice;
+
+import au.com.dius.pact.provider.junit.Provider;
+import au.com.dius.pact.provider.junit.State;
+import au.com.dius.pact.provider.junit.loader.PactFolder;
+import au.com.dius.pact.provider.junit5.HttpTestTarget;
+import au.com.dius.pact.provider.junit5.PactVerificationContext;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.boot.web.server.LocalServerPort;
+
+import java.util.Date;
+import java.util.UUID;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@Provider("user-service")
+@PactFolder("pacts")
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@Disabled
+public class MockedUserServiceContractTest {
+
+ @LocalServerPort
+ private int port;
+
+ @BeforeEach
+ void before(PactVerificationContext context) {
+ context.setTarget(new HttpTestTarget("localhost", port));
+ }
+
+ @MockBean
+ private UserService userService;
+
+ @State("User 1 exists")
+ public void user1Exists() {
+ when(userService.findUser(any())).thenReturn(User.builder()
+ .id("1")
+ .legacyId(UUID.randomUUID().toString())
+ .name("Beth")
+ .role(UserRole.ADMIN)
+ .lastLogin(new Date())
+ .friend(Friend.builder().id("2").name("Ronald Smith").build())
+ .friend(Friend.builder().id("3").name("Matt Spencer").build())
+ .build());
+ }
+
+ @State("User 2 does not exist")
+ public void user2DoesNotExist() {
+ when(userService.findUser(any())).thenThrow(NotFoundException.class);
+ }
+
+}
+
diff --git a/src/test/java/de/kreuzwerker/cdc/userservice/UserServiceApplicationTests.java b/src/test/java/de/kreuzwerker/cdc/userservice/UserServiceApplicationTests.java
new file mode 100644
index 0000000..658d852
--- /dev/null
+++ b/src/test/java/de/kreuzwerker/cdc/userservice/UserServiceApplicationTests.java
@@ -0,0 +1,12 @@
+package de.kreuzwerker.cdc.userservice;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+@SpringBootTest
+public class UserServiceApplicationTests {
+
+ @Test
+ public void contextLoads() {
+ }
+
+}