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() { + } + +}