diff --git a/docs/src/main/asciidoc/verifier/contract.adoc b/docs/src/main/asciidoc/verifier/contract.adoc index 3537d59cf0..24d60699c9 100644 --- a/docs/src/main/asciidoc/verifier/contract.adoc +++ b/docs/src/main/asciidoc/verifier/contract.adoc @@ -705,6 +705,114 @@ and the YAML implementation include::{verifier_core_path}/src/test/groovy/org/springframework/cloud/contract/verifier/converter/YamlContractConverter.groovy[indent=0,lines=16..-1] ---- +===== Pact converter + +Spring Cloud Contract comes with an out of the box support for https://docs.pact.io/[Pact] representation of contracts. +In other words instead of using the Groovy DSL you can use Pact files. In this section +we will present how to add such a support for your project. + +====== Pact contract + +We will be working on the following example of a Pact contract. We've placed this file under +the `src/test/resources/contracts` folder. + +[source,javascript,indent=0] +---- +include::{standalone_pact_path}/pact-http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.json[indent=0] +---- + +====== Pact for producers + +On the producer side you have add to your plugin configuration two additional dependencies. +One is the Spring Cloud Contract Pact support and the other represents the current +Pact version that you're using. + +[source,xml,indent=0,subs="verbatim,attributes",role="primary"] +.Maven +---- +include::{standalone_pact_path}/pact-http-server/pom.xml[tags=pact_dependency,indent=0] +---- + +[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] +.Gradle +---- +include::{standalone_pact_path}/pact-http-server/build.gradle[tags=pact_dependency,indent=0] +---- + +When you execute the build of your application a test, looking more or less like this, will be generated + +[source,java,indent=0] +---- + @Test + public void validate_shouldMarkClientAsFraud() throws Exception { + // given: + MockMvcRequestSpecification request = given() + .header("Content-Type", "application/vnd.fraud.v1+json") + .body("{\"clientId\":\"1234567890\",\"loanAmount\":99999}"); + + // when: + ResponseOptions response = given().spec(request) + .put("/fraudcheck"); + + // then: + assertThat(response.statusCode()).isEqualTo(200); + assertThat(response.header("Content-Type")).isEqualTo("application/vnd.fraud.v1+json;charset=UTF-8"); + // and: + DocumentContext parsedJson = JsonPath.parse(response.getBody().asString()); + assertThatJson(parsedJson).field("rejectionReason").isEqualTo("Amount too high"); + // and: + assertThat(parsedJson.read("$.fraudCheckStatus", String.class)).matches("FRAUD"); + } +---- + +and the stub looking like this + +[source,javascript,indent=0] +---- +{ + "uuid" : "996ae5ae-6834-4db6-8fac-358ca187ab62", + "request" : { + "url" : "/fraudcheck", + "method" : "PUT", + "headers" : { + "Content-Type" : { + "equalTo" : "application/vnd.fraud.v1+json" + } + }, + "bodyPatterns" : [ { + "matchesJsonPath" : "$[?(@.loanAmount == 99999)]" + }, { + "matchesJsonPath" : "$[?(@.clientId =~ /([0-9]{10})/)]" + } ] + }, + "response" : { + "status" : 200, + "body" : "{\"fraudCheckStatus\":\"FRAUD\",\"rejectionReason\":\"Amount too high\"}", + "headers" : { + "Content-Type" : "application/vnd.fraud.v1+json;charset=UTF-8" + } + } +} +---- + +====== Pact for consumers + +On the producer side you have add to your project dependencies two additional dependencies. +One is the Spring Cloud Contract Pact support and the other represents the current +Pact version that you're using. + +[source,xml,indent=0,subs="verbatim,attributes",role="primary"] +.Maven +---- +include::{standalone_pact_path}/pact-http-client/pom.xml[tags=pact_dependency,indent=0] +---- + +[source,groovy,indent=0,subs="verbatim,attributes",role="secondary"] +.Gradle +---- +include::{standalone_pact_path}/pact-http-client/build.gradle[tags=pact_dependency,indent=0] +---- + ==== Custom test generator If you want to generate tests for different languages than Java or you're diff --git a/docs/src/main/asciidoc/verifier/spring-cloud-contract-verifier.adoc b/docs/src/main/asciidoc/verifier/spring-cloud-contract-verifier.adoc index 1c921cbfbb..2d1c9fe47a 100644 --- a/docs/src/main/asciidoc/verifier/spring-cloud-contract-verifier.adoc +++ b/docs/src/main/asciidoc/verifier/spring-cloud-contract-verifier.adoc @@ -8,6 +8,7 @@ :stubrunner_core_path: {core_path}/spring-cloud-contract-stub-runner :standalone_samples_path: {samples_path}/standalone/dsl :standalone_messaging_samples_path: {samples_path}/standalone/messaging +:standalone_pact_path: {samples_path}/standalone/pact :tests_path: {core_path}/tests :samples_url: https://raw.githubusercontent.com/spring-cloud-samples/spring-cloud-contract-samples/master diff --git a/pom.xml b/pom.xml index abec0751fd..a397f70e9c 100644 --- a/pom.xml +++ b/pom.xml @@ -127,6 +127,16 @@ hoverfly-junit 0.1.8 + + org.apache.commons + commons-lang3 + 3.4 + + + au.com.dius + pact-jvm-model + 2.4.18 + org.springframework.cloud spring-cloud-contract-dependencies diff --git a/samples/standalone/pact/pact-http-client/.gitignore b/samples/standalone/pact/pact-http-client/.gitignore new file mode 100644 index 0000000000..b05ba7bd6d --- /dev/null +++ b/samples/standalone/pact/pact-http-client/.gitignore @@ -0,0 +1,6 @@ + +target/ + +.gradle +build/ + diff --git a/samples/standalone/pact/pact-http-client/.mvn/jvm.config b/samples/standalone/pact/pact-http-client/.mvn/jvm.config new file mode 100644 index 0000000000..894bef17a5 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/.mvn/jvm.config @@ -0,0 +1 @@ +-Xmx1024m -XX:MaxPermSize=256m -Djava.awt.headless=true \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-client/.mvn/maven.config b/samples/standalone/pact/pact-http-client/.mvn/maven.config new file mode 100644 index 0000000000..affad39a42 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/.mvn/maven.config @@ -0,0 +1 @@ +-T2 \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-client/.mvn/wrapper/maven-wrapper.jar b/samples/standalone/pact/pact-http-client/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000..c6feb8bb6f Binary files /dev/null and b/samples/standalone/pact/pact-http-client/.mvn/wrapper/maven-wrapper.jar differ diff --git a/samples/standalone/pact/pact-http-client/.mvn/wrapper/maven-wrapper.properties b/samples/standalone/pact/pact-http-client/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..6637cedb28 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-client/README.adoc b/samples/standalone/pact/pact-http-client/README.adoc new file mode 100644 index 0000000000..021bcdcdb5 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/README.adoc @@ -0,0 +1,26 @@ += Pact Http Client + +== Prerequisites + +First you have to publish to Maven Local the stubs of the *pact-http-server* module + +== How to run it? + +Run + +[source=groovy] +-------- +./gradlew clean build +-------- + +or + +-------- +./mvnw clean package +-------- + +To + +- build the app +- use spring-cloud-contract-stub-runner-spring[Stub Runner Spring] to download the stub of `Pact Http Server` +- run the tests against stubbed server \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-client/build.gradle b/samples/standalone/pact/pact-http-client/build.gradle new file mode 100644 index 0000000000..e9495023e8 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/build.gradle @@ -0,0 +1,68 @@ +buildscript { + repositories { + mavenCentral() + mavenLocal() + maven { url "http://repo.spring.io/snapshot" } + maven { url "http://repo.spring.io/milestone" } + maven { url "http://repo.spring.io/release" } + } + dependencies { + classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.0.BUILD-SNAPSHOT" + } +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +repositories { + mavenCentral() + mavenLocal() + maven { url "http://repo.spring.io/snapshot" } + maven { url "http://repo.spring.io/milestone" } + maven { url "http://repo.spring.io/release" } +} + +apply plugin: 'groovy' +apply plugin: 'spring-boot' +apply plugin: 'maven-publish' + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:$BOM_VERSION" + } +} + +dependencies { + compile("org.springframework.boot:spring-boot-starter-web") + compile("org.springframework.boot:spring-boot-starter-actuator") + + testCompile "org.springframework.cloud:spring-cloud-starter-contract-stub-runner" + //tag::pact_dependency[] + testCompile "org.springframework.cloud:spring-cloud-contract-spec-pact" + testCompile 'au.com.dius:pact-jvm-model:2.4.18' + //end::pact_dependency[] +} + +test { + systemProperty 'spring.profiles.active', 'gradle' + testLogging { + exceptionFormat = 'full' + } +} + +task wrapper(type: Wrapper) { + gradleVersion = '2.14' +} + +task resolveDependencies { + doLast { + project.rootProject.allprojects.each { subProject -> + subProject.buildscript.configurations.each { configuration -> + configuration.resolve() + } + subProject.configurations.each { configuration -> + configuration.resolve() + } + } + } +} diff --git a/samples/standalone/pact/pact-http-client/gradle.properties b/samples/standalone/pact/pact-http-client/gradle.properties new file mode 100644 index 0000000000..42cbd5f9cc --- /dev/null +++ b/samples/standalone/pact/pact-http-client/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.daemon=false +BOM_VERSION=Dalston.BUILD-SNAPSHOT \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-client/gradle/wrapper/gradle-wrapper.jar b/samples/standalone/pact/pact-http-client/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..3baa851b28 Binary files /dev/null and b/samples/standalone/pact/pact-http-client/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/standalone/pact/pact-http-client/gradle/wrapper/gradle-wrapper.properties b/samples/standalone/pact/pact-http-client/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..ff3de523bd --- /dev/null +++ b/samples/standalone/pact/pact-http-client/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Aug 19 15:39:05 CEST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.0-bin.zip diff --git a/samples/standalone/pact/pact-http-client/gradlew b/samples/standalone/pact/pact-http-client/gradlew new file mode 100755 index 0000000000..27309d9231 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## 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 + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/standalone/pact/pact-http-client/gradlew.bat b/samples/standalone/pact/pact-http-client/gradlew.bat new file mode 100644 index 0000000000..832fdb6079 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/gradlew.bat @@ -0,0 +1,90 @@ +@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 +if "%@eval[2+2]" == "4" goto 4NT_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=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +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/samples/standalone/pact/pact-http-client/mvnw b/samples/standalone/pact/pact-http-client/mvnw new file mode 100755 index 0000000000..fc7efd17d0 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/mvnw @@ -0,0 +1,234 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # + # Look for the Apple JDKs first to preserve the existing behaviour, and then look + # for the new JDKs provided by Oracle. + # + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then + # + # Oracle JDKs + # + export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then + # + # Apple JDKs + # + export JAVA_HOME=`/usr/libexec/java_home` + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + 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 + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + local basedir=$(pwd) + local wdir=$(pwd) + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + wdir=$(cd "$wdir/.."; pwd) + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CMD_LINE_ARGS + diff --git a/samples/standalone/pact/pact-http-client/mvnw.cmd b/samples/standalone/pact/pact-http-client/mvnw.cmd new file mode 100644 index 0000000000..001048081d --- /dev/null +++ b/samples/standalone/pact/pact-http-client/mvnw.cmd @@ -0,0 +1,145 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +set MAVEN_CMD_LINE_ARGS=%MAVEN_CONFIG% %* + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR=""%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/samples/standalone/pact/pact-http-client/pom.xml b/samples/standalone/pact/pact-http-client/pom.xml new file mode 100644 index 0000000000..af94bb7204 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/pom.xml @@ -0,0 +1,217 @@ + + + 4.0.0 + + com.example + pact-http-client + 0.0.1-SNAPSHOT + + Spring Cloud Contract Verifier Http Client Sample with Pact + Spring Cloud Contract Verifier Http Client Sample with Pact + + + org.springframework.boot + spring-boot-starter-parent + 1.5.0.BUILD-SNAPSHOT + + + + + UTF-8 + 1.8 + Dalston.BUILD-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.apache.httpcomponents + httpclient + + + + org.springframework.cloud + spring-cloud-starter-contract-stub-runner + test + + + + + org.springframework.cloud + spring-cloud-contract-spec-pact + test + + + au.com.dius + pact-jvm-model + 2.4.18 + test + + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud-dependencies.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + maven-deploy-plugin + + true + + + + org.apache.maven.plugins + maven-clean-plugin + 3.0.0 + + + + build + + + target + + + + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + + integration + + + + org.codehaus.mojo + exec-maven-plugin + + + gradle + test + + ./gradlew + + clean + build + publishToMavenLocal + -PverifierVersion=${spring-cloud-contract.version} + + + + exec + + + + + + + + + windows + + + + org.codehaus.mojo + exec-maven-plugin + + + gradle + test + + gradlew.bat + + clean + build + publishToMavenLocal + -PverifierVersion=${spring-cloud-contract.version} + + + + exec + + + + + + + + + + diff --git a/samples/standalone/pact/pact-http-client/settings.gradle b/samples/standalone/pact/pact-http-client/settings.gradle new file mode 100644 index 0000000000..d351972414 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'pact-http-client-gradle' diff --git a/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/Application.java b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/Application.java new file mode 100644 index 0000000000..6f36301172 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/Application.java @@ -0,0 +1,13 @@ +package com.example.loan; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/LoanApplicationService.java b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/LoanApplicationService.java new file mode 100644 index 0000000000..c91df67aea --- /dev/null +++ b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/LoanApplicationService.java @@ -0,0 +1,95 @@ +package com.example.loan; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.example.loan.model.FraudCheckStatus; +import com.example.loan.model.FraudServiceRequest; +import com.example.loan.model.FraudServiceResponse; +import com.example.loan.model.LoanApplication; +import com.example.loan.model.LoanApplicationResult; +import com.example.loan.model.LoanApplicationStatus; +import com.example.loan.model.Response; + +@Service +public class LoanApplicationService { + + private static final String FRAUD_SERVICE_JSON_VERSION_1 = + "application/vnd.fraud.v1+json"; + + private final RestTemplate restTemplate; + + private int port = 6565; + + @Autowired + public LoanApplicationService(RestTemplateBuilder builder) { + this.restTemplate = builder.build(); + } + + public LoanApplicationResult loanApplication(LoanApplication loanApplication) { + FraudServiceRequest request = + new FraudServiceRequest(loanApplication); + + FraudServiceResponse response = + sendRequestToFraudDetectionService(request); + + return buildResponseFromFraudResult(response); + } + + private FraudServiceResponse sendRequestToFraudDetectionService( + FraudServiceRequest request) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HttpHeaders.CONTENT_TYPE, FRAUD_SERVICE_JSON_VERSION_1); + + // tag::client_call_server[] + ResponseEntity response = + restTemplate.exchange("http://localhost:" + port + "/fraudcheck", HttpMethod.PUT, + new HttpEntity<>(request, httpHeaders), + FraudServiceResponse.class); + // end::client_call_server[] + + return response.getBody(); + } + + private LoanApplicationResult buildResponseFromFraudResult(FraudServiceResponse response) { + LoanApplicationStatus applicationStatus = null; + if (FraudCheckStatus.OK == response.getFraudCheckStatus()) { + applicationStatus = LoanApplicationStatus.LOAN_APPLIED; + } else if (FraudCheckStatus.FRAUD == response.getFraudCheckStatus()) { + applicationStatus = LoanApplicationStatus.LOAN_APPLICATION_REJECTED; + } + + return new LoanApplicationResult(applicationStatus, response.getRejectionReason()); + } + + public int countAllFrauds() { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HttpHeaders.CONTENT_TYPE, FRAUD_SERVICE_JSON_VERSION_1); + ResponseEntity response = + restTemplate.exchange("http://localhost:" + port + "/frauds", HttpMethod.GET, + new HttpEntity<>(httpHeaders), + Response.class); + return response.getBody().getCount(); + } + + public int countDrunks() { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HttpHeaders.CONTENT_TYPE, FRAUD_SERVICE_JSON_VERSION_1); + ResponseEntity response = + restTemplate.exchange("http://localhost:" + port + "/drunks", HttpMethod.GET, + new HttpEntity<>(httpHeaders), + Response.class); + return response.getBody().getCount(); + } + + public void setPort(int port) { + this.port = port; + } + +} diff --git a/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/Client.java b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/Client.java new file mode 100644 index 0000000000..abf66106b5 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/Client.java @@ -0,0 +1,21 @@ +package com.example.loan.model; + +public class Client { + + private String pesel; + + public Client() { + } + + public Client(String pesel) { + this.pesel = pesel; + } + + public String getPesel() { + return pesel; + } + + public void setPesel(String pesel) { + this.pesel = pesel; + } +} diff --git a/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/FraudCheckStatus.java b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/FraudCheckStatus.java new file mode 100644 index 0000000000..c5217ef471 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/FraudCheckStatus.java @@ -0,0 +1,5 @@ +package com.example.loan.model; + +public enum FraudCheckStatus { + OK, FRAUD +} diff --git a/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/FraudServiceRequest.java b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/FraudServiceRequest.java new file mode 100644 index 0000000000..0230540131 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/FraudServiceRequest.java @@ -0,0 +1,34 @@ +package com.example.loan.model; + +import java.math.BigDecimal; + +public class FraudServiceRequest { + + private String clientId; + + private BigDecimal loanAmount; + + public FraudServiceRequest() { + } + + public FraudServiceRequest(LoanApplication loanApplication) { + this.clientId = loanApplication.getClient().getPesel(); + this.loanAmount = loanApplication.getAmount(); + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public BigDecimal getLoanAmount() { + return loanAmount; + } + + public void setLoanAmount(BigDecimal loanAmount) { + this.loanAmount = loanAmount; + } +} diff --git a/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/FraudServiceResponse.java b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/FraudServiceResponse.java new file mode 100644 index 0000000000..50e64478b9 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/FraudServiceResponse.java @@ -0,0 +1,27 @@ +package com.example.loan.model; + +public class FraudServiceResponse { + + private FraudCheckStatus fraudCheckStatus; + + private String rejectionReason; + + public FraudServiceResponse() { + } + + public FraudCheckStatus getFraudCheckStatus() { + return fraudCheckStatus; + } + + public void setFraudCheckStatus(FraudCheckStatus fraudCheckStatus) { + this.fraudCheckStatus = fraudCheckStatus; + } + + public String getRejectionReason() { + return rejectionReason; + } + + public void setRejectionReason(String rejectionReason) { + this.rejectionReason = rejectionReason; + } +} diff --git a/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/LoanApplication.java b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/LoanApplication.java new file mode 100644 index 0000000000..27d66f884c --- /dev/null +++ b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/LoanApplication.java @@ -0,0 +1,44 @@ +package com.example.loan.model; + +import java.math.BigDecimal; + +public class LoanApplication { + + private Client client; + + private BigDecimal amount; + + private String loanApplicationId; + + public LoanApplication() { + } + + public LoanApplication(Client client, double amount) { + this.client = client; + this.amount = BigDecimal.valueOf(amount); + } + + public Client getClient() { + return client; + } + + public void setClient(Client client) { + this.client = client; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + + public String getLoanApplicationId() { + return loanApplicationId; + } + + public void setLoanApplicationId(String loanApplicationId) { + this.loanApplicationId = loanApplicationId; + } +} diff --git a/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/LoanApplicationResult.java b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/LoanApplicationResult.java new file mode 100644 index 0000000000..ffd44e1a26 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/LoanApplicationResult.java @@ -0,0 +1,32 @@ +package com.example.loan.model; + +public class LoanApplicationResult { + + private LoanApplicationStatus loanApplicationStatus; + + private String rejectionReason; + + public LoanApplicationResult() { + } + + public LoanApplicationResult(LoanApplicationStatus loanApplicationStatus, String rejectionReason) { + this.loanApplicationStatus = loanApplicationStatus; + this.rejectionReason = rejectionReason; + } + + public LoanApplicationStatus getLoanApplicationStatus() { + return loanApplicationStatus; + } + + public void setLoanApplicationStatus(LoanApplicationStatus loanApplicationStatus) { + this.loanApplicationStatus = loanApplicationStatus; + } + + public String getRejectionReason() { + return rejectionReason; + } + + public void setRejectionReason(String rejectionReason) { + this.rejectionReason = rejectionReason; + } +} diff --git a/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/LoanApplicationStatus.java b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/LoanApplicationStatus.java new file mode 100644 index 0000000000..34cf91a89f --- /dev/null +++ b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/LoanApplicationStatus.java @@ -0,0 +1,5 @@ +package com.example.loan.model; + +public enum LoanApplicationStatus { + LOAN_APPLIED, LOAN_APPLICATION_REJECTED +} diff --git a/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/Response.java b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/Response.java new file mode 100644 index 0000000000..8335ee0cc6 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/src/main/java/com/example/loan/model/Response.java @@ -0,0 +1,20 @@ +package com.example.loan.model; + +public class Response { + private int count; + + public Response(int count) { + this.count = count; + } + + public Response() { + } + + public int getCount() { + return this.count; + } + + public void setCount(int count) { + this.count = count; + } +} \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-client/src/main/resources/application.yml b/samples/standalone/pact/pact-http-client/src/main/resources/application.yml new file mode 100644 index 0000000000..4b7fe13158 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/src/main/resources/application.yml @@ -0,0 +1 @@ +server.port: 8090 \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java b/samples/standalone/pact/pact-http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java new file mode 100644 index 0000000000..9877692b94 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/src/test/java/com/example/loan/LoanApplicationServiceTests.java @@ -0,0 +1,80 @@ +package com.example.loan; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Before; +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.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner; +import org.springframework.core.env.Environment; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import com.example.loan.model.Client; +import com.example.loan.model.LoanApplication; +import com.example.loan.model.LoanApplicationResult; +import com.example.loan.model.LoanApplicationStatus; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment=WebEnvironment.NONE) +@AutoConfigureStubRunner(ids = {"com.example:pact-http-server:+:stubs"}, workOffline = true) +@DirtiesContext +public class LoanApplicationServiceTests { + + @Autowired + private LoanApplicationService service; + + @Autowired + private Environment environment; + + @Before + public void setup() { + this.service.setPort(this.environment.getProperty("stubrunner.runningstubs.pact-http-server.port", Integer.class)); + } + + @Test + public void shouldSuccessfullyApplyForLoan() { + // given: + LoanApplication application = new LoanApplication(new Client("1234567890"), + 123.123); + // when: + LoanApplicationResult loanApplication = service.loanApplication(application); + // then: + assertThat(loanApplication.getLoanApplicationStatus()) + .isEqualTo(LoanApplicationStatus.LOAN_APPLIED); + assertThat(loanApplication.getRejectionReason()).isNull(); + } + + @Test + public void shouldBeRejectedDueToAbnormalLoanAmount() { + // given: + LoanApplication application = new LoanApplication(new Client("1234567890"), + 99999); + // when: + LoanApplicationResult loanApplication = service.loanApplication(application); + // then: + assertThat(loanApplication.getLoanApplicationStatus()) + .isEqualTo(LoanApplicationStatus.LOAN_APPLICATION_REJECTED); + assertThat(loanApplication.getRejectionReason()).isEqualTo("Amount too high"); + } + + @Test + public void shouldSuccessfullyGetAllFrauds() { + // when: + int count = service.countAllFrauds(); + // then: + assertThat(count).isEqualTo(200); + } + + @Test + public void shouldSuccessfullyGetAllDrunks() { + // when: + int count = service.countDrunks(); + // then: + assertThat(count).isEqualTo(100); + } + +} diff --git a/samples/standalone/pact/pact-http-client/src/test/resources/application-gradle.yaml b/samples/standalone/pact/pact-http-client/src/test/resources/application-gradle.yaml new file mode 100644 index 0000000000..25f0e2b24a --- /dev/null +++ b/samples/standalone/pact/pact-http-client/src/test/resources/application-gradle.yaml @@ -0,0 +1,3 @@ +stubrunner: + work-offline: true + stubs.ids: 'com.example:pact-http-server-gradle:+:stubs' \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-client/src/test/resources/application.yaml b/samples/standalone/pact/pact-http-client/src/test/resources/application.yaml new file mode 100644 index 0000000000..b4e8858671 --- /dev/null +++ b/samples/standalone/pact/pact-http-client/src/test/resources/application.yaml @@ -0,0 +1,2 @@ +server: + port: 0 diff --git a/samples/standalone/pact/pact-http-server/.gitignore b/samples/standalone/pact/pact-http-server/.gitignore new file mode 100644 index 0000000000..b05ba7bd6d --- /dev/null +++ b/samples/standalone/pact/pact-http-server/.gitignore @@ -0,0 +1,6 @@ + +target/ + +.gradle +build/ + diff --git a/samples/standalone/pact/pact-http-server/.mvn/jvm.config b/samples/standalone/pact/pact-http-server/.mvn/jvm.config new file mode 100644 index 0000000000..894bef17a5 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/.mvn/jvm.config @@ -0,0 +1 @@ +-Xmx1024m -XX:MaxPermSize=256m -Djava.awt.headless=true \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-server/.mvn/maven.config b/samples/standalone/pact/pact-http-server/.mvn/maven.config new file mode 100644 index 0000000000..7681bc67b9 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/.mvn/maven.config @@ -0,0 +1 @@ +-T2 diff --git a/samples/standalone/pact/pact-http-server/.mvn/wrapper/maven-wrapper.jar b/samples/standalone/pact/pact-http-server/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000..c6feb8bb6f Binary files /dev/null and b/samples/standalone/pact/pact-http-server/.mvn/wrapper/maven-wrapper.jar differ diff --git a/samples/standalone/pact/pact-http-server/.mvn/wrapper/maven-wrapper.properties b/samples/standalone/pact/pact-http-server/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..6637cedb28 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-server/README.adoc b/samples/standalone/pact/pact-http-server/README.adoc new file mode 100644 index 0000000000..336b774815 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/README.adoc @@ -0,0 +1,20 @@ += Pact Http Server + +Run + +[source=groovy] +-------- +./gradlew clean build publishToMavenLocal +-------- + +or + +-------- +./mvnw clean install +-------- + +To + +- build the app +- generate and run Spring Cloud Contract Verifier tests +- publish the fatJar and the stubs to Maven Local \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-server/build.gradle b/samples/standalone/pact/pact-http-server/build.gradle new file mode 100644 index 0000000000..bd1b332b51 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/build.gradle @@ -0,0 +1,78 @@ +buildscript { + repositories { + mavenCentral() + mavenLocal() + maven { url "http://repo.spring.io/snapshot" } + maven { url "http://repo.spring.io/milestone" } + maven { url "http://repo.spring.io/release" } + } + dependencies { + classpath "org.springframework.boot:spring-boot-gradle-plugin:1.5.0.BUILD-SNAPSHOT" + classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:${findProperty('verifierVersion') ?: verifierVersion}" + //tag::pact_dependency[] + classpath "org.springframework.cloud:spring-cloud-contract-spec-pact:${findProperty('verifierVersion') ?: verifierVersion}" + classpath 'au.com.dius:pact-jvm-model:2.4.18' + //end::pact_dependency[] + } +} + +group = 'com.example' +version = '0.0.1-SNAPSHOT' + +repositories { + mavenCentral() + mavenLocal() + maven { url "http://repo.spring.io/snapshot" } + maven { url "http://repo.spring.io/milestone" } + maven { url "http://repo.spring.io/release" } +} + +apply plugin: 'groovy' +apply plugin: 'spring-boot' +apply plugin: 'spring-cloud-contract' +apply plugin: 'maven-publish' + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:$BOM_VERSION" + } +} + +contracts { + packageWithBaseClasses = 'com.example.fraud' +} + +dependencies { + compile("org.springframework.boot:spring-boot-starter-web") + compile("org.springframework.boot:spring-boot-starter-actuator") + + testCompile 'org.springframework.cloud:spring-cloud-starter-contract-verifier' +} + +test { + systemProperty 'spring.profiles.active', 'gradle' + testLogging { + exceptionFormat = 'full' + } +} + +task wrapper(type: Wrapper) { + gradleVersion = '2.14' +} + +clean.doFirst { + delete "~/.m2/repository/com/example/http-server-pact-gradle" +} + +task resolveDependencies { + doLast { + project.rootProject.allprojects.each { subProject -> + subProject.buildscript.configurations.each { configuration -> + configuration.resolve() + } + subProject.configurations.each { configuration -> + configuration.resolve() + } + } + } +} diff --git a/samples/standalone/pact/pact-http-server/gradle.properties b/samples/standalone/pact/pact-http-server/gradle.properties new file mode 100644 index 0000000000..38dc897f59 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.daemon=false +verifierVersion=1.1.0.BUILD-SNAPSHOT +BOM_VERSION=Dalston.BUILD-SNAPSHOT \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-server/gradle/wrapper/gradle-wrapper.jar b/samples/standalone/pact/pact-http-server/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..3baa851b28 Binary files /dev/null and b/samples/standalone/pact/pact-http-server/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/standalone/pact/pact-http-server/gradle/wrapper/gradle-wrapper.properties b/samples/standalone/pact/pact-http-server/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..95d7279349 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Aug 19 15:38:58 CEST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.0-bin.zip diff --git a/samples/standalone/pact/pact-http-server/gradlew b/samples/standalone/pact/pact-http-server/gradlew new file mode 100755 index 0000000000..27309d9231 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## 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 + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/standalone/pact/pact-http-server/gradlew.bat b/samples/standalone/pact/pact-http-server/gradlew.bat new file mode 100644 index 0000000000..832fdb6079 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/gradlew.bat @@ -0,0 +1,90 @@ +@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 +if "%@eval[2+2]" == "4" goto 4NT_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=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +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/samples/standalone/pact/pact-http-server/mvnw b/samples/standalone/pact/pact-http-server/mvnw new file mode 100755 index 0000000000..fc7efd17d0 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/mvnw @@ -0,0 +1,234 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # + # Look for the Apple JDKs first to preserve the existing behaviour, and then look + # for the new JDKs provided by Oracle. + # + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then + # + # Oracle JDKs + # + export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then + # + # Apple JDKs + # + export JAVA_HOME=`/usr/libexec/java_home` + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + 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 + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + local basedir=$(pwd) + local wdir=$(pwd) + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + wdir=$(cd "$wdir/.."; pwd) + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CMD_LINE_ARGS + diff --git a/samples/standalone/pact/pact-http-server/mvnw.cmd b/samples/standalone/pact/pact-http-server/mvnw.cmd new file mode 100644 index 0000000000..001048081d --- /dev/null +++ b/samples/standalone/pact/pact-http-server/mvnw.cmd @@ -0,0 +1,145 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +set MAVEN_CMD_LINE_ARGS=%MAVEN_CONFIG% %* + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" + +set WRAPPER_JAR=""%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/samples/standalone/pact/pact-http-server/pom.xml b/samples/standalone/pact/pact-http-server/pom.xml new file mode 100644 index 0000000000..26ead7abd4 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/pom.xml @@ -0,0 +1,259 @@ + + + 4.0.0 + + com.example + pact-http-server + 0.0.1-SNAPSHOT + + Spring Cloud Contract Verifier Http Server Sample with Pact + Spring Cloud Contract Verifier Http Server Sample with Pact + + + org.springframework.boot + spring-boot-starter-parent + 1.5.0.BUILD-SNAPSHOT + + + + + UTF-8 + 1.8 + 1.1.0.BUILD-SNAPSHOT + Dalston.BUILD-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.cloud + spring-cloud-starter-contract-verifier + test + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud-dependencies.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.springframework.cloud + spring-cloud-contract-maven-plugin + ${spring-cloud-contract.version} + true + + com.example.fraud + + + + org.springframework.cloud + spring-cloud-contract-spec-pact + ${spring-cloud-contract.version} + + + au.com.dius + pact-jvm-model + 2.4.18 + + + + + + maven-deploy-plugin + + true + + + + org.apache.maven.plugins + maven-clean-plugin + 3.0.0 + + + + build + + + target + + + + + + + + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + org.springframework.cloud + spring-cloud-contract-maven-plugin + [1.1.0.BUILD-SNAPSHOT,) + + convert + generateTests + + + + + + + + + + + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + + + integration + + + + org.codehaus.mojo + exec-maven-plugin + + + gradle + test + + ./gradlew + + clean + build + publishToMavenLocal + -PverifierVersion=${spring-cloud-contract.version} + + + + exec + + + + + + + + + windows + + + + org.codehaus.mojo + exec-maven-plugin + + + gradle + test + + gradlew.bat + + clean + build + publishToMavenLocal + -PverifierVersion=${spring-cloud-contract.version} + + + + exec + + + + + + + + + diff --git a/samples/standalone/pact/pact-http-server/settings.gradle b/samples/standalone/pact/pact-http-server/settings.gradle new file mode 100644 index 0000000000..9c202c3e04 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'pact-http-server-gradle' diff --git a/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/Application.java b/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/Application.java new file mode 100644 index 0000000000..9117009b09 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/Application.java @@ -0,0 +1,17 @@ +package com.example.fraud; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableAutoConfiguration +@ComponentScan +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/FraudDetectionController.java b/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/FraudDetectionController.java new file mode 100644 index 0000000000..99e7fea84e --- /dev/null +++ b/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/FraudDetectionController.java @@ -0,0 +1,39 @@ +package com.example.fraud; + +import static org.springframework.web.bind.annotation.RequestMethod.PUT; + +import java.math.BigDecimal; + +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.example.fraud.model.FraudCheck; +import com.example.fraud.model.FraudCheckResult; +import com.example.fraud.model.FraudCheckStatus; + +@RestController +public class FraudDetectionController { + + private static final String FRAUD_SERVICE_JSON_VERSION_1 = "application/vnd.fraud.v1+json"; + private static final String NO_REASON = null; + private static final String AMOUNT_TOO_HIGH = "Amount too high"; + private static final BigDecimal MAX_AMOUNT = new BigDecimal("5000"); + + @RequestMapping( + value = "/fraudcheck", + method = PUT, + consumes = FRAUD_SERVICE_JSON_VERSION_1, + produces = FRAUD_SERVICE_JSON_VERSION_1) + public FraudCheckResult fraudCheck(@RequestBody FraudCheck fraudCheck) { + if (amountGreaterThanThreshold(fraudCheck)) { + return new FraudCheckResult(FraudCheckStatus.FRAUD, AMOUNT_TOO_HIGH); + } + return new FraudCheckResult(FraudCheckStatus.OK, NO_REASON); + } + + private boolean amountGreaterThanThreshold(FraudCheck fraudCheck) { + return MAX_AMOUNT.compareTo(fraudCheck.getLoanAmount()) < 0; + } + +} diff --git a/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/FraudStatsController.java b/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/FraudStatsController.java new file mode 100644 index 0000000000..5b27668600 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/FraudStatsController.java @@ -0,0 +1,58 @@ +package com.example.fraud; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class FraudStatsController { + + private static final String FRAUD_SERVICE_JSON_VERSION_1 = "application/vnd.fraud.v1+json"; + + private final StatsProvider statsProvider; + + public FraudStatsController(StatsProvider statsProvider) { + this.statsProvider = statsProvider; + } + + @GetMapping( + value = "/frauds", + produces = FRAUD_SERVICE_JSON_VERSION_1) + public Response countAllFrauds() { + return new Response(this.statsProvider.count(FraudType.ALL)); + } + + @GetMapping( + value = "/drunks", + produces = FRAUD_SERVICE_JSON_VERSION_1) + public Response countAllDrunks() { + return new Response(this.statsProvider.count(FraudType.DRUNKS)); + } +} + +enum FraudType { + DRUNKS, ALL +} + +interface StatsProvider { + int count(FraudType fraudType); +} + +class Response { + private int count; + + public Response(int count) { + this.count = count; + } + + public Response() { + } + + public int getCount() { + return this.count; + } + + public void setCount(int count) { + this.count = count; + } +} + diff --git a/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/model/FraudCheck.java b/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/model/FraudCheck.java new file mode 100644 index 0000000000..b0210573e2 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/model/FraudCheck.java @@ -0,0 +1,29 @@ +package com.example.fraud.model; + +import java.math.BigDecimal; + +public class FraudCheck { + + private String clientId; + + private BigDecimal loanAmount; + + public FraudCheck() { + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public BigDecimal getLoanAmount() { + return loanAmount; + } + + public void setLoanAmount(BigDecimal loanAmount) { + this.loanAmount = loanAmount; + } +} diff --git a/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/model/FraudCheckResult.java b/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/model/FraudCheckResult.java new file mode 100644 index 0000000000..60b340276a --- /dev/null +++ b/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/model/FraudCheckResult.java @@ -0,0 +1,32 @@ +package com.example.fraud.model; + +public class FraudCheckResult { + + private FraudCheckStatus fraudCheckStatus; + + private String rejectionReason; + + public FraudCheckResult() { + } + + public FraudCheckResult(FraudCheckStatus fraudCheckStatus, String rejectionReason) { + this.fraudCheckStatus = fraudCheckStatus; + this.rejectionReason = rejectionReason; + } + + public FraudCheckStatus getFraudCheckStatus() { + return fraudCheckStatus; + } + + public void setFraudCheckStatus(FraudCheckStatus fraudCheckStatus) { + this.fraudCheckStatus = fraudCheckStatus; + } + + public String getRejectionReason() { + return rejectionReason; + } + + public void setRejectionReason(String rejectionReason) { + this.rejectionReason = rejectionReason; + } +} diff --git a/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/model/FraudCheckStatus.java b/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/model/FraudCheckStatus.java new file mode 100644 index 0000000000..5a6adcf274 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/src/main/java/com/example/fraud/model/FraudCheckStatus.java @@ -0,0 +1,5 @@ +package com.example.fraud.model; + +public enum FraudCheckStatus { + OK, FRAUD +} diff --git a/samples/standalone/pact/pact-http-server/src/main/resources/application.yml b/samples/standalone/pact/pact-http-server/src/main/resources/application.yml new file mode 100644 index 0000000000..24466e50d4 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/src/main/resources/application.yml @@ -0,0 +1 @@ +server.port: 0 \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-server/src/test/java/com/example/fraud/FraudBase.java b/samples/standalone/pact/pact-http-server/src/test/java/com/example/fraud/FraudBase.java new file mode 100644 index 0000000000..639d686c2d --- /dev/null +++ b/samples/standalone/pact/pact-http-server/src/test/java/com/example/fraud/FraudBase.java @@ -0,0 +1,30 @@ +package com.example.fraud; + +import org.junit.Before; + +import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc; + +public class FraudBase { + + @Before + public void setup() { + RestAssuredMockMvc.standaloneSetup(new FraudDetectionController(), + new FraudStatsController(stubbedStatsProvider())); + } + + private StatsProvider stubbedStatsProvider() { + return fraudType -> { + switch (fraudType) { + case DRUNKS: + return 100; + case ALL: + return 200; + } + return 0; + }; + } + + public void assertThatRejectionReasonIsNull(Object rejectionReason) { + assert rejectionReason == null; + } +} \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.json b/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.json new file mode 100644 index 0000000000..fc172767cb --- /dev/null +++ b/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.json @@ -0,0 +1,54 @@ +{ + "provider": { + "name": "Provider" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "description": "", + "request": { + "method": "PUT", + "path": "/fraudcheck", + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": { + "clientId": "1234567890", + "loanAmount": 99999 + }, + "matchingRules": { + "$.body.clientId": { + "match": "regex", + "regex": "[0-9]{10}" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8" + }, + "body": { + "fraudCheckStatus": "FRAUD", + "rejectionReason": "Amount too high" + }, + "matchingRules": { + "$.body.fraudCheckStatus": { + "match": "regex", + "regex": "FRAUD" + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "2.0.0" + }, + "pact-jvm": { + "version": "2.4.18" + } + } +} \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldMarkClientAsNotFraud.json b/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldMarkClientAsNotFraud.json new file mode 100644 index 0000000000..0b947bc8c0 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldMarkClientAsNotFraud.json @@ -0,0 +1,42 @@ +{ + "provider": { + "name": "Provider" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "description": "", + "request": { + "method": "PUT", + "path": "/fraudcheck", + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": { + "clientId": "1234567890", + "loanAmount": 123.123 + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8" + }, + "body": { + "fraudCheckStatus": "OK", + "rejectionReason": null + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "2.0.0" + }, + "pact-jvm": { + "version": "2.4.18" + } + } +} \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldReturnDrunksStats.json b/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldReturnDrunksStats.json new file mode 100644 index 0000000000..f939e710c9 --- /dev/null +++ b/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldReturnDrunksStats.json @@ -0,0 +1,34 @@ +{ + "provider": { + "name": "Provider" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "description": "", + "request": { + "method": "GET", + "path": "/drunks" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8" + }, + "body": { + "count": 100 + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "2.0.0" + }, + "pact-jvm": { + "version": "2.4.18" + } + } +} \ No newline at end of file diff --git a/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldReturnFraudStats.json b/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldReturnFraudStats.json new file mode 100644 index 0000000000..160f1e618f --- /dev/null +++ b/samples/standalone/pact/pact-http-server/src/test/resources/contracts/fraud/shouldReturnFraudStats.json @@ -0,0 +1,34 @@ +{ + "provider": { + "name": "Provider" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "description": "", + "request": { + "method": "GET", + "path": "/frauds" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8" + }, + "body": { + "count": 200 + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "2.0.0" + }, + "pact-jvm": { + "version": "2.4.18" + } + } +} \ No newline at end of file diff --git a/samples/standalone/pact/pom.xml b/samples/standalone/pact/pom.xml new file mode 100644 index 0000000000..16a1c71e7e --- /dev/null +++ b/samples/standalone/pact/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + + org.springframework.cloud + spring-cloud-contract-samples-standalone + 1.1.0.BUILD-SNAPSHOT + .. + + + spring-cloud-contract-samples-pact + pom + + Spring Cloud Contract Standalone Pact Test Samples + Spring Cloud Contract Standalone Test Samples used for end to end tests with Pact + + + 1.1.0.BUILD-SNAPSHOT + + + + pact-http-server + pact-http-client + + + + + + maven-deploy-plugin + + true + + + + + + diff --git a/samples/standalone/pom.xml b/samples/standalone/pom.xml index 2b6946ca6a..56f1211331 100644 --- a/samples/standalone/pom.xml +++ b/samples/standalone/pom.xml @@ -21,9 +21,11 @@ + contracts restdocs dsl messaging + pact diff --git a/spring-cloud-contract-dependencies/pom.xml b/spring-cloud-contract-dependencies/pom.xml index 7d6c78b7de..ed38eb15f0 100644 --- a/spring-cloud-contract-dependencies/pom.xml +++ b/spring-cloud-contract-dependencies/pom.xml @@ -40,6 +40,11 @@ spring-cloud-contract-converters ${project.version} + + org.springframework.cloud + spring-cloud-contract-spec-pact + ${project.version} + org.springframework.cloud spring-cloud-contract-stub-runner diff --git a/spring-cloud-contract-spec/pom.xml b/spring-cloud-contract-spec/pom.xml index 80bf92ed29..5a6f5b1812 100644 --- a/spring-cloud-contract-spec/pom.xml +++ b/spring-cloud-contract-spec/pom.xml @@ -17,11 +17,31 @@ org.codehaus.groovy groovy + + org.codehaus.groovy + groovy-nio + + + org.codehaus.groovy + groovy-json + + + org.codehaus.groovy + groovy-xml + dk.brics.automaton automaton 1.11-8 + + org.apache.commons + commons-lang3 + + + org.slf4j + slf4j-api + org.spockframework spock-core diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/Contract.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/Contract.groovy index bf4717d451..4c91ea62ec 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/Contract.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/Contract.groovy @@ -31,7 +31,7 @@ import org.springframework.cloud.contract.spec.internal.Response */ @TypeChecked @EqualsAndHashCode -@ToString(includeFields = true, includePackage = false, includeNames = true) +@ToString(includePackage = false, includeNames = true) class Contract { /** diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractConverter.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractConverter.groovy index e7f0345620..5ce9775d57 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractConverter.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/ContractConverter.groovy @@ -25,7 +25,7 @@ package org.springframework.cloud.contract.spec * @author Marcin Grzejszczak * @since 1.1.0 */ -public interface ContractConverter { +interface ContractConverter { /** * Should this file be accepted by the converter. Can use the file extension diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Body.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Body.groovy index cba1c3d72f..cfbde590b4 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Body.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Body.groovy @@ -25,7 +25,7 @@ import groovy.transform.ToString * * @since 1.0.0 */ -@ToString(includePackage = false, includeFields = true, includeNames = true, includeSuper = true) +@ToString(includePackage = false, includeNames = true, includeSuper = true) @EqualsAndHashCode(callSuper = true) @CompileStatic class Body extends DslProperty { diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ExecutionProperty.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ExecutionProperty.groovy index 68d1206c58..5163f27c9f 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ExecutionProperty.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/ExecutionProperty.groovy @@ -18,6 +18,7 @@ package org.springframework.cloud.contract.spec.internal import groovy.transform.CompileStatic import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString /** * Represents a property that will become an executable method in the @@ -27,6 +28,7 @@ import groovy.transform.EqualsAndHashCode */ @CompileStatic @EqualsAndHashCode +@ToString(includePackage = false, includeNames = true) class ExecutionProperty { private static final String PLACEHOLDER_VALUE = '\\$it' @@ -46,7 +48,7 @@ class ExecutionProperty { } @Override - public String toString() { + String toString() { return executionCommand } } diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/NamedProperty.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/NamedProperty.groovy index 798ddcb0f2..cbf8a5f543 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/NamedProperty.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/NamedProperty.groovy @@ -26,7 +26,7 @@ import groovy.transform.ToString * * @since 1.0.0 */ -@ToString(includePackage = false, includeFields = true, includeNames = true) +@ToString(includePackage = false, includeNames = true) @EqualsAndHashCode(includeFields = true) @CompileStatic class NamedProperty { diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OptionalProperty.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OptionalProperty.groovy index f1672a5ae0..8d5f963300 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OptionalProperty.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/OptionalProperty.groovy @@ -17,6 +17,7 @@ package org.springframework.cloud.contract.spec.internal import groovy.transform.CompileStatic +import groovy.transform.ToString /** * Represents a property that may or may not be there @@ -24,6 +25,7 @@ import groovy.transform.CompileStatic * @since 1.0.0 */ @CompileStatic +@ToString(includePackage = false, includeNames = true) class OptionalProperty { final Object value diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameter.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameter.groovy index 069d535b90..183385dd81 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameter.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameter.groovy @@ -27,8 +27,8 @@ import static org.springframework.cloud.contract.spec.util.ValidateUtils.validat * * @since 1.0.0 */ -@EqualsAndHashCode(includeFields = true, callSuper = true) -@ToString(includePackage = false, includeFields = true, ignoreNulls = true, includeNames = true, includeSuper = true) +@EqualsAndHashCode(callSuper = true) +@ToString(includePackage = false, ignoreNulls = true, includeNames = true, includeSuper = true) @CompileStatic class QueryParameter extends DslProperty { diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameters.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameters.groovy index 26cbec3730..123a9d4722 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameters.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/QueryParameters.groovy @@ -20,8 +20,8 @@ import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import groovy.transform.TypeChecked -@EqualsAndHashCode(includeFields = true) -@ToString(includePackage = false, includeFields = true, ignoreNulls = true, includeNames = true) +@EqualsAndHashCode +@ToString(includePackage = false, ignoreNulls = true, includeNames = true) @TypeChecked class QueryParameters { diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy index c9ca105382..780e79a39e 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Response.groovy @@ -30,7 +30,7 @@ import java.util.regex.Pattern * @since 1.0.0 */ @TypeChecked -@EqualsAndHashCode(includeFields = true) +@EqualsAndHashCode @ToString(includePackage = false, includeFields = true) class Response extends Common { diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Url.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Url.groovy index b7ebf2c2f5..e7782337cb 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Url.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/Url.groovy @@ -27,7 +27,7 @@ import static org.springframework.cloud.contract.spec.util.ValidateUtils.validat * * @since 1.0.0 */ -@ToString(includePackage = false, includeFields = true, includeNames = true, includeSuper = true) +@ToString(includePackage = false, includeNames = true, includeSuper = true) @EqualsAndHashCode(includeFields = true, callSuper = true) @CompileStatic class Url extends DslProperty { diff --git a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/UrlPath.groovy b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/UrlPath.groovy index 6fa2525fca..7a3ea8a3d1 100644 --- a/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/UrlPath.groovy +++ b/spring-cloud-contract-spec/src/main/groovy/org/springframework/cloud/contract/spec/internal/UrlPath.groovy @@ -21,7 +21,8 @@ import groovy.transform.EqualsAndHashCode import groovy.transform.ToString /** - * Represents a url path + * Represents a url path. Syntactic sugar when working with + * {@link QueryParameters}. It's logically equal to {@link Url} * * @since 1.0.0 */ diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRepository.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRepository.java index e0b43f0351..c3eada28fa 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRepository.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRepository.java @@ -56,6 +56,9 @@ class StubRepository { "Missing descriptor repository under path [" + repository + "]"); } this.contractConverters = SpringFactoriesLoader.loadFactories(ContractConverter.class, null); + if (log.isDebugEnabled()) { + log.debug("Found the following contract converters " + this.contractConverters); + } this.httpServerStubs = httpServerStubs; this.path = repository; this.stubs = stubs(); diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java index eee93df601..3c5b996acb 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/camel/StubRunnerCamelPredicate.java @@ -25,10 +25,10 @@ import org.springframework.cloud.contract.spec.Contract; import org.springframework.cloud.contract.spec.internal.BodyMatcher; import org.springframework.cloud.contract.spec.internal.BodyMatchers; import org.springframework.cloud.contract.spec.internal.Header; +import org.springframework.cloud.contract.verifier.util.MapConverter; import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper; import org.springframework.cloud.contract.verifier.util.JsonPaths; import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter; -import org.springframework.cloud.contract.verifier.util.MapConverter; import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable; import com.fasterxml.jackson.core.JsonProcessingException; diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java index 073cb2cfae..e056748ae4 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/integration/StubRunnerIntegrationMessageSelector.java @@ -23,10 +23,10 @@ import org.springframework.cloud.contract.spec.Contract; import org.springframework.cloud.contract.spec.internal.BodyMatcher; import org.springframework.cloud.contract.spec.internal.BodyMatchers; import org.springframework.cloud.contract.spec.internal.Header; +import org.springframework.cloud.contract.verifier.util.MapConverter; import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper; import org.springframework.cloud.contract.verifier.util.JsonPaths; import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter; -import org.springframework.cloud.contract.verifier.util.MapConverter; import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable; import org.springframework.integration.core.MessageSelector; import org.springframework.messaging.Message; diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java index d75a2c4ae3..6fa0d5dd77 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/stream/StubRunnerStreamMessageSelector.java @@ -23,10 +23,10 @@ import org.springframework.cloud.contract.spec.Contract; import org.springframework.cloud.contract.spec.internal.BodyMatcher; import org.springframework.cloud.contract.spec.internal.BodyMatchers; import org.springframework.cloud.contract.spec.internal.Header; +import org.springframework.cloud.contract.verifier.util.MapConverter; import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper; import org.springframework.cloud.contract.verifier.util.JsonPaths; import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter; -import org.springframework.cloud.contract.verifier.util.MapConverter; import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable; import org.springframework.integration.core.MessageSelector; import org.springframework.messaging.Message; diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java index 60fe2e8d21..727de1c5d7 100644 --- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java +++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/provider/wiremock/WireMockHttpServerStub.java @@ -119,7 +119,9 @@ public class WireMockHttpServerStub implements HttpServerStub { } } catch (Exception e) { - log.warn("Failed to register the stub mapping [" + mappingDescriptor + "]", e); + if (log.isDebugEnabled()) { + log.debug("Failed to register the stub mapping [" + mappingDescriptor + "]", e); + } } } } diff --git a/spring-cloud-contract-tools/pom.xml b/spring-cloud-contract-tools/pom.xml index fe4634f212..dfe7b50728 100644 --- a/spring-cloud-contract-tools/pom.xml +++ b/spring-cloud-contract-tools/pom.xml @@ -19,6 +19,7 @@ spring-cloud-contract-converters + spring-cloud-contract-spec-pact spring-cloud-contract-maven-plugin spring-cloud-contract-gradle-plugin diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.groovy index 38c6bc6a19..677c9cd369 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/RecursiveFilesConverter.groovy @@ -24,6 +24,7 @@ import org.springframework.cloud.contract.verifier.config.ContractVerifierConfig import org.springframework.cloud.contract.verifier.file.ContractFileScanner import org.springframework.cloud.contract.verifier.file.ContractMetadata import org.springframework.cloud.contract.verifier.util.NamesUtil +import org.springframework.cloud.contract.verifier.wiremock.DslToWireMockClientConverter import java.nio.charset.StandardCharsets import java.nio.file.Files @@ -64,8 +65,12 @@ class RecursiveFilesConverter { } contracts.asMap().entrySet().each { entry -> entry.value.each { ContractMetadata contract -> + if (log.isDebugEnabled()) { + log.debug("Will create a stub for contract [${contract}]") + } File sourceFile = contract.path.toFile() - StubGenerator stubGenerator = holder.converterForName(sourceFile.name); + StubGenerator stubGenerator = contract.convertedContract ? holder.firstOrDefault(new DslToWireMockClientConverter()) : + holder.converterForName(sourceFile.name) try { String path = sourceFile.path if (properties.isExcludeBuildFolders() && (matchesPath(path, "target") || matchesPath(path, "build"))) { @@ -78,7 +83,11 @@ class RecursiveFilesConverter { return } int contractsSize = contract.convertedContract.size() - Map convertedContent = stubGenerator.convertContents(entry.key.last().toString(), contract) + def entryKey = entry.key + if (log.isDebugEnabled()) { + log.debug("Stub Generator [${stubGenerator}] will convert contents of [${entryKey}]") + } + Map convertedContent = stubGenerator.convertContents(entryKey.last().toString(), contract) if (!convertedContent) { return } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/StubGeneratorProvider.groovy b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/StubGeneratorProvider.groovy index 9e187741ce..5afade974b 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/StubGeneratorProvider.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-converters/src/main/groovy/org/springframework/cloud/contract/verifier/converter/StubGeneratorProvider.groovy @@ -25,4 +25,8 @@ class StubGeneratorProvider { StubGenerator converterForName(String fileName) { return this.converters.find { it.canHandleFileName(fileName) } } + + StubGenerator firstOrDefault(StubGenerator defaultStubGenerator) { + return this.converters.empty ? defaultStubGenerator : this.converters.first() + } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/pom.xml b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/pom.xml index cedc473db0..b6526ac27b 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/pom.xml +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/pom.xml @@ -369,6 +369,16 @@ assertj-core test + + org.springframework.cloud + spring-cloud-contract-spec-pact + test + + + au.com.dius + pact-jvm-model + test + diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/java/org/springframework/cloud/contract/maven/verifier/PluginUnitTest.java b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/java/org/springframework/cloud/contract/maven/verifier/PluginUnitTest.java index 01faf6555a..d9701852e0 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/java/org/springframework/cloud/contract/maven/verifier/PluginUnitTest.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/java/org/springframework/cloud/contract/maven/verifier/PluginUnitTest.java @@ -16,20 +16,21 @@ */ package org.springframework.cloud.contract.maven.verifier; -import static io.takari.maven.testing.TestMavenRuntime.newParameter; -import static io.takari.maven.testing.TestResources.assertFilesNotPresent; -import static io.takari.maven.testing.TestResources.assertFilesPresent; -import static org.assertj.core.api.BDDAssertions.then; - import java.io.File; import org.apache.commons.io.FileUtils; import org.junit.Rule; import org.junit.Test; +import org.springframework.util.StringUtils; import io.takari.maven.testing.TestMavenRuntime; import io.takari.maven.testing.TestResources; +import static io.takari.maven.testing.TestMavenRuntime.newParameter; +import static io.takari.maven.testing.TestResources.assertFilesNotPresent; +import static io.takari.maven.testing.TestResources.assertFilesPresent; +import static org.assertj.core.api.BDDAssertions.then; + public class PluginUnitTest { @Rule @@ -256,4 +257,19 @@ public class PluginUnitTest { assertFilesPresent(basedir, "target/stubs/contracts/consumer1/Messaging.groovy"); assertFilesPresent(basedir, "target/stubs/contracts/pom.xml"); } + + @Test + public void shouldGenerateContractTestsForPactAndMaintainIndents() throws Exception { + File basedir = this.resources.getBasedir("pact"); + + this.maven.executeMojo(basedir, "generateTests"); + + assertFilesPresent(basedir, + "target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/ContractVerifierTest.java"); + File test = new File(basedir, "target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/ContractVerifierTest.java"); + String testContents = FileUtils.readFileToString(test); + int countOccurrencesOf = StringUtils + .countOccurrencesOf(testContents, "\t\tMockMvcRequestSpecification"); + then(countOccurrencesOf).isEqualTo(4); + } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/pom.xml b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/pom.xml new file mode 100644 index 0000000000..297e4e005e --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/pom.xml @@ -0,0 +1,62 @@ + + + + 4.0.0 + + org.springframework.cloud.verifier.sample + sample-pact-project + 0.1 + + + 1.1.0.BUILD-SNAPSHOT + + + + + + org.springframework.cloud + spring-cloud-contract-maven-plugin + + com.example.FooBase + + + .*com.* + com.example.TestBase + + + + + + org.springframework.cloud + spring-cloud-contract-spec-pact + ${spring.cloud.contract.version} + + + au.com.dius + pact-jvm-model + 2.4.18 + + + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/src/test/resources/contracts/shouldMarkClientAsFraud.json b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/src/test/resources/contracts/shouldMarkClientAsFraud.json new file mode 100644 index 0000000000..cd81cc5fc5 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/src/test/resources/contracts/shouldMarkClientAsFraud.json @@ -0,0 +1,42 @@ +{ + "provider": { + "name": "Provider" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "description": "", + "request": { + "method": "PUT", + "path": "/fraudcheck", + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": { + "clientId": "1234567890", + "loanAmount": 99999 + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8" + }, + "body": { + "fraudCheckStatus": "FRAUD", + "rejectionReason": "Amount too high" + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "2.0.0" + }, + "pact-jvm": { + "version": "2.4.18" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/src/test/resources/contracts/shouldMarkClientAsNotFraud.json b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/src/test/resources/contracts/shouldMarkClientAsNotFraud.json new file mode 100644 index 0000000000..0b947bc8c0 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/src/test/resources/contracts/shouldMarkClientAsNotFraud.json @@ -0,0 +1,42 @@ +{ + "provider": { + "name": "Provider" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "description": "", + "request": { + "method": "PUT", + "path": "/fraudcheck", + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": { + "clientId": "1234567890", + "loanAmount": 123.123 + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8" + }, + "body": { + "fraudCheckStatus": "OK", + "rejectionReason": null + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "2.0.0" + }, + "pact-jvm": { + "version": "2.4.18" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/src/test/resources/contracts/shouldReturnDrunksStats.json b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/src/test/resources/contracts/shouldReturnDrunksStats.json new file mode 100644 index 0000000000..f939e710c9 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/src/test/resources/contracts/shouldReturnDrunksStats.json @@ -0,0 +1,34 @@ +{ + "provider": { + "name": "Provider" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "description": "", + "request": { + "method": "GET", + "path": "/drunks" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8" + }, + "body": { + "count": 100 + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "2.0.0" + }, + "pact-jvm": { + "version": "2.4.18" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/src/test/resources/contracts/shouldReturnFraudStats.json b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/src/test/resources/contracts/shouldReturnFraudStats.json new file mode 100644 index 0000000000..160f1e618f --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/pact/src/test/resources/contracts/shouldReturnFraudStats.json @@ -0,0 +1,34 @@ +{ + "provider": { + "name": "Provider" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "description": "", + "request": { + "method": "GET", + "path": "/frauds" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json;charset=UTF-8" + }, + "body": { + "count": 200 + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "2.0.0" + }, + "pact-jvm": { + "version": "2.4.18" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/pom.xml b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/pom.xml new file mode 100644 index 0000000000..bc55e13dab --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-contract-tools + 1.1.0.BUILD-SNAPSHOT + .. + + spring-cloud-contract-spec-pact + jar + Spring Cloud Contract Spec Pact + Spring Cloud Contract Spec Pact + + + org.springframework + spring-context + + + org.springframework.cloud + spring-cloud-contract-verifier + + + org.springframework.boot + spring-boot-starter-logging + + + org.codehaus.groovy + groovy + + + org.codehaus.groovy + groovy-nio + + + au.com.dius + pact-jvm-model + true + + + org.spockframework + spock-core + test + + + org.springframework.boot + spring-boot-starter-test + test + + + info.solidsoft.spock + spock-global-unroll + test + + + + + + org.codehaus.gmavenplus + gmavenplus-plugin + + + + addSources + compile + testCompile + + + + + + + diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy new file mode 100644 index 0000000000..4f1b7df1cc --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverter.groovy @@ -0,0 +1,318 @@ +package org.springframework.cloud.contract.verifier.spec.pact + +import au.com.dius.pact.model.BasePact +import au.com.dius.pact.model.Consumer +import au.com.dius.pact.model.Interaction +import au.com.dius.pact.model.OptionalBody +import au.com.dius.pact.model.Pact +import au.com.dius.pact.model.PactReader +import au.com.dius.pact.model.Provider +import au.com.dius.pact.model.Request +import au.com.dius.pact.model.RequestResponseInteraction +import au.com.dius.pact.model.RequestResponsePact +import au.com.dius.pact.model.Response +import groovy.json.JsonOutput +import groovy.transform.CompileStatic +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.spec.ContractConverter +import org.springframework.cloud.contract.spec.internal.BodyMatchers +import org.springframework.cloud.contract.spec.internal.DslProperty +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.spec.internal.Headers +import org.springframework.cloud.contract.spec.internal.MatchingType +import org.springframework.cloud.contract.spec.internal.QueryParameters +import org.springframework.cloud.contract.verifier.util.MapConverter + +/** + * Converter of JSON PACT file + * + * @author Marcin Grzejszczak + * @since 1.1.0 + */ +@CompileStatic +class PactContractConverter implements ContractConverter { + + private static final String MATCH_KEY = "match" + private static final String REGEX_KEY = "regex" + private static final String MAX_KEY = "max" + private static final String MIN_KEY = "min" + + @Override + boolean isAccepted(File file) { + try { + PactReader.loadPact(file) + return true + } catch (Exception e) { + return false + } + } + + @Override + Collection convertFrom(File file) { + Pact pact = PactReader.loadPact(file) + List interactions = pact.interactions + return interactions.collect { Interaction interaction -> + Contract.make { + if (interaction instanceof RequestResponseInteraction) { + RequestResponseInteraction requestResponseInteraction = (RequestResponseInteraction) interaction + description("$requestResponseInteraction.description${providerState(interaction)}") + request { + method(requestResponseInteraction.request.method) + if (requestResponseInteraction.request.query) { + url(requestResponseInteraction.request.path) { + queryParameters { + requestResponseInteraction.request.query.each { String key, List value -> + value.each { String singleValue -> + parameter(key, singleValue) + } + } + } + } + } else { + url(requestResponseInteraction.request.path) + } + if (requestResponseInteraction.request.headers) { + headers { + requestResponseInteraction.request.headers.each { String key, String value -> + header(key, value) + } + } + } + if (requestResponseInteraction.request.body.state == OptionalBody.State.PRESENT) { + def parsedBody = BasePact.parseBody(requestResponseInteraction.request) + if (parsedBody instanceof Map) { + body(parsedBody as Map) + } else if (parsedBody instanceof List) { + body(parsedBody as List) + } else { + body(parsedBody.toString()) + } + } + if (requestResponseInteraction.request?.matchingRules) { + stubMatchers { + requestResponseInteraction.request.matchingRules.each { String key, Map value -> + String keyFromBody = toKeyStartingFromBody(key) + if (value.containsKey(MATCH_KEY)) { + MatchingType matchingType = MatchingType.valueOf((value.get(MATCH_KEY) as String).toUpperCase()) + switch (matchingType) { + case MatchingType.EQUALITY: + // equality is checked by default in the standard way + break + case MatchingType.DATE: + jsonPath(keyFromBody, byDate()) + break + case MatchingType.TIME: + jsonPath(keyFromBody, byTime()) + break + case MatchingType.TIMESTAMP: + jsonPath(keyFromBody, byTimestamp()) + break + case MatchingType.REGEX: + jsonPath(keyFromBody, byRegex(value.get(REGEX_KEY) as String)) + break + } + } else if (value.containsKey(REGEX_KEY)) { + jsonPath(keyFromBody, byRegex(value.get(REGEX_KEY) as String)) + } + } + } + } + } + response { + status(requestResponseInteraction.response.status) + if (requestResponseInteraction.response.body.state == OptionalBody.State.PRESENT) { + def parsedBody = BasePact.parseBody(requestResponseInteraction.response) + if (parsedBody instanceof Map) { + body(parsedBody as Map) + } else if (parsedBody instanceof List) { + body(parsedBody as List) + } else { + body(parsedBody.toString()) + } + } + if (requestResponseInteraction.response?.matchingRules) { + testMatchers { + requestResponseInteraction.response.matchingRules.each { String key, Map value -> + String keyFromBody = toKeyStartingFromBody(key) + if (value.containsKey(MATCH_KEY)) { + MatchingType matchingType = MatchingType.valueOf((value.get(MATCH_KEY) as String).toUpperCase()) + switch (matchingType) { + case MatchingType.EQUALITY: + // equality is checked by default in the standard way + break + case MatchingType.DATE: + jsonPath(keyFromBody, byDate()) + break + case MatchingType.TIME: + jsonPath(keyFromBody, byTime()) + break + case MatchingType.TIMESTAMP: + jsonPath(keyFromBody, byTimestamp()) + break + case MatchingType.REGEX: + jsonPath(keyFromBody, byRegex(value.get(REGEX_KEY) as String)) + break + case MatchingType.TYPE: + jsonPath(keyFromBody, byType() { + if (value.containsKey(MIN_KEY)) { + minOccurrence(value.get(MIN_KEY) as Integer) + } + if (value.containsKey(MAX_KEY)) { + maxOccurrence(value.get(MAX_KEY) as Integer) + } + }) + break + } + } else if (value.containsKey(REGEX_KEY)) { + jsonPath(keyFromBody, byRegex(value.get(REGEX_KEY) as String)) + } + } + } + } + requestResponseInteraction.response.headers?.each { String key, String value -> + headers { + header(key, value) + } + } + } + } + } + } + } + + protected String providerState(Interaction interaction) { + return interaction.providerState ? " ${interaction.providerState}" : "" + } + + protected String toKeyStartingFromBody(String key) { + return key.replace('$.body', '$') + } + + @Override + Pact convertTo(Collection contract) { + Provider provider = new Provider() + provider.name = "Provider" + Consumer consumer = new Consumer() + consumer.name = "Consumer" + List interactions = contract.find { it.request }.collect { Contract dsl -> + RequestResponseInteraction interaction = new RequestResponseInteraction() + interaction.description = dsl.description ?: "" + Request request = new Request().with { + method = dsl.request.method.serverValue.toString() + path = url(dsl) + QueryParameters params = queryParams(dsl) + if (params) { + query = params.parameters.collectEntries { + String name = it.name + String value = it.serverValue + return [(name) : [value]] + } + } + if (dsl.request.headers) { + headers = headers(dsl.request.headers, { DslProperty property -> property.serverValue }) + } + if (dsl.request.body) { + assertInputContract(dsl.request.body.serverValue) + def json = MapConverter.getTestSideValues(dsl.request.body.serverValue) + String jsonBody = JsonOutput.toJson(json) + body = new OptionalBody(OptionalBody.State.PRESENT, jsonBody) + } + if (dsl.request.matchers && dsl.request.matchers.hasMatchers()) { + matchingRules = matchingRules(dsl.request.matchers) + } + return it + } + Response response = new Response().with { + status = dsl.response.status.clientValue as Integer + if (dsl.response.headers) { + headers = headers(dsl.response.headers, { DslProperty property -> property.clientValue }) + } + if (dsl.response.body) { + assertInputContract(dsl.response.body.clientValue) + def json = MapConverter.getStubSideValues(dsl.response.body.clientValue) + String jsonBody = JsonOutput.toJson(json) + body = new OptionalBody(OptionalBody.State.PRESENT, jsonBody) + } + if (dsl.response.matchers && dsl.response.matchers.hasMatchers()) { + matchingRules = matchingRules(dsl.response.matchers) + } + return it + } + interaction.request = request + interaction.response = response + return interaction + } + return new RequestResponsePact(provider, consumer, interactions) + } + + protected void assertInputContract(parsedJson) { + boolean hasExecutionProp = false + MapConverter.transformValues(parsedJson, { + if (it instanceof ExecutionProperty) { + hasExecutionProp = true + } + return it + }) + if (hasExecutionProp) { + throw new UnsupportedOperationException("We can't convert a contract that has execution property") + } + } + + protected Map headers(Headers headers, Closure closure) { + return headers.entries.collectEntries { + String name = it.name + String value = closure(it) + return [(name) : value] + } + } + + protected Map> matchingRules(BodyMatchers bodyMatchers) { + return bodyMatchers.jsonPathMatchers().collectEntries { + MatchingType matchingType = it.matchingType() + String key = it.path() + Object value = it.value() + Integer minTypeOccurrence = it.minTypeOccurrence() + Integer maxTypeOccurrence = it.maxTypeOccurrence() + Map matchingRule = [:] + switch (matchingType) { + case MatchingType.EQUALITY: + matchingRule << [(MATCH_KEY) : MatchingType.EQUALITY.toString().toLowerCase() as Object] + break + case MatchingType.TYPE: + Map map = [(MATCH_KEY) : MatchingType.TYPE.toString().toLowerCase() as Object] + if (minTypeOccurrence) map.put(MIN_KEY, minTypeOccurrence) + if (maxTypeOccurrence) map.put(MAX_KEY, maxTypeOccurrence) + matchingRule << map + break + case MatchingType.DATE: + case MatchingType.TIME: + case MatchingType.TIMESTAMP: + case MatchingType.REGEX: + matchingRule << [ + (MATCH_KEY) : MatchingType.REGEX.toString().toLowerCase() as Object, + (REGEX_KEY) : value + ] + break + } + return [(key) : matchingRule] + } + } + + protected String url(Contract dsl) { + if (dsl.request.urlPath) { + return dsl.request.urlPath.serverValue.toString() + } else if (dsl.request.url) { + return dsl.request.url.serverValue.toString() + } + throw new IllegalStateException("No url provided") + } + + protected QueryParameters queryParams(Contract dsl) { + if (dsl.request.urlPath) { + return dsl.request.urlPath.queryParameters + } else if (dsl.request.url) { + return dsl.request.url.queryParameters + } + throw new IllegalStateException("No url provided") + } +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..640c7019cf --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.cloud.contract.spec.ContractConverter=\ +org.springframework.cloud.contract.verifier.spec.pact.PactContractConverter diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy new file mode 100644 index 0000000000..bc3f6375df --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/groovy/org/springframework/cloud/contract/verifier/spec/pact/PactContractConverterSpec.groovy @@ -0,0 +1,290 @@ +package org.springframework.cloud.contract.verifier.spec.pact + +import au.com.dius.pact.model.Pact +import au.com.dius.pact.model.PactSpecVersion +import groovy.json.JsonOutput +import org.skyscreamer.jsonassert.JSONAssert +import org.springframework.cloud.contract.spec.Contract +import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter +import org.springframework.core.io.Resource +import org.springframework.core.io.support.PathMatchingResourcePatternResolver +import spock.lang.Specification +import spock.lang.Subject +/** + * @author Marcin Grzejszczak + */ +class PactContractConverterSpec extends Specification { + + File pactJson = new File(PactContractConverterSpec.getResource("/pact/pact.json").toURI()) + @Subject PactContractConverter converter = new PactContractConverter() + + def "should accept json files that are pact files"() { + expect: + converter.isAccepted(pactJson) + } + + def "should reject json files that are pact files"() { + given: + File invalidPact = new File(PactContractConverterSpec.getResource("/pact/invalid_pact.json").toURI()) + expect: + converter.isAccepted(invalidPact) + } + + def "should convert from pact to contract"() { + given: + Contract expectedContract = Contract.make { + description("a retrieve Mallory request a user with username 'username' and password 'password' exists") + request { + method(GET()) + url("/mallory") { + queryParameters { + parameter("name", "ron") + parameter("status", "good") + } + } + headers { + contentType(applicationJson()) + } + body(id: "123", method: "create") + stubMatchers { + jsonPath('$.id', byRegex("[0-9]{3}")) + } + } + response { + status(200) + headers { + contentType(applicationJson()) + } + body([[ + [email: "rddtGwwWMEhnkAPEmsyE", + id: "eb0f8c17-c06a-479e-9204-14f7c95b63a6", + userName: "AJQrokEGPAVdOHprQpKP"] + ]]) + testMatchers { + jsonPath('$[0][*].email', byType()) + jsonPath('$[0][*].id', byRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")) + jsonPath('$[0]', byType() { + maxOccurrence(5) + }) + jsonPath('$[0][*].userName', byType()) + } + } + } + when: + Collection contracts = converter.convertFrom(pactJson) + then: + contracts == [expectedContract] + } + + def "should convert from contract to pact"() { + given: + Collection inputContracts = [ + Contract.make { + description("a retrieve Mallory request") + request { + method(GET()) + url("/mallory") { + queryParameters { + parameter("name", "ron") + parameter("status", "good") + } + } + headers { + contentType(applicationJson()) + } + body( + id: 123, + method: $(stub(regex("[0][1][2]"))), + something: "foo" + ) + stubMatchers { + jsonPath('$.id', byRegex("[0-9]{3}")) + jsonPath('$.something', byEquality()) + } + } + response { + status(200) + headers { + contentType(applicationJson()) + } + body([[ + [email: "rddtGwwWMEhnkAPEmsyE", + id: "eb0f8c17-c06a-479e-9204-14f7c95b63a6", + number: $(producer(regex("[0-9]{3}")), consumer(923)), + something: "foo", + userName: "AJQrokEGPAVdOHprQpKP"] + ]]) + testMatchers { + jsonPath('$[0][*].email', byType()) + jsonPath('$[0][*].id', byRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")) + jsonPath('$[0]', byType() { + minOccurrence(1) + maxOccurrence(5) + }) + jsonPath('$[0][*].userName', byType()) + jsonPath('$[0][*].something', byEquality()) + } + } + } + ] + String expectedJson = ''' +{ + "provider": { + "name": "Provider" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "description": "a retrieve Mallory request", + "request": { + "method": "GET", + "path": "\\/mallory", + "query": "name=ron&status=good", + "headers": { + "Content-Type": "application\\/json" + }, + "body": { + "id": 123, + "method": "012", + "something": "foo" + }, + "matchingRules": { + "$.id": { + "match": "regex", + "regex": "[0-9]{3}" + }, + "$.something": { + "match": "equality" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application\\/json" + }, + "body": [ + [ + { + "email": "rddtGwwWMEhnkAPEmsyE", + "id": "eb0f8c17-c06a-479e-9204-14f7c95b63a6", + "number": 923, + "userName": "AJQrokEGPAVdOHprQpKP" + } + ] + ], + "matchingRules": { + "$[0][*].email": { + "match": "type" + }, + "$[0][*].id": { + "match": "regex", + "regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" + }, + "$[0]": { + "match": "type", + "min": 1, + "max": 5 + }, + "$[0][*].userName": { + "match": "type" + }, + "$[0][*].something": { + "match": "equality" + } + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "2.0.0" + }, + "pact-jvm": { + "version": "2.4.18" + } + } +} +''' + when: + Pact pact = converter.convertTo(inputContracts) + then: + String actual = JsonOutput.toJson(pact.toMap(PactSpecVersion.V2)) + JSONAssert.assertEquals(expectedJson, actual, false) + } + + def "should fail to convert from contract to pact when contract has execution property in request"() { + given: + Collection inputContracts = [ + Contract.make { + request { + method(GET()) + url("/mallory") + body( + id: $(c("foo"), p(execute("foo"))) + ) + } + response { + status(200) + + } + } + ] + when: + converter.convertTo(inputContracts) + then: + def e = thrown(UnsupportedOperationException) + e.message.contains("execution property") + } + + def "should fail to convert from contract to pact when contract has execution property in response"() { + given: + Collection inputContracts = [ + Contract.make { + request { + method(GET()) + url("/mallory") + } + response { + status(200) + body( + id: $(c(execute("foo")), p("foo")) + ) + } + } + ] + when: + converter.convertTo(inputContracts) + then: + def e = thrown(UnsupportedOperationException) + e.message.contains("execution property") + } + + def "should convert contracts from samples to pacts"() { + given: + Resource[] contractResources = new PathMatchingResourcePatternResolver().getResources("contracts/*.groovy") + Resource[] pactResources = new PathMatchingResourcePatternResolver().getResources("contracts/*.json") + Map> contracts = contractResources.collectEntries { [(it.filename) : ContractVerifierDslConverter.convertAsCollection(it.file)] } + Map jsonPacts = pactResources.collectEntries { [(it.filename) : it.file.text] } + when: + Map pacts = contracts.entrySet().collectEntries { [(it.key) : converter.convertTo(it.value)] } + then: + pacts.entrySet().each { + String convertedPactAsText = JsonOutput.toJson(it.value.toMap(PactSpecVersion.V2)) + String pactFileName = it.key.replace("groovy", "json") + println "File name [${it.key}]" + JSONAssert.assertEquals(jsonPacts.get(pactFileName), convertedPactAsText, false) + } + } +} + + + +// file creator +/* +pacts.entrySet().each { + new File("target/${it.key.replace("groovy", "json")}").text = JsonOutput.toJson(it.value.toMap(PactSpecVersion.V2)) +} + */ diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsFraud.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsFraud.groovy new file mode 100644 index 0000000000..bb6d491ce4 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsFraud.groovy @@ -0,0 +1,66 @@ +package contracts + +org.springframework.cloud.contract.spec.Contract.make { + request { // (1) + method 'PUT' // (2) + url '/fraudcheck' // (3) + body([ // (4) + clientId: $(c(regex('[0-9]{10}')), p("8532032713")), + loanAmount: 99999 + ]) + headers { // (5) + contentType('application/vnd.fraud.v1+json') + } + } + response { // (6) + status 200 // (7) + body([ // (8) + fraudCheckStatus: "FRAUD", + rejectionReason: "Amount too high" + ]) + headers { // (9) + contentType('application/vnd.fraud.v1+json') + } + } +} + +/* +Since we don't want to force on the user to hardcode values of fields that are dynamic +(timestamps, database ids etc.), one can parametrize those entries. If you wrap your field's + value in a `$(...)` or `value(...)` and provide a dynamic value of a field then + the concrete value will be generated for you. If you want to be really explicit about + which side gets which value you can do that by using the `value(consumer(...), producer(...))` notation. + That way what's present in the `consumer` section will end up in the produced stub. What's + there in the `producer` will end up in the autogenerated test. If you provide only the + regular expression side without the concrete value then Spring Cloud Contract will generate one for you. + +From the Consumer perspective, when shooting a request in the integration test: + +(1) - If the consumer sends a request +(2) - With the "PUT" method +(3) - to the URL "/fraudcheck" +(4) - with the JSON body that + * has a field `clientId` that matches a regular expression `[0-9]{10}` + * has a field `loanAmount` that is equal to `99999` +(5) - with header `Content-Type` equal to `application/vnd.fraud.v1+json` +(6) - then the response will be sent with +(7) - status equal `200` +(8) - and JSON body equal to + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +(9) - with header `Content-Type` equal to `application/vnd.fraud.v1+json` + +From the Producer perspective, in the autogenerated producer-side test: + +(1) - A request will be sent to the producer +(2) - With the "PUT" method +(3) - to the URL "/fraudcheck" +(4) - with the JSON body that + * has a field `clientId` that will have a generated value that matches a regular expression `[0-9]{10}` + * has a field `loanAmount` that is equal to `99999` +(5) - with header `Content-Type` equal to `application/vnd.fraud.v1+json` +(6) - then the test will assert if the response has been sent with +(7) - status equal `200` +(8) - and JSON body equal to + { "fraudCheckStatus": "FRAUD", "rejectionReason": "Amount too high" } +(9) - with header `Content-Type` matching `application/vnd.fraud.v1+json.*` + */ \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsFraud.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsFraud.json new file mode 100644 index 0000000000..7de1aeddf0 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsFraud.json @@ -0,0 +1,42 @@ +{ + "provider": { + "name": "Provider" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "description": "", + "request": { + "method": "PUT", + "path": "/fraudcheck", + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": { + "clientId": "8532032713", + "loanAmount": 99999 + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": { + "fraudCheckStatus": "FRAUD", + "rejectionReason": "Amount too high" + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "2.0.0" + }, + "pact-jvm": { + "version": "2.4.18" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsNotFraud.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsNotFraud.groovy new file mode 100644 index 0000000000..c3dce3f32d --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsNotFraud.groovy @@ -0,0 +1,30 @@ +package contracts + +org.springframework.cloud.contract.spec.Contract.make { + request { + method 'PUT' + url '/fraudcheck' + body(""" + { + "clientId":"${value(consumer(regex('[0-9]{10}')), producer('1234567890'))}", + "loanAmount":123.123 + } + """ + ) + headers { + contentType("application/vnd.fraud.v1+json") + } + + } + response { + status 200 + body( + fraudCheckStatus: "OK", + rejectionReason: $(consumer(null), producer(execute('assertThatRejectionReasonIsNull($it)'))) + ) + headers { + contentType("application/vnd.fraud.v1+json") + } + } + +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsNotFraud.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsNotFraud.json new file mode 100644 index 0000000000..db5f37e92e --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldMarkClientAsNotFraud.json @@ -0,0 +1,42 @@ +{ + "provider": { + "name": "Provider" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "description": "", + "request": { + "method": "PUT", + "path": "/fraudcheck", + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": { + "clientId": "1234567890", + "loanAmount": 123.123 + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": { + "fraudCheckStatus": "OK", + "rejectionReason": null + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "2.0.0" + }, + "pact-jvm": { + "version": "2.4.18" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldReturnFraudStats.groovy b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldReturnFraudStats.groovy new file mode 100644 index 0000000000..a6ed5a97d7 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldReturnFraudStats.groovy @@ -0,0 +1,37 @@ +package contracts + +import org.springframework.cloud.contract.spec.Contract + +[ + Contract.make { + request { + name "should count all frauds" + method GET() + url '/frauds' + } + response { + status 200 + body([ + count: 200 + ]) + headers { + contentType("application/vnd.fraud.v1+json") + } + } + }, + Contract.make { + request { + method GET() + url '/drunks' + } + response { + status 200 + body([ + count: 100 + ]) + headers { + contentType("application/vnd.fraud.v1+json") + } + } + } +] \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldReturnFraudStats.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldReturnFraudStats.json new file mode 100644 index 0000000000..32a0a0b806 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/contracts/shouldReturnFraudStats.json @@ -0,0 +1,34 @@ +{ + "provider": { + "name": "Provider" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "description": "", + "request": { + "method": "GET", + "path": "/frauds" + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/vnd.fraud.v1+json" + }, + "body": { + "count": 200 + } + } + } + ], + "metadata": { + "pact-specification": { + "version": "2.0.0" + }, + "pact-jvm": { + "version": "2.4.18" + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/invalid_pact.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/invalid_pact.json new file mode 100644 index 0000000000..1b333cad4d --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/invalid_pact.json @@ -0,0 +1,3 @@ +{ + "some" : "json" +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact.json b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact.json new file mode 100644 index 0000000000..c7fc03126b --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-spec-pact/src/test/resources/pact/pact.json @@ -0,0 +1,59 @@ +{ + "provider": { + "name": "Alice Service" + }, + "consumer": { + "name": "Consumer" + }, + "interactions": [ + { + "description": "a retrieve Mallory request", + "provider_state": "a user with username 'username' and password 'password' exists", + "request": { + "method": "GET", + "path": "/mallory", + "query": "name=ron&status=good", + "body" : {"id": "123", "method": "create"}, + "headers": { + "Content-Type": "application/json" + }, + "matchingRules": { + "$.body.id": { + "match": "regex", + "regex": "[0-9]{3}" + } + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": [ + [ + { + "email": "rddtGwwWMEhnkAPEmsyE", + "id": "eb0f8c17-c06a-479e-9204-14f7c95b63a6", + "userName": "AJQrokEGPAVdOHprQpKP" + } + ] + ], + "matchingRules": { + "$.body[0][*].email": { + "match": "type" + }, + "$.body[0][*].id": { + "regex": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}" + }, + "$.body[0]": { + "max": 5, + "match": "type" + }, + "$.body[0][*].userName": { + "match": "type" + } + } + } + } + ] +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy index eddc53d8d6..bc4167ec64 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy @@ -23,11 +23,11 @@ import groovy.transform.PackageScope import groovy.transform.TypeChecked import org.apache.commons.lang3.StringEscapeUtils import org.springframework.cloud.contract.spec.internal.* +import org.springframework.cloud.contract.verifier.util.MapConverter; import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties import org.springframework.cloud.contract.verifier.util.ContentType import org.springframework.cloud.contract.verifier.util.JsonPaths import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter -import org.springframework.cloud.contract.verifier.util.MapConverter import java.util.regex.Pattern @@ -187,7 +187,7 @@ abstract class MethodBodyBuilder { protected abstract void then(BlockBuilder bb) /** - * Returns a {@link org.springframework.cloud.contract.verifier.util.ContentType} for the given request + * Returns a {@link ContentType} for the given request */ protected abstract ContentType getResponseContentType() @@ -415,7 +415,7 @@ abstract class MethodBodyBuilder { } private String stripFirstChar(String s) { - return s.substring(1); + return s.substring(1) } /** diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy index 1ade040def..5ab743a258 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/RequestProcessingMethodBodyBuilder.groovy @@ -28,9 +28,9 @@ import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.QueryParameter import org.springframework.cloud.contract.spec.internal.Response import org.springframework.cloud.contract.spec.internal.Url +import org.springframework.cloud.contract.verifier.util.MapConverter import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties import org.springframework.cloud.contract.verifier.util.ContentType -import org.springframework.cloud.contract.verifier.util.MapConverter import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromContent import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromHeader diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy index ca8fad8475..bbbf0748fc 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/BaseWireMockStubStrategy.groovy @@ -20,8 +20,8 @@ import groovy.json.JsonBuilder import groovy.transform.PackageScope import groovy.transform.TypeChecked import org.springframework.cloud.contract.spec.internal.Headers -import org.springframework.cloud.contract.verifier.util.MapConverter import org.springframework.cloud.contract.spec.internal.DslProperty +import org.springframework.cloud.contract.verifier.util.MapConverter import org.springframework.cloud.contract.verifier.util.ContentType import org.springframework.cloud.contract.verifier.util.ContentUtils @@ -51,7 +51,7 @@ abstract class BaseWireMockStubStrategy { } /** - * For the given {@link org.springframework.cloud.contract.verifier.util.ContentType} returns the String version of the body + * For the given {@link ContentType} returns the String version of the body */ String parseBody(Object value, ContentType contentType) { return parseBody(value.toString(), contentType) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy index a2f7e051ad..8ea7dfe983 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockRequestStubStrategy.groovy @@ -18,14 +18,28 @@ package org.springframework.cloud.contract.verifier.dsl.wiremock import com.github.tomakehurst.wiremock.client.WireMock import com.github.tomakehurst.wiremock.http.RequestMethod -import com.github.tomakehurst.wiremock.matching.* +import com.github.tomakehurst.wiremock.matching.RequestPattern +import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder +import com.github.tomakehurst.wiremock.matching.StringValuePattern +import com.github.tomakehurst.wiremock.matching.UrlPattern import groovy.json.JsonOutput import groovy.transform.PackageScope import groovy.transform.TypeChecked import groovy.transform.TypeCheckingMode import org.springframework.cloud.contract.spec.Contract -import org.springframework.cloud.contract.spec.internal.* -import org.springframework.cloud.contract.verifier.util.* +import org.springframework.cloud.contract.spec.internal.Body +import org.springframework.cloud.contract.spec.internal.DslProperty +import org.springframework.cloud.contract.spec.internal.MatchingStrategy +import org.springframework.cloud.contract.spec.internal.NamedProperty +import org.springframework.cloud.contract.spec.internal.OptionalProperty +import org.springframework.cloud.contract.spec.internal.QueryParameters +import org.springframework.cloud.contract.spec.internal.RegexPatterns +import org.springframework.cloud.contract.spec.internal.Request +import org.springframework.cloud.contract.verifier.util.MapConverter +import org.springframework.cloud.contract.verifier.util.ContentType +import org.springframework.cloud.contract.verifier.util.ContentUtils +import org.springframework.cloud.contract.verifier.util.JsonPaths +import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter import java.util.regex.Pattern @@ -108,7 +122,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { if (request.multipart.clientValue instanceof Map) { List multipartPatterns = (request.multipart.clientValue as Map).collect { - (it.value instanceof NamedProperty + (it.value instanceof NamedProperty ? WireMock.matching(RegexPatterns.multipartFile(it.key, (it.value as NamedProperty).name.clientValue, (it.value as NamedProperty).value.clientValue)) : WireMock.matching(RegexPatterns.multipartParam(it.key, it.value)) ) } @@ -141,7 +155,7 @@ class WireMockRequestStubStrategy extends BaseWireMockStubStrategy { } Object url = getUrlIfGstring(request?.url?.clientValue) if (url instanceof Pattern) { - return WireMock.urlMatching(url.pattern()) + return WireMock.urlMatching((url as Pattern).pattern()) } return WireMock.urlEqualTo(url.toString()) } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy index 713af9a1dc..ed810b89fe 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/dsl/wiremock/WireMockResponseStubStrategy.groovy @@ -28,6 +28,7 @@ import org.springframework.cloud.contract.spec.internal.Response import org.springframework.cloud.contract.verifier.util.ContentType import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromContent + import static org.springframework.cloud.contract.verifier.util.ContentUtils.recognizeContentTypeFromHeader /** * Converts a {@link Request} into {@link ResponseDefinition} diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy index a9204a0c8e..670a1e4262 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/file/ContractFileScanner.groovy @@ -84,9 +84,12 @@ class ContractFileScanner { */ private void appendRecursively(File baseDir, ListMultimap result) { List converters = SpringFactoriesLoader.loadFactories(ContractConverter, null) + if (log.isDebugEnabled()) { + log.debug("Found the following contract converters ${converters}") + } File[] files = baseDir.listFiles() if (!files) { - return; + return } files.sort().eachWithIndex { File file, int index -> boolean excluded = matchesPattern(file, excludeMatchers) @@ -114,17 +117,19 @@ class ContractFileScanner { private void addContractToTestGeneration(List converters, ListMultimap result, File[] files, File file, int index) { boolean converted = false - for (ContractConverter converter : converters) { - if (converter.isAccepted(file)) { - addContractToTestGeneration(result, files, file, index, converter.convertFrom(file)) - converted = true - break + if (!file.isDirectory()) { + for (ContractConverter converter : converters) { + if (converter.isAccepted(file)) { + addContractToTestGeneration(result, files, file, index, converter.convertFrom(file)) + converted = true + break + } } } if (!converted) { appendRecursively(file, result) if (log.isDebugEnabled()) { - log.debug("File [$file] wasn't ignored but no converter was applicable.") + log.debug("File [$file] wasn't ignored but no converter was applicable. The file is a directory [${file.isDirectory()}]") } } } @@ -147,10 +152,10 @@ class ContractFileScanner { private boolean matchesPattern(File file, Set matchers) { for (PathMatcher matcher : matchers) { if (matcher.matches(file.toPath())) { - return true; + return true } } - return false; + return false } private boolean isContractFile(File file) { @@ -159,15 +164,15 @@ class ContractFileScanner { private static String getFilenameExtension(String path) { if (path == null) { - return null; + return null } int extIndex = path.lastIndexOf('.'); if (extIndex == -1) { - return null; + return null } int folderIndex = path.lastIndexOf('/'); if (folderIndex > extIndex) { - return null; + return null } return path.substring(extIndex + 1); } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy index 0e39559d13..74115b30fd 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/ContentUtils.groovy @@ -15,17 +15,18 @@ */ package org.springframework.cloud.contract.verifier.util + import groovy.json.JsonException import groovy.json.JsonOutput import groovy.json.JsonSlurper import groovy.transform.TypeChecked import groovy.util.logging.Slf4j -import org.springframework.cloud.contract.spec.internal.Headers import org.codehaus.groovy.runtime.GStringImpl -import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.DslProperty import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.spec.internal.Headers import org.springframework.cloud.contract.spec.internal.MatchingStrategy +import org.springframework.cloud.contract.spec.internal.NamedProperty import org.springframework.cloud.contract.spec.internal.OptionalProperty import java.util.regex.Matcher @@ -35,6 +36,11 @@ import static org.apache.commons.lang3.StringEscapeUtils.escapeJava import static org.apache.commons.lang3.StringEscapeUtils.escapeJson import static org.apache.commons.lang3.StringEscapeUtils.escapeXml11 +/** + * A utility class that can operate on a message body basing on the provided Content Type. + * + * @since 1.0.0 + */ @TypeChecked @Slf4j class ContentUtils { @@ -66,7 +72,7 @@ class ContentUtils { * @param valueProvider - provider of values either for server or client side * @return JSON structure with replaced client / server side parts */ - public static Object extractValue(GString bodyAsValue, ContentType contentType, Closure valueProvider) { + static Object extractValue(GString bodyAsValue, ContentType contentType, Closure valueProvider) { if (bodyAsValue.isEmpty()){ return bodyAsValue } @@ -95,7 +101,7 @@ class ContentUtils { } } - public static ContentType getClientContentType(GString bodyAsValue) { + static ContentType getClientContentType(GString bodyAsValue) { try { extractValueForJSON(bodyAsValue, GET_STUB_SIDE) return ContentType.JSON @@ -110,7 +116,7 @@ class ContentUtils { } } - public static ContentType getClientContentType(String bodyAsValue) { + static ContentType getClientContentType(String bodyAsValue) { try { new JsonSlurper().parseText(bodyAsValue) return ContentType.JSON @@ -124,11 +130,11 @@ class ContentUtils { } } - public static ContentType getClientContentType(Object bodyAsValue) { + static ContentType getClientContentType(Object bodyAsValue) { return ContentType.UNKNOWN } - public static ContentType getClientContentType(Map bodyAsValue) { + static ContentType getClientContentType(Map bodyAsValue) { try { JsonOutput.toJson(bodyAsValue) return ContentType.JSON @@ -137,7 +143,7 @@ class ContentUtils { } } - public static ContentType getClientContentType(List bodyAsValue) { + static ContentType getClientContentType(List bodyAsValue) { try { JsonOutput.toJson(bodyAsValue) return ContentType.JSON @@ -153,7 +159,7 @@ class ContentUtils { ) } - public static Object extractValue(GString bodyAsValue, Closure valueProvider) { + static Object extractValue(GString bodyAsValue, Closure valueProvider) { return extractValue(bodyAsValue, ContentType.UNKNOWN, valueProvider) } @@ -275,7 +281,7 @@ class ContentUtils { return val[1] } - public static ContentType recognizeContentTypeFromHeader(Headers headers) { + static ContentType recognizeContentTypeFromHeader(Headers headers) { String content = headers?.entries.find { it.name == "Content-Type" } ?.clientValue?.toString() if (content?.endsWith("json")) { return ContentType.JSON @@ -289,7 +295,7 @@ class ContentUtils { return ContentType.UNKNOWN } - public static MatchingStrategy.Type getEqualsTypeFromContentType(ContentType contentType) { + static MatchingStrategy.Type getEqualsTypeFromContentType(ContentType contentType) { switch (contentType) { case ContentType.JSON: return MatchingStrategy.Type.EQUAL_TO_JSON @@ -299,7 +305,7 @@ class ContentUtils { return MatchingStrategy.Type.EQUAL_TO } - public static ContentType recognizeContentTypeFromContent(GString gstring) { + static ContentType recognizeContentTypeFromContent(GString gstring) { if (isJsonType(gstring)) { return ContentType.JSON } @@ -309,15 +315,15 @@ class ContentUtils { return ContentType.UNKNOWN } - public static ContentType recognizeContentTypeFromContent(Map jsonMap) { + static ContentType recognizeContentTypeFromContent(Map jsonMap) { return ContentType.JSON } - public static ContentType recognizeContentTypeFromContent(List jsonList) { + static ContentType recognizeContentTypeFromContent(List jsonList) { return ContentType.JSON } - public static ContentType recognizeContentTypeFromContent(String string) { + static ContentType recognizeContentTypeFromContent(String string) { try { new JsonSlurper().parseText(string) return ContentType.JSON @@ -326,11 +332,11 @@ class ContentUtils { } } - public static ContentType recognizeContentTypeFromContent(Object gstring) { + static ContentType recognizeContentTypeFromContent(Object gstring) { return ContentType.UNKNOWN } - public static boolean isJsonType(GString gstring) { + static boolean isJsonType(GString gstring) { if (gstring.isEmpty()) { return false } @@ -349,7 +355,7 @@ class ContentUtils { return false } - public static boolean isXmlType(GString gstring) { + static boolean isXmlType(GString gstring) { GString stringWithoutValues = new GStringImpl( gstring.values.collect({ it instanceof String || it instanceof GString ? it.toString() : escapeXml11(it.toString()) @@ -365,7 +371,7 @@ class ContentUtils { return false } - public static ContentType recognizeContentTypeFromMatchingStrategy(MatchingStrategy.Type type) { + static ContentType recognizeContentTypeFromMatchingStrategy(MatchingStrategy.Type type) { switch (type) { case MatchingStrategy.Type.EQUAL_TO_XML: return ContentType.XML diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy index 3e5fc06487..0f499ef57a 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/JsonToJsonPathsConverter.groovy @@ -23,7 +23,11 @@ import com.toomuchcoding.jsonassert.JsonAssertion import groovy.json.JsonOutput import groovy.json.JsonSlurper import groovy.util.logging.Slf4j -import org.springframework.cloud.contract.spec.internal.* +import org.springframework.cloud.contract.spec.internal.BodyMatcher +import org.springframework.cloud.contract.spec.internal.BodyMatchers +import org.springframework.cloud.contract.spec.internal.ExecutionProperty +import org.springframework.cloud.contract.spec.internal.MatchingType +import org.springframework.cloud.contract.spec.internal.OptionalProperty import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties import java.util.regex.Pattern diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy index 31c5aa1899..6a8d1e3cf0 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/MapConverter.groovy @@ -27,7 +27,7 @@ import org.springframework.cloud.contract.spec.internal.DslProperty * * @author Marcin Grzejszczak * - * @since 1.0.0 + * @since 1.1.0 */ class MapConverter { @@ -58,7 +58,7 @@ class MapConverter { } } catch (Exception ignore) { } - return extractValue(value, closure); + return extractValue(value, closure) } else if (value instanceof Map) { return convert(value as Map, closure) } else if (value instanceof List) { @@ -77,7 +77,7 @@ class MapConverter { if (newValue instanceof Map || newValue instanceof List || newValue instanceof String && value) { return transformValues(newValue, closure) } - return newValue; + return newValue }) } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy index a07700d914..a13924269f 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/util/RegexpBuilders.groovy @@ -23,8 +23,9 @@ import org.springframework.cloud.contract.spec.util.RegexpUtils import java.util.regex.Pattern -import static ContentUtils.extractValue import static org.apache.commons.lang3.StringEscapeUtils.escapeJson +import static ContentType.* +import static ContentUtils.extractValue /** * Useful utility methods to work with regular expresisons @@ -105,7 +106,7 @@ class RegexpBuilders { private final static String WS = /\s*/ static String buildJSONRegexpMatch(GString gString) { - return buildJSONRegexpMatch(extractValue(gString, ContentType.JSON, { DslProperty dslProperty -> dslProperty.clientValue })) + return buildJSONRegexpMatch(extractValue(gString, JSON, { DslProperty dslProperty -> dslProperty.clientValue })) } static String buildJSONRegexpMatch(Map jsonMap) { diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/ContentUtilsSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/ContentUtilsSpec.groovy new file mode 100644 index 0000000000..98c2229de7 --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/util/ContentUtilsSpec.groovy @@ -0,0 +1,25 @@ +package org.springframework.cloud.contract.verifier.util + +import org.springframework.cloud.contract.spec.internal.DslProperty +import org.springframework.cloud.contract.verifier.util.ContentUtils +import spock.lang.Specification + +/** + * @author Marcin Grzejszczak + */ +class ContentUtilsSpec extends Specification { + + def "should return the stub side"() { + given: + DslProperty dslProperty = new DslProperty<>("stub", "test") + expect: + "stub" == ContentUtils.GET_STUB_SIDE(dslProperty) + } + + def "should return the test side"() { + given: + DslProperty dslProperty = new DslProperty<>("stub", "test") + expect: + "test" == ContentUtils.GET_TEST_SIDE(dslProperty) + } +}