diff --git a/feign-with-spring-data-rest/README.md b/feign-with-spring-data-rest/README.md index 1ac3006..a7e7fd4 100644 --- a/feign-with-spring-data-rest/README.md +++ b/feign-with-spring-data-rest/README.md @@ -3,6 +3,8 @@ This repo contains some example code that creates a REST client using Feign that accesses a REST API exposed by Spring Data REST. +## Companion Blog Post + The companion blog post with more details can be found [here](https://reflectoring.io/accessing-spring-data-rest-with-feign/). ## Running the application diff --git a/pact-feign-consumer/.gitignore b/pact-feign-consumer/.gitignore new file mode 100644 index 0000000..c9f7010 --- /dev/null +++ b/pact-feign-consumer/.gitignore @@ -0,0 +1,25 @@ +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ \ No newline at end of file diff --git a/pact-feign-consumer/README.md b/pact-feign-consumer/README.md new file mode 100644 index 0000000..77cd192 --- /dev/null +++ b/pact-feign-consumer/README.md @@ -0,0 +1,16 @@ +# Consumer-Driven-Contract Test for a Feign Consumer + +This repo contains an example of consumer-driven-contract testing for a Feign client +that consumes a REST API provided by the module `pact-spring-data-rest-provider`. + +The contract is created and verified with [Pact](https://docs.pact.io/). + +## Companion Blog Post + +The Companion Blog Post on [https://reflectoring.io](https://reflectoring.io) will be +published shortly. + +## Running the application + +The interesting part in this code base is the class `ConsumerPactVerificationTest`. +You can run the tests with `gradlew test` on Windows or `./gradlew test` on Unix. \ No newline at end of file diff --git a/pact-feign-consumer/build.gradle b/pact-feign-consumer/build.gradle new file mode 100644 index 0000000..d7a5fc8 --- /dev/null +++ b/pact-feign-consumer/build.gradle @@ -0,0 +1,46 @@ +buildscript { + ext { + springBootVersion = '1.5.4.RELEASE' + } + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'org.springframework.boot' + +version = '0.0.1-SNAPSHOT' +sourceCompatibility = 1.8 + +repositories { + mavenLocal() + mavenCentral() +} + +ext { + springCloudVersion = 'Dalston.SR2' +} + +dependencies { + compile('org.springframework.cloud:spring-cloud-starter-feign') + // locking transitive guava version to work around "java.lang.IllegalAccessError: tried to access method com.google.common.collect.Lists.cartesianProduct" + compile('com.google.guava:guava:22.0') + compile('org.springframework.boot:spring-boot-starter-hateoas') + testCompile group: 'au.com.dius', name: 'pact-jvm-consumer-junit_2.11', version: '3.5.2' + testCompile('org.springframework.boot:spring-boot-starter-test') +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +bootRun{ + jvmArgs = ["-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5006"] +} diff --git a/pact-feign-consumer/gradle/wrapper/gradle-wrapper.jar b/pact-feign-consumer/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1a958be Binary files /dev/null and b/pact-feign-consumer/gradle/wrapper/gradle-wrapper.jar differ diff --git a/pact-feign-consumer/gradle/wrapper/gradle-wrapper.properties b/pact-feign-consumer/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..90a06ce --- /dev/null +++ b/pact-feign-consumer/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5.1-bin.zip diff --git a/pact-feign-consumer/gradlew b/pact-feign-consumer/gradlew new file mode 100644 index 0000000..4453cce --- /dev/null +++ b/pact-feign-consumer/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/pact-feign-consumer/gradlew.bat b/pact-feign-consumer/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/pact-feign-consumer/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pact-feign-consumer/src/main/java/com/example/demo/Address.java b/pact-feign-consumer/src/main/java/com/example/demo/Address.java new file mode 100644 index 0000000..523f539 --- /dev/null +++ b/pact-feign-consumer/src/main/java/com/example/demo/Address.java @@ -0,0 +1,24 @@ +package com.example.demo; + +public class Address { + + private long id; + + private String street; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } +} diff --git a/pact-feign-consumer/src/main/java/com/example/demo/AddressClient.java b/pact-feign-consumer/src/main/java/com/example/demo/AddressClient.java new file mode 100644 index 0000000..93adee7 --- /dev/null +++ b/pact-feign-consumer/src/main/java/com/example/demo/AddressClient.java @@ -0,0 +1,26 @@ +package com.example.demo; + +import org.springframework.cloud.netflix.feign.FeignClient; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +@FeignClient(value = "addresses", path = "/addresses") +public interface AddressClient { + + @RequestMapping(method = RequestMethod.GET, path = "/") + Resources
getAddresses(); + + @RequestMapping(method = RequestMethod.GET, path = "/{id}") + Resource
getAddress(@PathVariable("id") long id); + + @RequestMapping(method = RequestMethod.PUT, consumes = "text/uri-list", path="/{addressId}/customer") + Resource
associateWithCustomer(@PathVariable("addressId") long addressId, @RequestBody String customerUri); + + @RequestMapping(method = RequestMethod.GET, path="/{addressId}/customer") + Resource getCustomer(@PathVariable("addressId") long addressId); + +} diff --git a/pact-feign-consumer/src/main/java/com/example/demo/Customer.java b/pact-feign-consumer/src/main/java/com/example/demo/Customer.java new file mode 100644 index 0000000..7142577 --- /dev/null +++ b/pact-feign-consumer/src/main/java/com/example/demo/Customer.java @@ -0,0 +1,24 @@ +package com.example.demo; + +public class Customer { + + private long id; + + private String name; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/pact-feign-consumer/src/main/java/com/example/demo/CustomerClient.java b/pact-feign-consumer/src/main/java/com/example/demo/CustomerClient.java new file mode 100644 index 0000000..2dacea5 --- /dev/null +++ b/pact-feign-consumer/src/main/java/com/example/demo/CustomerClient.java @@ -0,0 +1,19 @@ +package com.example.demo; + +import org.springframework.cloud.netflix.feign.FeignClient; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +@FeignClient(value = "customers", path = "/customers_mto") +public interface CustomerClient { + + @RequestMapping(method = RequestMethod.GET, value = "/") + Resources getCustomers(); + + @RequestMapping(method = RequestMethod.GET, value = "/{id}") + Resource getCustomer(@PathVariable("id") long id); + +} diff --git a/pact-feign-consumer/src/main/java/com/example/demo/DemoApplication.java b/pact-feign-consumer/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 0000000..f2d4a39 --- /dev/null +++ b/pact-feign-consumer/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,16 @@ +package com.example.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.feign.EnableFeignClients; +import org.springframework.hateoas.config.EnableHypermediaSupport; + +@SpringBootApplication +@EnableFeignClients +@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL) +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } +} diff --git a/pact-feign-consumer/src/main/java/com/example/demo/FeignConfiguration.java b/pact-feign-consumer/src/main/java/com/example/demo/FeignConfiguration.java new file mode 100644 index 0000000..07e62bd --- /dev/null +++ b/pact-feign-consumer/src/main/java/com/example/demo/FeignConfiguration.java @@ -0,0 +1,15 @@ +package com.example.demo; + +import feign.Logger; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FeignConfiguration { + + @Bean + public Logger.Level logLevel(){ + return Logger.Level.FULL; + } + +} diff --git a/pact-feign-consumer/src/main/resources/application.yml b/pact-feign-consumer/src/main/resources/application.yml new file mode 100644 index 0000000..ff07c4b --- /dev/null +++ b/pact-feign-consumer/src/main/resources/application.yml @@ -0,0 +1,17 @@ +server.port: 8081 + +logging.level.com.example.demo.CustomerClient: DEBUG +logging.level.com.example.demo.AddressClient: DEBUG +logging.level.org.hibernate.SQL: DEBUG + +customers: + ribbon: + eureka: + enabled: false + listOfServers: localhost:8080 + +addresses: + ribbon: + eureka: + enabled: false + listOfServers: localhost:8080 \ No newline at end of file diff --git a/pact-feign-consumer/src/test/java/com/example/demo/ConsumerPactVerificationTest.java b/pact-feign-consumer/src/test/java/com/example/demo/ConsumerPactVerificationTest.java new file mode 100644 index 0000000..9a0111e --- /dev/null +++ b/pact-feign-consumer/src/test/java/com/example/demo/ConsumerPactVerificationTest.java @@ -0,0 +1,137 @@ +package com.example.demo; + +import au.com.dius.pact.consumer.Pact; +import au.com.dius.pact.consumer.PactProviderRuleMk2; +import au.com.dius.pact.consumer.PactVerification; +import au.com.dius.pact.consumer.dsl.PactDslWithProvider; +import au.com.dius.pact.model.RequestResponsePact; +import org.apache.http.entity.ContentType; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.Resources; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(properties = { + // overriding producer address + "addresses.ribbon.listOfServers: localhost:8888" +}) +public class ConsumerPactVerificationTest { + + @Rule + public PactProviderRuleMk2 stubProvider = new PactProviderRuleMk2("customerServiceProvider", "localhost", 8888, this); + + @Autowired + private AddressClient addressClient; + + @Pact(state = "address-collection", provider = "customerServiceProvider", consumer = "addressClient") + public RequestResponsePact createAddressCollectionResourcePact(PactDslWithProvider builder) { + return builder + .given("address-collection") + .uponReceiving("a request to the address collection resource") + .path("/addresses/") + .method("GET") + .willRespondWith() + .status(200) + .body("{\n" + + " \"_embedded\": {\n" + + " \"addresses\": [\n" + + " {\n" + + " \"street\": \"Elm Street\",\n" + + " \"_links\": {\n" + + " \"self\": {\n" + + " \"href\": \"http://localhost:8080/addresses/1\"\n" + + " },\n" + + " \"address\": {\n" + + " \"href\": \"http://localhost:8080/addresses/1\"\n" + + " },\n" + + " \"customer\": {\n" + + " \"href\": \"http://localhost:8080/addresses/1/customer\"\n" + + " }\n" + + " }\n" + + " },\n" + + " {\n" + + " \"street\": \"High Street\",\n" + + " \"_links\": {\n" + + " \"self\": {\n" + + " \"href\": \"http://localhost:8080/addresses/2\"\n" + + " },\n" + + " \"address\": {\n" + + " \"href\": \"http://localhost:8080/addresses/2\"\n" + + " },\n" + + " \"customer\": {\n" + + " \"href\": \"http://localhost:8080/addresses/2/customer\"\n" + + " }\n" + + " }\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"_links\": {\n" + + " \"self\": {\n" + + " \"href\": \"http://localhost:8080/addresses{?page,size,sort}\",\n" + + " \"templated\": true\n" + + " },\n" + + " \"profile\": {\n" + + " \"href\": \"http://localhost:8080/profile/addresses\"\n" + + " },\n" + + " \"search\": {\n" + + " \"href\": \"http://localhost:8080/addresses/search\"\n" + + " }\n" + + " },\n" + + " \"page\": {\n" + + " \"size\": 20,\n" + + " \"totalElements\": 2,\n" + + " \"totalPages\": 1,\n" + + " \"number\": 0\n" + + " }\n" + + "}", "application/hal+json") + .toPact(); + } + + @Pact(state = "single-address", provider = "customerServiceProvider", consumer = "addressClient") + public RequestResponsePact createAddressResourcePact(PactDslWithProvider builder) { + return builder + .given("single-address") + .uponReceiving("a request to the address resource") + .path("/addresses/1") + .method("GET") + .willRespondWith() + .status(200) + .body("{\n" + + " \"street\": \"Elm Street\",\n" + + " \"_links\": {\n" + + " \"self\": {\n" + + " \"href\": \"http://localhost:8080/addresses/1\"\n" + + " },\n" + + " \"address\": {\n" + + " \"href\": \"http://localhost:8080/addresses/1\"\n" + + " },\n" + + " \"customer\": {\n" + + " \"href\": \"http://localhost:8080/addresses/1/customer\"\n" + + " }\n" + + " }\n" + + "}", "application/hal+json") + .toPact(); + } + + @Test + @PactVerification(fragment = "createAddressCollectionResourcePact") + public void addressCollectionResourcePact() { + Resources
addresses = addressClient.getAddresses(); + assertThat(addresses).hasSize(2); + } + + @Test + @PactVerification(fragment = "createAddressResourcePact") + public void addressResourcePact() { + Resource
address = addressClient.getAddress(1L); + assertThat(address).isNotNull(); + } + +} diff --git a/pact-feign-consumer/src/test/java/com/example/demo/DemoApplicationTests.java b/pact-feign-consumer/src/test/java/com/example/demo/DemoApplicationTests.java new file mode 100644 index 0000000..b76e7f2 --- /dev/null +++ b/pact-feign-consumer/src/test/java/com/example/demo/DemoApplicationTests.java @@ -0,0 +1,16 @@ +package com.example.demo; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class DemoApplicationTests { + + @Test + public void contextLoads() { + } + +} diff --git a/pact-spring-data-rest-provider/.gitignore b/pact-spring-data-rest-provider/.gitignore new file mode 100644 index 0000000..c9f7010 --- /dev/null +++ b/pact-spring-data-rest-provider/.gitignore @@ -0,0 +1,25 @@ +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ \ No newline at end of file diff --git a/pact-spring-data-rest-provider/README.md b/pact-spring-data-rest-provider/README.md new file mode 100644 index 0000000..9915ae4 --- /dev/null +++ b/pact-spring-data-rest-provider/README.md @@ -0,0 +1,17 @@ +# Consumer-Driven-Contract Test for a Spring Data Rest Provider + +This repo contains an example of consumer-driven-contract testing for a Spring +Data REST API provider. The corresponding consumer to the contract is +implemented in the module `pact-feign-consumer`. + +The contract is created and verified with [Pact](https://docs.pact.io/). + +## Companion Blog Post + +The Companion Blog Post on [https://reflectoring.io](https://reflectoring.io) will be +published shortly. + +## Running the application + +The interesting part in this code base is the class `ProviderPactVerificationTest`. +You can run the tests with `gradlew test` on Windows or `./gradlew test` on Unix. \ No newline at end of file diff --git a/pact-spring-data-rest-provider/build.gradle b/pact-spring-data-rest-provider/build.gradle new file mode 100644 index 0000000..c9458f9 --- /dev/null +++ b/pact-spring-data-rest-provider/build.gradle @@ -0,0 +1,35 @@ +buildscript { + ext { + springBootVersion = '1.5.4.RELEASE' + } + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'org.springframework.boot' + +version = '0.0.1-SNAPSHOT' +sourceCompatibility = 1.8 + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies { + compile('org.springframework.boot:spring-boot-starter-data-jpa') + compile('org.springframework.boot:spring-boot-starter-data-rest') + compile group: 'au.com.dius', name: 'pact-jvm-provider-junit_2.11', version: '3.5.2' + compile('com.h2database:h2:1.4.196') + testCompile('org.springframework.boot:spring-boot-starter-test') +} + +bootRun{ + jvmArgs = ["-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"] +} diff --git a/pact-spring-data-rest-provider/gradle/wrapper/gradle-wrapper.jar b/pact-spring-data-rest-provider/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1a958be Binary files /dev/null and b/pact-spring-data-rest-provider/gradle/wrapper/gradle-wrapper.jar differ diff --git a/pact-spring-data-rest-provider/gradle/wrapper/gradle-wrapper.properties b/pact-spring-data-rest-provider/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..90a06ce --- /dev/null +++ b/pact-spring-data-rest-provider/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5.1-bin.zip diff --git a/pact-spring-data-rest-provider/gradlew b/pact-spring-data-rest-provider/gradlew new file mode 100644 index 0000000..4453cce --- /dev/null +++ b/pact-spring-data-rest-provider/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/pact-spring-data-rest-provider/gradlew.bat b/pact-spring-data-rest-provider/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/pact-spring-data-rest-provider/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/pact-spring-data-rest-provider/src/main/java/com/example/demo/Address.java b/pact-spring-data-rest-provider/src/main/java/com/example/demo/Address.java new file mode 100644 index 0000000..0b0c416 --- /dev/null +++ b/pact-spring-data-rest-provider/src/main/java/com/example/demo/Address.java @@ -0,0 +1,43 @@ +package com.example.demo; + +import javax.persistence.*; + +@Entity +public class Address { + + @GeneratedValue + @Id + private Long id; + + @Column + private String street; + + @ManyToOne + private Customer customer; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } +} + + diff --git a/pact-spring-data-rest-provider/src/main/java/com/example/demo/AddressRepository.java b/pact-spring-data-rest-provider/src/main/java/com/example/demo/AddressRepository.java new file mode 100644 index 0000000..eb6199f --- /dev/null +++ b/pact-spring-data-rest-provider/src/main/java/com/example/demo/AddressRepository.java @@ -0,0 +1,14 @@ +package com.example.demo; + +import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.hateoas.Resources; + +import java.util.List; + +@RepositoryRestResource(path = "addresses") +public interface AddressRepository extends PagingAndSortingRepository { + + List
findByCustomerId(long customerId); + +} diff --git a/pact-spring-data-rest-provider/src/main/java/com/example/demo/Customer.java b/pact-spring-data-rest-provider/src/main/java/com/example/demo/Customer.java new file mode 100644 index 0000000..869648e --- /dev/null +++ b/pact-spring-data-rest-provider/src/main/java/com/example/demo/Customer.java @@ -0,0 +1,30 @@ +package com.example.demo; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +@Entity +public class Customer { + + @Id + @GeneratedValue + private long id; + + @Column + private String name; + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/pact-spring-data-rest-provider/src/main/java/com/example/demo/CustomerRepository.java b/pact-spring-data-rest-provider/src/main/java/com/example/demo/CustomerRepository.java new file mode 100644 index 0000000..64ebf77 --- /dev/null +++ b/pact-spring-data-rest-provider/src/main/java/com/example/demo/CustomerRepository.java @@ -0,0 +1,9 @@ +package com.example.demo; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; + +@RepositoryRestResource(path = "customers") +public interface CustomerRepository extends CrudRepository { + +} diff --git a/pact-spring-data-rest-provider/src/main/java/com/example/demo/DemoApplication.java b/pact-spring-data-rest-provider/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 0000000..5d799be --- /dev/null +++ b/pact-spring-data-rest-provider/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,12 @@ +package com.example.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } +} diff --git a/pact-spring-data-rest-provider/src/main/resources/application.properties b/pact-spring-data-rest-provider/src/main/resources/application.properties new file mode 100644 index 0000000..ddf90bd --- /dev/null +++ b/pact-spring-data-rest-provider/src/main/resources/application.properties @@ -0,0 +1,7 @@ +spring.datasource.url=jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.h2.console.enabled=true + +logging.level.org.hibernate.SQL=OFF \ No newline at end of file diff --git a/pact-spring-data-rest-provider/src/test/java/com/example/demo/ProviderPactVerificationTest.java b/pact-spring-data-rest-provider/src/test/java/com/example/demo/ProviderPactVerificationTest.java new file mode 100644 index 0000000..97061e6 --- /dev/null +++ b/pact-spring-data-rest-provider/src/test/java/com/example/demo/ProviderPactVerificationTest.java @@ -0,0 +1,49 @@ +package com.example.demo; + +import au.com.dius.pact.provider.junit.PactRunner; +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.junit.target.HttpTarget; +import au.com.dius.pact.provider.junit.target.Target; +import au.com.dius.pact.provider.junit.target.TestTarget; +import com.example.framework.DatabaseState; +import com.example.framework.DatabaseStateHolder; +import com.example.framework.SpringBootStarter; +import org.junit.ClassRule; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ConfigurableApplicationContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@RunWith(PactRunner.class) +@Provider("customerServiceProvider") +@PactFolder("../pact-feign-consumer/target/pacts") +public class ProviderPactVerificationTest { + + @ClassRule + public static SpringBootStarter appStarter = SpringBootStarter.builder() + .withApplicationClass(DemoApplication.class) + .withArgument("--spring.config.location=classpath:/application-pact.properties") + .withDatabaseState("single-address", "/initial-schema.sql", "/single-address.sql") + .withDatabaseState("address-collection", "/initial-schema.sql", "/address-collection.sql") + .build(); + + @State("single-address") + public void toSingleAddressState() { + DatabaseStateHolder.setCurrentDatabaseState("single-address"); + } + + @State("address-collection") + public void toAddressCollectionState() { + DatabaseStateHolder.setCurrentDatabaseState("address-collection"); + } + + @TestTarget + public final Target target = new HttpTarget(8080); + + +} diff --git a/pact-spring-data-rest-provider/src/test/java/com/example/framework/DatabaseState.java b/pact-spring-data-rest-provider/src/test/java/com/example/framework/DatabaseState.java new file mode 100644 index 0000000..348e0e3 --- /dev/null +++ b/pact-spring-data-rest-provider/src/test/java/com/example/framework/DatabaseState.java @@ -0,0 +1,22 @@ +package com.example.framework; + +public class DatabaseState { + + private final String stateName; + + private final String[] sqlscripts; + + public DatabaseState(String stateName, String... sqlscripts) { + this.stateName = stateName; + this.sqlscripts = sqlscripts; + } + + public String getStateName() { + return stateName; + } + + public String[] getSqlscripts() { + return sqlscripts; + } + +} diff --git a/pact-spring-data-rest-provider/src/test/java/com/example/framework/DatabaseStateHolder.java b/pact-spring-data-rest-provider/src/test/java/com/example/framework/DatabaseStateHolder.java new file mode 100644 index 0000000..4fe63e4 --- /dev/null +++ b/pact-spring-data-rest-provider/src/test/java/com/example/framework/DatabaseStateHolder.java @@ -0,0 +1,15 @@ +package com.example.framework; + +public class DatabaseStateHolder { + + private static String currentDatabaseState; + + public static void setCurrentDatabaseState(String databaseStateName) { + currentDatabaseState = databaseStateName; + } + + public static String getCurrentDatabaseState() { + return currentDatabaseState; + } + +} diff --git a/pact-spring-data-rest-provider/src/test/java/com/example/framework/DatabaseStatesInitializer.java b/pact-spring-data-rest-provider/src/test/java/com/example/framework/DatabaseStatesInitializer.java new file mode 100644 index 0000000..13418c6 --- /dev/null +++ b/pact-spring-data-rest-provider/src/test/java/com/example/framework/DatabaseStatesInitializer.java @@ -0,0 +1,33 @@ +package com.example.framework; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; + +import javax.annotation.PostConstruct; +import javax.sql.DataSource; +import java.util.List; + +public class DatabaseStatesInitializer { + + private final DataSource dataSource; + + private final List databaseStates; + + public DatabaseStatesInitializer(DataSource dataSource, List databaseStates) { + this.dataSource = dataSource; + this.databaseStates = databaseStates; + } + + @PostConstruct + public void initialize() { + for (DatabaseState databaseState : this.databaseStates) { + DatabaseStateHolder.setCurrentDatabaseState(databaseState.getStateName()); + ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); + for (String script : databaseState.getSqlscripts()) { + populator.addScript(new ClassPathResource(script)); + } + populator.execute(this.dataSource); + } + } + +} diff --git a/pact-spring-data-rest-provider/src/test/java/com/example/framework/PactDatabaseStatesAutoConfiguration.java b/pact-spring-data-rest-provider/src/test/java/com/example/framework/PactDatabaseStatesAutoConfiguration.java new file mode 100644 index 0000000..922c12a --- /dev/null +++ b/pact-spring-data-rest-provider/src/test/java/com/example/framework/PactDatabaseStatesAutoConfiguration.java @@ -0,0 +1,54 @@ +package com.example.framework; + +import au.com.dius.pact.provider.junit.PactRunner; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; + +import javax.sql.DataSource; +import java.util.HashMap; +import java.util.Map; + +@Configuration +@ConditionalOnClass(PactRunner.class) +@EnableConfigurationProperties(PactProperties.class) +public class PactDatabaseStatesAutoConfiguration { + + @Bean + public DataSource dataSource(PactProperties pactProperties) { + AbstractRoutingDataSource dataSource = new AbstractRoutingDataSource() { + @Override + protected Object determineCurrentLookupKey() { + return DatabaseStateHolder.getCurrentDatabaseState(); + } + }; + + Map targetDataSources = new HashMap<>(); + dataSource.setTargetDataSources(targetDataSources); + + // create a DataSource for each DatabaseState + for (DatabaseState databaseState : pactProperties.getDatabaseStatesList()) { + DataSource ds = DataSourceBuilder + .create() + .url(String.format("jdbc:h2:mem:%s;DB_CLOSE_ON_EXIT=FALSE", databaseState.getStateName())) + .driverClassName("org.h2.Driver") + .username("sa") + .password("") + .build(); + targetDataSources.put(databaseState.getStateName(), ds); + DatabaseStateHolder.setCurrentDatabaseState(databaseState.getStateName()); + } + + return dataSource; + } + + @Bean + public DatabaseStatesInitializer databaseStatesInitializer(DataSource routingDataSource, PactProperties pactProperties) { + return new DatabaseStatesInitializer(routingDataSource, pactProperties.getDatabaseStatesList()); + } + + +} diff --git a/pact-spring-data-rest-provider/src/test/java/com/example/framework/PactProperties.java b/pact-spring-data-rest-provider/src/test/java/com/example/framework/PactProperties.java new file mode 100644 index 0000000..8b8a552 --- /dev/null +++ b/pact-spring-data-rest-provider/src/test/java/com/example/framework/PactProperties.java @@ -0,0 +1,61 @@ +package com.example.framework; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@ConfigurationProperties("pact") +public class PactProperties { + + private Map databaseStates; + + /** + * Retrieves a map with the names of the configured database states as keys and {@link DatabaseState} objects + * as values. + */ + public List getDatabaseStatesList() { + List databaseStatesList = new ArrayList<>(); + for (Map.Entry entry : databaseStates.entrySet()) { + String stateName = entry.getKey(); + + // When reading a property as a Map, as is done for databaseStates, Spring Boot automatically adds numeric + // keys and moves the actual key into the value, separated by a comma. Thus, we have all entries duplicated + // and have to remove the entries with numeric keys. + + if(!stateName.matches("^[0-9]+$")) { + String sqlScriptsString = entry.getValue(); + String[] sqlScripts = sqlScriptsString.split(","); + databaseStatesList.add(new DatabaseState(stateName, sqlScripts)); + } + } + return databaseStatesList; + } + + + static List toCommandLineArguments(List databaseStates) { + List args = new ArrayList<>(); + for (DatabaseState databaseState : databaseStates) { + String argString = String.format("--pact.databaseStates.%s=", databaseState.getStateName()); + int i = 0; + for (String scriptPath : databaseState.getSqlscripts()) { + argString += String.format("%s", scriptPath); + if (i < databaseState.getSqlscripts().length - 1) { + argString += ","; + } + i++; + } + args.add(argString); + } + return args; + } + + public Map getDatabaseStates() { + return databaseStates; + } + + public void setDatabaseStates(Map databaseStates) { + this.databaseStates = databaseStates; + } +} diff --git a/pact-spring-data-rest-provider/src/test/java/com/example/framework/SpringBootStarter.java b/pact-spring-data-rest-provider/src/test/java/com/example/framework/SpringBootStarter.java new file mode 100644 index 0000000..c4f23df --- /dev/null +++ b/pact-spring-data-rest-provider/src/test/java/com/example/framework/SpringBootStarter.java @@ -0,0 +1,111 @@ +package com.example.framework; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ApplicationContext; + +import java.util.ArrayList; +import java.util.List; + +/** + *

+ * Starts a Spring Boot application. + *

+ *

+ * When included in a JUnit test with the {@link org.junit.ClassRule} annotation as in the example below, the Spring Boot application will be + * started before any of the test methods are run. + *

+ * public class MyTest {
+ *
+ *   @ClassRule
+ *   public static SpringBootStarter starter = SpringBootStarter.builder()
+ *     .withApplicationClass(MyApplication.class)
+ *     ...
+ *     .build();
+ *
+ *   @Test
+ *   public void test(){
+ *     ...
+ *   }
+ *
+ * }
+ * 
+ *

+ */ +public class SpringBootStarter implements TestRule { + + private final Class applicationClass; + + private final List args; + + private final List databaseStates; + + /** + * Constructor. + * + * @param applicationClass the Spring Boot application class. + * @param databaseStates list containing {@link DatabaseState} objects, each describing a database state + * in form of one or more SQL scripts. + * @param args the command line arguments the application should be started with. + */ + public SpringBootStarter(Class applicationClass, List databaseStates, List args) { + this.args = args; + this.applicationClass = applicationClass; + this.databaseStates = databaseStates; + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + List args = new ArrayList<>(); + args.addAll(SpringBootStarter.this.args); + args.addAll(PactProperties.toCommandLineArguments(SpringBootStarter.this.databaseStates)); + ApplicationContext context = SpringApplication.run(SpringBootStarter.this.applicationClass, args.toArray(new String[]{})); + base.evaluate(); + SpringApplication.exit(context); + } + }; + } + + /** + * Creates a builder that provides a fluent API to create a new {@link SpringBootStarter} instance. + */ + public static SpringBootStarterBuilder builder() { + return new SpringBootStarterBuilder(); + } + + public static class SpringBootStarterBuilder { + + private Class applicationClass; + + private List args = new ArrayList<>(); + + private List databaseStates = new ArrayList<>(); + + public SpringBootStarterBuilder withApplicationClass(Class clazz) { + this.applicationClass = clazz; + return this; + } + + public SpringBootStarterBuilder withArgument(String argument) { + this.args.add(argument); + return this; + } + + public SpringBootStarterBuilder withDatabaseState(String stateName, String... sqlScripts) { + this.databaseStates.add(new DatabaseState(stateName, sqlScripts)); + return this; + } + + public SpringBootStarter build() { + return new SpringBootStarter(this.applicationClass, this.databaseStates, args); + } + + } + + +} diff --git a/pact-spring-data-rest-provider/src/test/java/com/example/framework/SpringBootStarterBuilder.java b/pact-spring-data-rest-provider/src/test/java/com/example/framework/SpringBootStarterBuilder.java new file mode 100644 index 0000000..f1ae0d2 --- /dev/null +++ b/pact-spring-data-rest-provider/src/test/java/com/example/framework/SpringBootStarterBuilder.java @@ -0,0 +1,34 @@ +package com.example.framework; + +import java.util.ArrayList; +import java.util.List; + +public class SpringBootStarterBuilder { + + private Class applicationClass; + + private List args = new ArrayList<>(); + + private List databaseStates = new ArrayList<>(); + + public SpringBootStarterBuilder withApplicationClass(Class clazz) { + this.applicationClass = clazz; + return this; + } + + public SpringBootStarterBuilder withArgument(String argument) { + this.args.add(argument); + return this; + } + + public SpringBootStarterBuilder withDatabaseState(String stateName, String... sqlScripts) { + this.databaseStates.add(new DatabaseState(stateName, sqlScripts)); + return this; + } + + public SpringBootStarter build() { + return new SpringBootStarter(this.applicationClass, this.databaseStates, args); + } + + +} diff --git a/pact-spring-data-rest-provider/src/test/resources/META-INF/spring.factories b/pact-spring-data-rest-provider/src/test/resources/META-INF/spring.factories new file mode 100644 index 0000000..6c46f1e --- /dev/null +++ b/pact-spring-data-rest-provider/src/test/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.framework.PactDatabaseStatesAutoConfiguration \ No newline at end of file diff --git a/pact-spring-data-rest-provider/src/test/resources/address-collection.sql b/pact-spring-data-rest-provider/src/test/resources/address-collection.sql new file mode 100644 index 0000000..fcccfb1 --- /dev/null +++ b/pact-spring-data-rest-provider/src/test/resources/address-collection.sql @@ -0,0 +1,2 @@ +insert into address (id, street) values (1, 'Elm Street'); +insert into address (id, street) values (2, 'High Street'); diff --git a/pact-spring-data-rest-provider/src/test/resources/application-pact.properties b/pact-spring-data-rest-provider/src/test/resources/application-pact.properties new file mode 100644 index 0000000..7e62d4c --- /dev/null +++ b/pact-spring-data-rest-provider/src/test/resources/application-pact.properties @@ -0,0 +1,6 @@ +server.port=8080 +#spring.jpa.generate-ddl=false +spring.jpa.hibernate.ddl-auto=none + +pact.databaseStates[0]=single-address,/initial-schema.sql,/single-address.sql +pact.databaseStates[1]=address-collection,/initial-schema.sql,/address-collection.sql \ No newline at end of file diff --git a/pact-spring-data-rest-provider/src/test/resources/initial-schema.sql b/pact-spring-data-rest-provider/src/test/resources/initial-schema.sql new file mode 100644 index 0000000..86bc85f --- /dev/null +++ b/pact-spring-data-rest-provider/src/test/resources/initial-schema.sql @@ -0,0 +1,11 @@ +create table CUSTOMER ( + id NUMBER, + name VARCHAR +); + +create table ADDRESS ( + id NUMBER, + customer_id NUMBER, + street VARCHAR, + FOREIGN KEY (customer_id) REFERENCES CUSTOMER(id) +); diff --git a/pact-spring-data-rest-provider/src/test/resources/single-address.sql b/pact-spring-data-rest-provider/src/test/resources/single-address.sql new file mode 100644 index 0000000..fc92d9b --- /dev/null +++ b/pact-spring-data-rest-provider/src/test/resources/single-address.sql @@ -0,0 +1 @@ +insert into address (id, street) values (1, 'Elm Street'); diff --git a/settings.gradle b/settings.gradle index bd1cd75..b434685 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,3 +2,7 @@ include 'spring-data-rest-associations' include 'feign-with-spring-data-rest' include 'spring-data-rest-springfox' +// pact-feign-consumer must run before pact-spring-data-rest-provider because it creates a shared pact file +include 'pact-feign-consumer' +include 'pact-spring-data-rest-provider' + diff --git a/spring-data-rest-associations/README.md b/spring-data-rest-associations/README.md index 7a66407..5665b50 100644 --- a/spring-data-rest-associations/README.md +++ b/spring-data-rest-associations/README.md @@ -4,6 +4,8 @@ This repo contains some example code that exposes some Spring Data repositories via Spring Data REST to show how to handle relationships between JPA entities via the REST API. +## Companion Blog Post + The companion blog post with more details can be found [here](https://reflectoring.io/relations-with-spring-data-rest/). ## Running the application diff --git a/spring-data-rest-springfox/README.md b/spring-data-rest-springfox/README.md index 15b7e3b..be4604b 100644 --- a/spring-data-rest-springfox/README.md +++ b/spring-data-rest-springfox/README.md @@ -3,6 +3,8 @@ This repo contains some example code that exposes some Spring Data repositories via Spring Data REST and creates a documentation of that API using Springfox. +## Companion Blog Post + The companion blog post with more details can be found [here](https://reflectoring.io/documenting-spring-data-rest-api-with-springfox/). ## Running the application