From f1c11281dc67e5c61a5d0c66cfdb6d6aca9c1eba Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Fri, 21 Jul 2017 09:26:37 -0600 Subject: [PATCH 01/18] Initial commit --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4fa332f5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +build +.gradle +.idea +*.iml +ci/variables.yml From 7dfe5b5bc9d4d3924790e70c41aedc3e82b4c0db Mon Sep 17 00:00:00 2001 From: Ruurd Keizer Date: Mon, 14 Aug 2017 15:33:58 +0200 Subject: [PATCH 02/18] Simple Spring Boot app --- settings.gradle | 1 + .../pivotal/pal/tracker/PalTrackerApplication.java | 11 +++++++++++ .../io/pivotal/pal/tracker/WelcomeController.java | 13 +++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 settings.gradle create mode 100644 src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java create mode 100644 src/main/java/io/pivotal/pal/tracker/WelcomeController.java diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..ef961960e --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = "pal-tracker" \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java new file mode 100644 index 000000000..6b3c2c877 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -0,0 +1,11 @@ +package io.pivotal.pal.tracker; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class PalTrackerApplication { + public static void main(String[] args) { + SpringApplication.run(PalTrackerApplication.class, args); + } +} diff --git a/src/main/java/io/pivotal/pal/tracker/WelcomeController.java b/src/main/java/io/pivotal/pal/tracker/WelcomeController.java new file mode 100644 index 000000000..42ea947dd --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/WelcomeController.java @@ -0,0 +1,13 @@ +package io.pivotal.pal.tracker; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class WelcomeController { + + @GetMapping("/") + public String sayHello() { + return "hello"; + } +} From 97d47d46a3a552a438b5f5d2376a20337e3e6ac9 Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Thu, 20 Jul 2017 13:56:50 -0600 Subject: [PATCH 03/18] Add tests for deployment lab --- .../pal/tracker/EnvControllerTest.java | 28 +++++++++++++++++++ .../pal/tracker/WelcomeControllerTest.java | 16 +++++++++++ .../pal/trackerapi/WelcomeApiTest.java | 26 +++++++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 src/test/java/test/pivotal/pal/tracker/EnvControllerTest.java create mode 100644 src/test/java/test/pivotal/pal/tracker/WelcomeControllerTest.java create mode 100644 src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java diff --git a/src/test/java/test/pivotal/pal/tracker/EnvControllerTest.java b/src/test/java/test/pivotal/pal/tracker/EnvControllerTest.java new file mode 100644 index 000000000..fda0f0f34 --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/EnvControllerTest.java @@ -0,0 +1,28 @@ +package test.pivotal.pal.tracker; + +import org.junit.Test; + +import java.util.Map; +import io.pivotal.pal.tracker.EnvController; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EnvControllerTest { + @Test + public void getEnv() throws Exception { + EnvController controller = new EnvController( + "8675", + "12G", + "34", + "123.sesame.street" + ); + + Map env = controller.getEnv(); + + assertThat(env.get("PORT")).isEqualTo("8675"); + assertThat(env.get("MEMORY_LIMIT")).isEqualTo("12G"); + assertThat(env.get("CF_INSTANCE_INDEX")).isEqualTo("34"); + assertThat(env.get("CF_INSTANCE_ADDR")).isEqualTo("123.sesame.street"); + } + +} diff --git a/src/test/java/test/pivotal/pal/tracker/WelcomeControllerTest.java b/src/test/java/test/pivotal/pal/tracker/WelcomeControllerTest.java new file mode 100644 index 000000000..bfa8271a0 --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/WelcomeControllerTest.java @@ -0,0 +1,16 @@ +package test.pivotal.pal.tracker; + +import io.pivotal.pal.tracker.WelcomeController; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class WelcomeControllerTest { + + @Test + public void itSaysHello() throws Exception { + WelcomeController controller = new WelcomeController("A welcome message"); + + assertThat(controller.sayHello()).isEqualTo("A welcome message"); + } +} diff --git a/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java new file mode 100644 index 000000000..cc7091ed4 --- /dev/null +++ b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java @@ -0,0 +1,26 @@ +package test.pivotal.pal.trackerapi; + +import io.pivotal.pal.tracker.PalTrackerApplication; +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.web.client.TestRestTemplate; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) +public class WelcomeApiTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void exampleTest() { + String body = this.restTemplate.getForObject("/", String.class); + assertThat(body).isEqualTo("Hello from test"); + } +} From ffa287f2e1bf2824f8c08841be211629e7901a71 Mon Sep 17 00:00:00 2001 From: Ruurd Keizer Date: Tue, 15 Aug 2017 10:49:58 +0200 Subject: [PATCH 04/18] Fly stuff --- build.gradle | 21 +++ ci/build.yml | 22 +++ ci/pipeline.yml | 31 ++++ ci/variables.example.yml | 9 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54212 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++++++++++++++++++ gradlew.bat | 84 +++++++++ manifest.yml | 7 + .../io/pivotal/pal/tracker/EnvController.java | 28 +++ .../pal/tracker/WelcomeController.java | 11 +- 11 files changed, 390 insertions(+), 1 deletion(-) create mode 100644 build.gradle create mode 100644 ci/build.yml create mode 100644 ci/pipeline.yml create mode 100644 ci/variables.example.yml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 manifest.yml create mode 100644 src/main/java/io/pivotal/pal/tracker/EnvController.java diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..e10110928 --- /dev/null +++ b/build.gradle @@ -0,0 +1,21 @@ +plugins { + id "java" + id 'org.springframework.boot' version '1.5.6.RELEASE' +} + +repositories { + mavenCentral() +} + +dependencies { + compile("org.springframework.boot:spring-boot-starter-web") + testCompile("org.springframework.boot:spring-boot-starter-test") +} + +bootRun.environment([ + "WELCOME_MESSAGE": "hello", +]) + +test.environment([ + "WELCOME_MESSAGE": "Hello from test", +]) \ No newline at end of file diff --git a/ci/build.yml b/ci/build.yml new file mode 100644 index 000000000..e9b625396 --- /dev/null +++ b/ci/build.yml @@ -0,0 +1,22 @@ +platform: linux + +image_resource: + type: docker-image + source: + repository: openjdk + tag: '8-jdk' + +inputs: + - name: pal-tracker + +outputs: + - name: build-output + +run: + path: bash + args: + - -exc + - | + cd pal-tracker + ./gradlew build + cp build/libs/pal-tracker.jar ../build-output \ No newline at end of file diff --git a/ci/pipeline.yml b/ci/pipeline.yml new file mode 100644 index 000000000..c72874c4a --- /dev/null +++ b/ci/pipeline.yml @@ -0,0 +1,31 @@ +--- +resources: +- name: pal-tracker + type: git + source: + uri: {{github-repository}} + branch: master + private_key: {{github-private-key}} + +- name: deploy + type: cf + source: + api: {{cf-api-url}} + username: {{cf-username}} + password: {{cf-password}} + organization: {{cf-org}} + space: sandbox + +jobs: +- name: build-and-deploy + plan: + - get: pal-tracker + trigger: true + - task: build and test + file: pal-tracker/ci/build.yml + - put: deploy + params: + manifest: pal-tracker/manifest.yml + path: build-output/pal-tracker.jar + environment_variables: + WELCOME_MESSAGE: "Hello from Concourse" \ No newline at end of file diff --git a/ci/variables.example.yml b/ci/variables.example.yml new file mode 100644 index 000000000..801cec689 --- /dev/null +++ b/ci/variables.example.yml @@ -0,0 +1,9 @@ +cf-api-url: CF_API_URL +cf-username: CF_USERNAME +cf-password: CF_PASSWORD +cf-org: CF_ORG +github-repository: git@github.com:ruurdk/pal-tracker.git +github-private-key: | + -----BEGIN RSA PRIVATE KEY----- + REPLACE WITH YOUR PRIVATE KEY HERE + -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..56e0ca709d4f270ff65f881d6884edc2385a074f GIT binary patch literal 54212 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
  • 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..e95643d6a --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/manifest.yml b/manifest.yml new file mode 100644 index 000000000..6ab3d3080 --- /dev/null +++ b/manifest.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: pal-tracker + path: build/libs/pal-tracker.jar + random-route: true + env: + \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/EnvController.java b/src/main/java/io/pivotal/pal/tracker/EnvController.java new file mode 100644 index 000000000..ca286847f --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/EnvController.java @@ -0,0 +1,28 @@ +package io.pivotal.pal.tracker; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; +import java.util.Map; + +@RestController +public class EnvController { + private final Map env = new HashMap<>(); + + public EnvController(@Value("${PORT:NOT SET}") String port, + @Value("${MEMORY_LIMIT:NOT SET}") String memory_limit, + @Value("${CF_INSTANCE_INDEX:NOT SET}") String cf_instance_index, + @Value("${CF_INSTANCE_ADDR:NOT SET}") String cf_instance_addr) { + env.put("PORT", port); + env.put("MEMORY_LIMIT", memory_limit); + env.put("CF_INSTANCE_INDEX", cf_instance_index); + env.put("CF_INSTANCE_ADDR", cf_instance_addr); + } + + @GetMapping("/env") + public Map getEnv(){ + return env; + } +} diff --git a/src/main/java/io/pivotal/pal/tracker/WelcomeController.java b/src/main/java/io/pivotal/pal/tracker/WelcomeController.java index 42ea947dd..f3af50fc5 100644 --- a/src/main/java/io/pivotal/pal/tracker/WelcomeController.java +++ b/src/main/java/io/pivotal/pal/tracker/WelcomeController.java @@ -1,13 +1,22 @@ package io.pivotal.pal.tracker; +import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class WelcomeController { + private final String hello; + + + public WelcomeController(@Value("${welcome_message}") String message) { + hello = message; + } + @GetMapping("/") public String sayHello() { - return "hello"; + + return hello; } } From 46edb2711f7a3ee892a94bf33066062cce2ec07d Mon Sep 17 00:00:00 2001 From: Ruurd Keizer Date: Tue, 15 Aug 2017 10:53:29 +0200 Subject: [PATCH 05/18] executable stuff --- gradlew | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From 385d4111ea57a3aada1417e68232dd8d497cc331 Mon Sep 17 00:00:00 2001 From: Ruurd Keizer Date: Tue, 15 Aug 2017 10:54:48 +0200 Subject: [PATCH 06/18] env removed from manifest --- manifest.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/manifest.yml b/manifest.yml index 6ab3d3080..69ad95367 100644 --- a/manifest.yml +++ b/manifest.yml @@ -2,6 +2,4 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar - random-route: true - env: - \ No newline at end of file + random-route: true \ No newline at end of file From 364afd68fc0d20515c4a88127bed18180f33bfc2 Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Thu, 20 Jul 2017 15:04:20 -0600 Subject: [PATCH 07/18] Add tests for MVC lab --- .../InMemoryTimeEntryRepositoryTest.java | 71 +++++++++++ .../pal/tracker/TimeEntryControllerTest.java | 107 ++++++++++++++++ .../pal/trackerapi/TimeEntryApiTest.java | 119 ++++++++++++++++++ 3 files changed, 297 insertions(+) create mode 100644 src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java create mode 100644 src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java create mode 100644 src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java diff --git a/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java b/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java new file mode 100644 index 000000000..95e810cad --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.java @@ -0,0 +1,71 @@ +package test.pivotal.pal.tracker; + +import io.pivotal.pal.tracker.InMemoryTimeEntryRepository; +import io.pivotal.pal.tracker.TimeEntry; + +import org.junit.Test; + +import java.util.List; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +public class InMemoryTimeEntryRepositoryTest { + @Test + public void create() throws Exception { + InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntry createdTimeEntry = repo.create(new TimeEntry(123, 456, "today", 8)); + + TimeEntry expected = new TimeEntry(1L, 123, 456, "today", 8); + assertThat(createdTimeEntry).isEqualTo(expected); + + TimeEntry readEntry = repo.find(createdTimeEntry.getId()); + assertThat(readEntry).isEqualTo(expected); + } + + @Test + public void find() throws Exception { + InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + repo.create(new TimeEntry(123, 456, "today", 8)); + + TimeEntry expected = new TimeEntry(1L, 123, 456, "today", 8); + TimeEntry readEntry = repo.find(1L); + assertThat(readEntry).isEqualTo(expected); + } + + @Test + public void list() throws Exception { + InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + repo.create(new TimeEntry(123, 456, "today", 8)); + repo.create(new TimeEntry(789, 654, "yesterday", 4)); + + List expected = asList( + new TimeEntry(1L, 123, 456, "today", 8), + new TimeEntry(2L, 789, 654, "yesterday", 4) + ); + assertThat(repo.list()).isEqualTo(expected); + } + + @Test + public void update() throws Exception { + InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntry created = repo.create(new TimeEntry(123, 456, "today", 8)); + + TimeEntry updatedEntry = repo.update( + created.getId(), + new TimeEntry(321, 654, "tomorrow", 5)); + + TimeEntry expected = new TimeEntry(created.getId(), 321, 654, "tomorrow", 5); + assertThat(updatedEntry).isEqualTo(expected); + assertThat(repo.find(created.getId())).isEqualTo(expected); + } + + @Test + public void delete() throws Exception { + InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntry created = repo.create(new TimeEntry(123, 456, "today", 8)); + + repo.delete(created.getId()); + assertThat(repo.list()).isEmpty(); + } +} diff --git a/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java new file mode 100644 index 000000000..a6f1100b5 --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java @@ -0,0 +1,107 @@ +package test.pivotal.pal.tracker; + +import io.pivotal.pal.tracker.TimeEntryRepository; +import io.pivotal.pal.tracker.TimeEntryController; +import io.pivotal.pal.tracker.TimeEntry; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.List; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class TimeEntryControllerTest { + private TimeEntryRepository timeEntryRepository; + private TimeEntryController controller; + + @Before + public void setUp() throws Exception { + timeEntryRepository = mock(TimeEntryRepository.class); + controller = new TimeEntryController(timeEntryRepository); + } + + @Test + public void testCreate() throws Exception { + TimeEntry expected = new TimeEntry(1L, 123, 456, "today", 8); + doReturn(expected) + .when(timeEntryRepository) + .create(any(TimeEntry.class)); + + ResponseEntity response = controller.create(new TimeEntry(123, 456, "today", 8)); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getBody()).isEqualTo(expected); + } + + @Test + public void testRead() throws Exception { + TimeEntry expected = new TimeEntry(1L, 123, 456, "today", 8); + doReturn(expected) + .when(timeEntryRepository) + .find(1L); + + ResponseEntity response = controller.read(1L); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(expected); + } + + @Test + public void testRead_NotFound() throws Exception { + doReturn(null) + .when(timeEntryRepository) + .find(1L); + + ResponseEntity response = controller.read(1L); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void testList() throws Exception { + List expected = asList( + new TimeEntry(1, 123, 456, "today", 8), + new TimeEntry(2, 789, 321, "yesterday", 4) + ); + doReturn(expected).when(timeEntryRepository).list(); + + ResponseEntity> response = controller.list(); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(expected); + } + + @Test + public void testUpdate() throws Exception { + TimeEntry expected = new TimeEntry(1, 987, 654, "yesterday", 4); + doReturn(expected) + .when(timeEntryRepository) + .update(eq(1L), any(TimeEntry.class)); + + ResponseEntity response = controller.update(1L, expected); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(expected); + } + + @Test + public void testUpdate_NotFound() throws Exception { + doReturn(null) + .when(timeEntryRepository) + .update(eq(1L), any(TimeEntry.class)); + + ResponseEntity response = controller.update(1L, new TimeEntry()); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + public void testDelete() throws Exception { + ResponseEntity response = controller.delete(1L); + verify(timeEntryRepository).delete(1L); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } +} diff --git a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java new file mode 100644 index 000000000..36dd6c687 --- /dev/null +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java @@ -0,0 +1,119 @@ +package test.pivotal.pal.trackerapi; + +import com.jayway.jsonpath.DocumentContext; +import io.pivotal.pal.tracker.PalTrackerApplication; +import io.pivotal.pal.tracker.TimeEntry; +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.web.client.TestRestTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Collection; + +import static com.jayway.jsonpath.JsonPath.parse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) +public class TimeEntryApiTest { + + @Autowired + private TestRestTemplate restTemplate; + + private TimeEntry timeEntry = new TimeEntry(123, 456, "today", 8); + + @Test + public void testCreate() throws Exception { + ResponseEntity createResponse = restTemplate.postForEntity("/time-entries", timeEntry, String.class); + + + assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + DocumentContext createJson = parse(createResponse.getBody()); + assertThat(createJson.read("$.id", Long.class)).isGreaterThan(0); + assertThat(createJson.read("$.projectId", Long.class)).isEqualTo(123L); + assertThat(createJson.read("$.userId", Long.class)).isEqualTo(456L); + assertThat(createJson.read("$.date", String.class)).isEqualTo("today"); + assertThat(createJson.read("$.hours", Long.class)).isEqualTo(8); + } + + @Test + public void testList() throws Exception { + Long id = createTimeEntry(); + + + ResponseEntity listResponse = restTemplate.getForEntity("/time-entries", String.class); + + + assertThat(listResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + DocumentContext listJson = parse(listResponse.getBody()); + + Collection timeEntries = listJson.read("$[*]", Collection.class); + assertThat(timeEntries.size()).isEqualTo(1); + + Long readId = listJson.read("$[0].id", Long.class); + assertThat(readId).isEqualTo(id); + } + + @Test + public void testRead() throws Exception { + Long id = createTimeEntry(); + + + ResponseEntity readResponse = this.restTemplate.getForEntity("/time-entries/" + id, String.class); + + + assertThat(readResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + DocumentContext readJson = parse(readResponse.getBody()); + assertThat(readJson.read("$.id", Long.class)).isEqualTo(id); + assertThat(readJson.read("$.projectId", Long.class)).isEqualTo(123L); + assertThat(readJson.read("$.userId", Long.class)).isEqualTo(456L); + assertThat(readJson.read("$.date", String.class)).isEqualTo("today"); + assertThat(readJson.read("$.hours", Long.class)).isEqualTo(8); + } + + @Test + public void testUpdate() throws Exception { + Long id = createTimeEntry(); + TimeEntry updatedTimeEntry = new TimeEntry(2, 3, "tomorrow", 9); + + + ResponseEntity updateResponse = restTemplate.exchange("/time-entries/" + id, HttpMethod.PUT, new HttpEntity<>(updatedTimeEntry, null), String.class); + + + assertThat(updateResponse.getStatusCode()).isEqualTo(HttpStatus.OK); + + DocumentContext updateJson = parse(updateResponse.getBody()); + assertThat(updateJson.read("$.id", Long.class)).isEqualTo(id); + assertThat(updateJson.read("$.projectId", Long.class)).isEqualTo(2L); + assertThat(updateJson.read("$.userId", Long.class)).isEqualTo(3L); + assertThat(updateJson.read("$.date", String.class)).isEqualTo("tomorrow"); + assertThat(updateJson.read("$.hours", Long.class)).isEqualTo(9); + } + + @Test + public void testDelete() throws Exception { + Long id = createTimeEntry(); + + + ResponseEntity deleteResponse = restTemplate.exchange("/time-entries/" + id, HttpMethod.DELETE, null, String.class); + + + assertThat(deleteResponse.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + + ResponseEntity deletedReadResponse = this.restTemplate.getForEntity("/time-entries/" + id, String.class); + assertThat(deletedReadResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + private Long createTimeEntry() { + return restTemplate.postForObject("/time-entries", timeEntry, TimeEntry.class).getId(); + } +} From ec02471f864dd2a00211f50fbcfc6a8f851b044c Mon Sep 17 00:00:00 2001 From: Ruurd Keizer Date: Tue, 15 Aug 2017 15:56:03 +0200 Subject: [PATCH 08/18] Spring Boot App --- .../pivotal/pal/tracker/EnvController.class | Bin 0 -> 1504 bytes .../tracker/InMemoryTimeEntryRepository.class | Bin 0 -> 1945 bytes .../pal/tracker/PalTrackerApplication.class | Bin 0 -> 993 bytes .../io/pivotal/pal/tracker/TimeEntry.class | Bin 0 -> 2127 bytes .../pal/tracker/TimeEntryController.class | Bin 0 -> 2613 bytes .../pal/tracker/TimeEntryRepository.class | Bin 0 -> 560 bytes .../pal/tracker/WelcomeController.class | Bin 0 -> 838 bytes .../pal/tracker/EnvControllerTest.class | Bin 0 -> 1469 bytes .../InMemoryTimeEntryRepositoryTest.class | Bin 0 -> 3052 bytes .../pal/tracker/TimeEntryControllerTest.class | Bin 0 -> 5066 bytes .../pal/tracker/WelcomeControllerTest.class | Bin 0 -> 1029 bytes .../pal/trackerapi/TimeEntryApiTest.class | Bin 0 -> 6313 bytes .../pal/trackerapi/WelcomeApiTest.class | Bin 0 -> 1704 bytes .../tracker/InMemoryTimeEntryRepository.java | 40 ++++++++ .../pal/tracker/PalTrackerApplication.java | 6 ++ .../io/pivotal/pal/tracker/TimeEntry.java | 87 ++++++++++++++++++ .../pal/tracker/TimeEntryController.java | 58 ++++++++++++ .../pal/tracker/TimeEntryRepository.java | 11 +++ 18 files changed, 202 insertions(+) create mode 100644 out/production/classes/io/pivotal/pal/tracker/EnvController.class create mode 100644 out/production/classes/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.class create mode 100644 out/production/classes/io/pivotal/pal/tracker/PalTrackerApplication.class create mode 100644 out/production/classes/io/pivotal/pal/tracker/TimeEntry.class create mode 100644 out/production/classes/io/pivotal/pal/tracker/TimeEntryController.class create mode 100644 out/production/classes/io/pivotal/pal/tracker/TimeEntryRepository.class create mode 100644 out/production/classes/io/pivotal/pal/tracker/WelcomeController.class create mode 100644 out/test/classes/test/pivotal/pal/tracker/EnvControllerTest.class create mode 100644 out/test/classes/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.class create mode 100644 out/test/classes/test/pivotal/pal/tracker/TimeEntryControllerTest.class create mode 100644 out/test/classes/test/pivotal/pal/tracker/WelcomeControllerTest.class create mode 100644 out/test/classes/test/pivotal/pal/trackerapi/TimeEntryApiTest.class create mode 100644 out/test/classes/test/pivotal/pal/trackerapi/WelcomeApiTest.class create mode 100644 src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java create mode 100644 src/main/java/io/pivotal/pal/tracker/TimeEntry.java create mode 100644 src/main/java/io/pivotal/pal/tracker/TimeEntryController.java create mode 100644 src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java diff --git a/out/production/classes/io/pivotal/pal/tracker/EnvController.class b/out/production/classes/io/pivotal/pal/tracker/EnvController.class new file mode 100644 index 0000000000000000000000000000000000000000..13735a1784f66ebd244f377221b17e4c469b57b8 GIT binary patch literal 1504 zcmbVMYflqF6g}H6v_O%Uf;>dPiclWwgO@-^NGVEe3uzma_@SAWp$u$yo84*A5Py@J zXo#Bl1N>3OJKM%C!Uy7q***8(nLB6By)(alfBy+!0c-8(z-$O}?FeAD4egi@!|1?c zJPG4z7|+632w^dVB@IDt)iiV^Pgso^RVhp(#VTtW+Onc#FaAs zMf?_$wo@|Pijwn4O*pfiQ=`C{99bF5PL`0GK-t`vc~ z$L~|hYlz;YZ)s(-U3Uv7yh!aCoKm-?v&oK_k+)K%*oyx~)))9Mal_N7uy+X>E{xN2 z(m~G*T@iIk`rjfefG!$D13mQbCC^6$(L(=3>;hWs2LcBd2!4eg`vSeUW#$`NgLv&3 zp;@01p7R-PgL>Wi>}}CU-%*6<+XJ08@1l)H(1&T<#uDye3;l%D3+Qc3o_?~30tRrG z$SE{PDBz(XLO~A=6VfRYFsIYBdo)H}+;76MCLC|V34#w0r6lUS23cy07Imgi1Omhi qX!V>!9459DEJ{ literal 0 HcmV?d00001 diff --git a/out/production/classes/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.class b/out/production/classes/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..3ef76f098da068cb6b60372e15cbd444f13c2efa GIT binary patch literal 1945 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 literal 0 HcmV?d00001 diff --git a/out/production/classes/io/pivotal/pal/tracker/TimeEntryController.class b/out/production/classes/io/pivotal/pal/tracker/TimeEntryController.class new file mode 100644 index 0000000000000000000000000000000000000000..10440409564195418e09e192e7d577b1370a1f66 GIT binary patch literal 2613 zcmb_dVRO?)5Pc^*ajl3DFc7CLg#ZD?2?*M>r8rHS8mBZUcIvq9@QIOa#T+P-E6ZW> zui8vIY1*0ofc~fsyONPZA!B)l4?f?mZr{Frcf0rRe}DZ0UdkSAm9m`r4YC_GHcpA6R~K=5(;E@WXH9FzE9kKd7s=? zM-y5jN@>!iO>*j+<7BN!@)4T?+hh18W#q_SAh4v5L&!?Ld)0hy?OR4akd9$es6@0_ zs>y!09p0gezByXlaoGFs6);gklup}rY%U3P$LsqodqZ-~<>R|Kte-n9n7~}EPq%*0 zZpfabdR%v1>J5RUauB%x&XRd)H;txr+lCeIqU4z)u^(x(H-lxpSmdikNi!aI*9>}Ep; z!hN7eg>fR$Vj@?TVtS~N>A4+>zi&BxySgio;}-GeXYM<@bT0%}P6lO9YQ70HP^ypV zd7zpPapO`Z+{Rmgqwx}j%)JH7d8?J`(^9okFIVaUx}H!k%+rHCLl3xs!dIVWlQhra z0*x1GwLx};{?)<}L}BRkq=JCvnkq!euZjr5b{LrffD@>lv41yDn<9Hz*P!LLScG59P##$|$@f`(6M zrr=8GdzEbSxHbr9Vj}d*a|{l7SS9)VSxEFu}(9a*O<)u_A#bXrvT5NkZ3NZ zJvX4uX|Y(TK>|w@Y&n)_o9uid`Jp_T0*-u$Ge?;I|B)=<3?-jLCIz<%SHT?^^v?p{ G1zrJi1Hdo< literal 0 HcmV?d00001 diff --git a/out/production/classes/io/pivotal/pal/tracker/TimeEntryRepository.class b/out/production/classes/io/pivotal/pal/tracker/TimeEntryRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..7f2d9dfdfd81f3a85f7ae54dbc34aea50b2ce830 GIT binary patch literal 560 zcma)3y-ve05dN;D2`zu6U3i3q0WTFx5nDS&LZYadL>_0<@703Hf)4H6wP zq=R+R_j7mt?)&HS3&0eU2-gwDgfMeTdPP{oskWlhTkE9}6+iD}_O6_G(I0AFdbgV; z-4a6Zrpuf#jnlQNozu~A`F+dc9~je!;+i-aGzCWH`-gbyW8hRxa~T} z!9f@uvVMA9tIUT4LXU3^^aH{P0)T5DVN)ZVGCyFphauxZV-7~h&-7p;BC;%qLp7Klz?f8+%y|HIp{3Y;I6eCGh zs^j2L+nXOJWfKRpx}O*poj7}0PHC;(nI8nh#{aEdqN9n-c&7B>`DR{7|4Iyp<>(n_ zsPCJR&E>8N6K%~+=TW#ZhQ}8k`Jw`Q()n}4O%UT=f+}hZ?aN*X!}7m}@+Lcxxo22t zcTzl{XH}tRMpF!$vLw%!Jn|)1XX? V*2)JkhwH?-F=cKb?1rK@fj`|9(cb_7 literal 0 HcmV?d00001 diff --git a/out/test/classes/test/pivotal/pal/tracker/EnvControllerTest.class b/out/test/classes/test/pivotal/pal/tracker/EnvControllerTest.class new file mode 100644 index 0000000000000000000000000000000000000000..7edbc421f4d5471d58ffe66c6c9c26a499ee4a53 GIT binary patch literal 1469 zcmbVMTT|0O6#lj?O$bFwxG3HLMWld4?jRSfZBfP+6gntROo%H4OHz_8qd&_R@G|50 z?1Mka@oZBuoqA#HOwaCqd(OAtxlF!(|M(fe6dr5Paa+NVij<0B6(cG}HAHYnnlTkd z3+`fE!GwlMOlg?LjEY$mb1LQ)+*7c?5MQ(%TP!m~hf-S%v5fnYGqmMxhp$x*cez(I zcT0rmdDk*aTc&5ra}A7%ecNXk%?s`eqii3$!YmnO`U}srUUSdLIma2-5uRHr5fypK zC-=A@oI$hBt{4XMw)<}i3XgIb+m=5tk4>W@?2=J1%Y=8=D|}o2sdBpBbul=WOlQy7 z6rSzuEi@v_M5yK7SiCH3*YOoBGW2Xz9AO{wE!(%L25HBkbgS14$-L|B83z@rrBO>$ z+jJ|Q#aC=uzV3!Xj7j|obfA-A;5?~QbLM7dr>Owr6OR}olT!@s;}er(KKIQ-KIRLL zb0Ha*61WU%Y3%9xMp4221Rfww8mkox<*ndh0vTi#HFLh2ZrT(+%U_wk*)%AXlR0h+v2`S zyty!MZSYdnv1#PkrDmFz$n2ZmCO@ih$KnAmjivGXXa5+&&}{p;ql#H7x(xGS6=&J~ zm9q9S`q54w9QliYnuboYU34;NWwbU8pMt%sUU!p?S3wOIX`cuja0xwhVlV7s6O9z}muCQ-CGI zSX^RKT@+H?btq(8kt8KugrrF466t$J3OVVMs`~#j`Y!UiMhp@3l9$Xa aK>7&jCnQG54cw%itib@S47bQeF!%$aGJFF7 literal 0 HcmV?d00001 diff --git a/out/test/classes/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.class b/out/test/classes/test/pivotal/pal/tracker/InMemoryTimeEntryRepositoryTest.class new file mode 100644 index 0000000000000000000000000000000000000000..b29b333b1498268f830af0d93e84df54181cd510 GIT binary patch literal 3052 zcmbVO>sHiO7~O{nWQ;H zLtmkPMpkY6yQ?qI-+hs;?vrFPQ*l5$YmuCM=R4=y``h~@{Q1u>zXQnOrG|63qvEcH z4tc-V$*}x=9}mPIX-sKQFfB9g%h6{VW-zPabL8dYoQ8Qkkh2ftHK`BcLC242JZkLN zp_QidB%)0 zXKlJ(ZOPuWrkud5Em#%Tw*wjCnsEXsc$OKkg5q&XAeXn@_uY!ybVu71lR^S#+wGL( za9!GZS+NR%RTS7qAjJ@*fHw8AU{wO!b$k`y2=pyfoq!ycZQmyUNyj18aGIs_u2(WP zs_df?lTy}ghdU}|+Ye;IhPh=L)xa(rc}7^a_rL9|RT?eLy^%YyH;G%ai`7ao?4la? zK|qf}4Mtr>t8B?WYm08xD_GOEl=a*m0$q~Gb)3h5z~%QBMxejVqC^9M&cH32HCg0a z9V7Toj!YG6ItnPNuym}Wq~j@U6&pI9p{(ONHYr$*7P34pR^jMyQPFV&&*eH#g)i5! z`{}@?13mYJjtNwC+{TuU7n0D8|7%-H8Y)^gos#ijZG#39IMNIy<`FoSot=#&4U-oG z&vr`VGxDrWXZO~2aPJhFlQr5EIW4_u`o84_8%DwPEW@nW#$*_f?xr82*`=o@N!-r1 zfQ^`q4~G8UxyPC>Pk9h!MLW`ogKsbRmShTO`%}-WW_gL{{^t8vPHJ|RtYC)cjqk8o zfrCvU@~%@7P`Au-)q1c_jk2?YEd=E>A?I34hp~5Rd*p%eR8M-ISz|A|OkW;wf%DlX zEp?JchR9mq13lRoPiyc>jHelEBY~`yG~w+^H&X;YY2}$<+EuUSa`tJtb$!^Uz1{gv zojkH*V|hMQx?{5v)NlqL@jEJiU0`c5!0QD*8N3VLJBMCD{LDcIvT+h{0D~OsQ38fA z%qPLu%K%d0;$NXbejgWJqhs|IQu*Q6P*yYO%s}ng*PVU^t&aV3GN`pi_4$!)9JpA= z!Ij~WIu324yN<(SN}tjQH<6rz$_m{`<1kL)2u6{?79LWH>pMT{_wSzN>= zB0a_b9}IjFHG?;Z{0-qjIU8^cGi@Na&*(BrEjAZS>6z*KvFsACmHk zka8-tQ$%@ylrNBSmavETVYvj4@<@yIM-UUr&tl5!1V4(KA?3)3QDmHvcJfUm)c=m{ z>F!E>u3^E5(n;BY(~))i=VeSfG=n6rk10~rKIIMjCb6c{1K3A$S4cZY+Sf?)IDL_9@a(=FoV667NZ^4=JEJE=Y;t`1E83maU*i2wiq literal 0 HcmV?d00001 diff --git a/out/test/classes/test/pivotal/pal/tracker/TimeEntryControllerTest.class b/out/test/classes/test/pivotal/pal/tracker/TimeEntryControllerTest.class new file mode 100644 index 0000000000000000000000000000000000000000..0fa10b6b91d8f5cc28d961d741e1c4fa8f672f41 GIT binary patch literal 5066 zcmb7I`CA*;6+I)C5%PEw*o_^MIHryT*p}lhO>vxJmO4a;0y3D|2_3`$gM~&Ojcn62 zU6ZcvzOU)N*KN|6C9V^9Pt*P9e*IVa_1u|38Y~1de2m___wK#t-0eN_#{XV<6~Hn4 zJAw|3t9VxgIwl(MR-{!-Mo0;N$qjiok#TLFiM0?V9ZVr0nKv7~?Yfw7~z2EPPgM zeon>bReV7~qhrn)Jz2+|A2M=Q-gGQ`UcrHcX~lEqWy{es@f`mgTTfp!?0BktcZY&V z+M)(4lQHaSWV?#HDO-Qi%$m+A1vRnO5e2nf)}*0eYr@PL$->-(VW;$o3}>PVE3Icn zbla47f3nt@G4l$JBpf5}tPi+kG{wC%T$eYTvpItAxtca|j%j7{D!!;-*H9rVP8l)t zCM|Vlv-EXbcZm*#?_pWc8o~{dpC3i8t?_BMx`P_=X z`l~f@a%1eL2xI$-&GDq=^jd{1jk`D!X5JB$v-)K{UU1AzTxKY7?2f4_4UJb1e6`7{ z^a~X?if40^ZWJ4Zm-iB2=5FaWGKK^?GHey>w9#uyayA$1Vs)Aw5-=LxhE4@-)t8it zFKPHPzM|nI9;-sKWK?W@Rl_Mfu3*nb23Z4+HL1^w(APA49p6y#O%30|3o5>?;X4B0 zyZD|ge_uvFQ1L?zKf;ey{6xb~@iP@a*YFEmSMf^?zru?eUc#?sndK;a2vnX@H_lo9)D1)rvud}Q`&O3#P35v#tWnkDu(;TF9=Q_DvnK7vPp*2;Pd{iXY z6jax*JLwt2W}B|jvpies(|)V$-Pi^6-9|}F^!Ly+6&%xFm5u8A?*1ybD%d=2IKwq;C&KrOTSj z>9(AZbluu|Ch~HaJEfhTW*Tm(NUnrWf!a0H)0yh&X0BIm$|V8nJ$P3@*c;4z&!vK% zNm&XW4YFI6&gCBRnHBk{48dt@k~!Bz=dY2ckKes=4?@vI>$=o}n^&5e+ZYm7cAB2N ztY-?wz!Y0btS<;qmg%?JDN_#AV?mVeQdLz^%QB<%0a0Bsuw@sE(7=-nKyn~8-aBwM z$sl`e(LKUDZCjsb`qk@s?|k1EJ74ZrpGfe~ug$pEf#^yUf@|&T)kPV+qrNXn8Y-l@ z04zpn?TRXGQSeyM$JM1-Nt~ey8J8F@y1I|M7LQws5y*A#cedj~%opvaYn&(nO1 z^HbqxUE3m*7u{`LeAc^@JJ8Ls=50U^dilndM&TM)IEh+!t+_24T0+fjgl?d=?I!BR z+FnNOBI<8IE$Q4sDd1k}Yv%L5fbM2r_ZhB>#wXCnkzjbzwU{Vcd<_v-i*Xa-u}Bjd zny@Lld0Ti9TN3R{*eZ6Hu`R?aODGy%!uHz;FC!YlC|6Vw3d%GtpsD}B0(LB6=Sz6g z0`3{*+pZ<-ZePH?qb2Bj`9%%%X(RR$dJFd9&3G6O5oQeM(27a4xpek;;Cwob7(zdi zq|ryI0lb5;D5p5+QXDE$yya6Itb$@>3C+v6&!-nz!2KmU53EI}1`=J5&LPr?6WC#w z%t0K*34ROj#&H5Y5uh_zq;txrbD~IRnA#*iyqVNG1kV-;UZayjlslr_5wLPL3C6(- z&K_YS0r42@W_Exn9%hnnXM*qMrMiXb{V-Eoa(1U@!AIVLt*-kOtl;%#Qbb-UAR|S* z|HWoEtXADE=1)y|Scb=Hz07*B2`x?77u~NiABUF^E6d4cv?^XME}(6VSltG*_yAcS zEF;^^0G)A5V~}Mrjs&s{hmiHM&hp6~r91jDN@|0+-_JUsEiUoa&K1|j)(O}2v@4LK}1C>|P@kEAS8>EaEuXtdX1{x4gXMe2U5WG?R0j z-9l$kO>iw8NX~8_`SD_MR>FIsi1%5)blRnxvD{B2q~R^$5w<(8dfpO3v1T4!T{BVk mR$!V3zzjRE$(A)6fYa{7X<&DdR#mDMZ)Uw(d5U^Nc>4bzwH`$Nv9XVejk~yK z;l70j49hQ7p!6$-e6`kRD73>5l3~qNfqa`x1~Tf2flr>Y8+yX;i%6OM%vsPQ6*D|@ zwTv~Ns*_L)pHJ!4k?=msh#yMd3n#K22099TpWrC7$xu?dD^BA#()U9K+q;M})Lj)` z4U2H>&Sy_fwF-mS!b67L_er4DMD|szh-WJZh*BD6Sa-u{$j1rI%V#v~ZkR-#e67rg zdke`n#^OXcuu)>DUX>Zc^;YFmHhX0v<5&!(!Fc50F{&174(d3t@WjDWG#DEHCm_vY zSaDwjL;h|smY!zVn7b}SVc4p=bAMMyDi}7+K^J3jI#YkMTDzRXuy527aV#S}=3W>{ zE~bjNQisu-#LuF=k)ZaSMU7I$FJla1b3b%@B%-c7PGsQ843|!nFK+wS5{4BOca9U` z_vj|Q_>)DZOtbcH7PHRuQxxf!F;5OO8%ne~wBuwM*`@k9*q1bXjn?JVS;Q)RJ6QtO zuueNVd@9bT#XYh)^6h*_?x>tUN5MTf!_rqQm-A<^4(E8QlxdJ`Lfj@KgL@;Zo#C2D Y85@MZNwEUiEy~E4Y?Ec!p*4ry-!S$OL;wH) literal 0 HcmV?d00001 diff --git a/out/test/classes/test/pivotal/pal/trackerapi/TimeEntryApiTest.class b/out/test/classes/test/pivotal/pal/trackerapi/TimeEntryApiTest.class new file mode 100644 index 0000000000000000000000000000000000000000..c7489a483a104045093ef2829000af998d931b09 GIT binary patch literal 6313 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 literal 0 HcmV?d00001 diff --git a/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java new file mode 100644 index 000000000..2c066d7ce --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java @@ -0,0 +1,40 @@ +package io.pivotal.pal.tracker; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class InMemoryTimeEntryRepository implements TimeEntryRepository { + + private HashMap timeEntries = new HashMap<>(); + + @Override + public TimeEntry create(TimeEntry timeEntry) { + timeEntry.setId(timeEntries.size() + 1); + timeEntries.put(timeEntry.getId(), timeEntry); + + return timeEntry; + } + + @Override + public TimeEntry find(Long id) { + return timeEntries.get(id); + } + + @Override + public List list() { + return new ArrayList<>(timeEntries.values()); + } + + @Override + public TimeEntry update(Long id, TimeEntry timeEntry) { + timeEntries.replace(id, timeEntry); + timeEntry.setId(id); + return timeEntry; + } + + @Override + public void delete(Long id) { + timeEntries.remove(id); + } +} diff --git a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java index 6b3c2c877..1e7676004 100644 --- a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -2,10 +2,16 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; @SpringBootApplication public class PalTrackerApplication { public static void main(String[] args) { SpringApplication.run(PalTrackerApplication.class, args); } + + @Bean + public TimeEntryRepository getTimeEntryRepository() { + return new InMemoryTimeEntryRepository(); + } } diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntry.java b/src/main/java/io/pivotal/pal/tracker/TimeEntry.java new file mode 100644 index 000000000..e25ccd7a6 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntry.java @@ -0,0 +1,87 @@ +package io.pivotal.pal.tracker; + +public class TimeEntry { + private long id; + private long projectId; + private long userId; + private String date; + private int hours; + + public TimeEntry(){ + + } + + public TimeEntry(long id, long projectId, long userId, String date, int hours) { + this.id = id; + this.projectId = projectId; + this.userId = userId; + this.date = date; + this.hours = hours; + } + + public TimeEntry(long projectId, long userId, String date, int hours) { + this.projectId = projectId; + this.userId = userId; + this.date = date; + this.hours = hours; + } + + public long getId() { + return id; + } + + public long getProjectId() { + return projectId; + } + + public long getUserId() { + return userId; + } + + public String getDate() { + return date; + } + + public int getHours() { + return hours; + } + + public void setId(long id) { + this.id = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + TimeEntry timeEntry = (TimeEntry) o; + + if (id != timeEntry.id) return false; + if (projectId != timeEntry.projectId) return false; + if (userId != timeEntry.userId) return false; + if (hours != timeEntry.hours) return false; + return date != null ? date.equals(timeEntry.date) : timeEntry.date == null; + } + + @Override + public int hashCode() { + int result = (int) (id ^ (id >>> 32)); + result = 31 * result + (int) (projectId ^ (projectId >>> 32)); + result = 31 * result + (int) (userId ^ (userId >>> 32)); + result = 31 * result + (date != null ? date.hashCode() : 0); + result = 31 * result + hours; + return result; + } + + @Override + public String toString() { + return "TimeEntry{" + + "id=" + id + + ", projectId=" + projectId + + ", userId=" + userId + + ", date='" + date + '\'' + + ", hours=" + hours + + '}'; + } +} diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java new file mode 100644 index 000000000..6262bcafd --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java @@ -0,0 +1,58 @@ +package io.pivotal.pal.tracker; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.sql.Time; +import java.util.List; + +@RestController +@RequestMapping("/time-entries") +public class TimeEntryController { + + private TimeEntryRepository timeEntriesRepo; + + public TimeEntryController(TimeEntryRepository timeEntriesRepo) { + this.timeEntriesRepo = timeEntriesRepo; + } + + @PostMapping + public ResponseEntity create(@RequestBody TimeEntry timeEntry) { + TimeEntry createdTimeEntry = timeEntriesRepo.create(timeEntry); + + return new ResponseEntity<>(createdTimeEntry, HttpStatus.CREATED); + } + + @GetMapping("{id}") + public ResponseEntity read(@PathVariable Long id) { + TimeEntry timeEntry = timeEntriesRepo.find(id); + if (timeEntry != null) { + return new ResponseEntity<>(timeEntry, HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + @GetMapping + public ResponseEntity> list() { + return new ResponseEntity<>(timeEntriesRepo.list(), HttpStatus.OK); + } + + @PutMapping("{id}") + public ResponseEntity update(@PathVariable Long id, @RequestBody TimeEntry timeEntry) { + TimeEntry updatedTimeEntry = timeEntriesRepo.update(id, timeEntry); + if (updatedTimeEntry != null) { + return new ResponseEntity<>(updatedTimeEntry, HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + @DeleteMapping("{id}") + public ResponseEntity delete(@PathVariable Long id) { + timeEntriesRepo.delete(id); + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java new file mode 100644 index 000000000..27f39083d --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java @@ -0,0 +1,11 @@ +package io.pivotal.pal.tracker; + +import java.util.List; + +public interface TimeEntryRepository { + TimeEntry create(TimeEntry timeEntry); + TimeEntry find(Long id); + List list(); + TimeEntry update(Long id, TimeEntry timeEntry); + void delete(Long id); +} From 6e7a521cf88256fe06252c0d5513087388c24e00 Mon Sep 17 00:00:00 2001 From: Ruurd Keizer Date: Tue, 15 Aug 2017 16:24:31 +0200 Subject: [PATCH 09/18] Concourse pipeline --- ci/build.yml | 5 +- ci/pipeline.yml | 66 ++++++++++++++++++++++--- ci/variables.example.yml | 61 +++++++++++++++++++++-- manifest.yml => manifest-production.yml | 2 +- manifest-review.yml | 5 ++ 5 files changed, 124 insertions(+), 15 deletions(-) rename manifest.yml => manifest-production.yml (72%) create mode 100644 manifest-review.yml diff --git a/ci/build.yml b/ci/build.yml index e9b625396..46263869c 100644 --- a/ci/build.yml +++ b/ci/build.yml @@ -8,6 +8,7 @@ image_resource: inputs: - name: pal-tracker + - name: version outputs: - name: build-output @@ -18,5 +19,5 @@ run: - -exc - | cd pal-tracker - ./gradlew build - cp build/libs/pal-tracker.jar ../build-output \ No newline at end of file + ./gradlew -P version=$(cat ../version/number) build + cp build/libs/pal-tracker-*.jar ../build-output \ No newline at end of file diff --git a/ci/pipeline.yml b/ci/pipeline.yml index c72874c4a..0130bb9c5 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -7,25 +7,77 @@ resources: branch: master private_key: {{github-private-key}} -- name: deploy +- name: pal-tracker-artifacts + type: s3 + source: + bucket: {{aws-bucket}} + regexp: releases/pal-tracker-(.*).jar + access_key_id: {{aws-access-key-id}} + secret_access_key: {{aws-secret-access-key}} + +- name: version + type: semver + source: + bucket: {{aws-bucket}} + key: pal-tracker/version + access_key_id: {{aws-access-key-id}} + secret_access_key: {{aws-secret-access-key}} + +- name: review-deployment + type: cf + source: + api: {{cf-api-url}} + username: {{cf-username}} + password: {{cf-password}} + organization: {{cf-org}} + space: review + +- name: production-deployment type: cf source: api: {{cf-api-url}} username: {{cf-username}} password: {{cf-password}} organization: {{cf-org}} - space: sandbox + space: production jobs: -- name: build-and-deploy +- name: build plan: - get: pal-tracker trigger: true + - get: version + params: {bump: patch} - task: build and test file: pal-tracker/ci/build.yml - - put: deploy + - put: pal-tracker-artifacts + params: + file: build-output/pal-tracker-*.jar + - put: version + params: + file: version/number + +- name: deploy-review + plan: + - get: pal-tracker + - get: pal-tracker-artifacts + trigger: true + passed: [build] + - put: review-deployment + params: + manifest: pal-tracker/manifest-review.yml + path: pal-tracker-artifacts/pal-tracker-*.jar + environment_variables: + WELCOME_MESSAGE: "Hello from the review environment" + +- name: deploy-production + plan: + - get: pal-tracker + - get: pal-tracker-artifacts + passed: [deploy-review] + - put: production-deployment params: - manifest: pal-tracker/manifest.yml - path: build-output/pal-tracker.jar + manifest: pal-tracker/manifest-production.yml + path: pal-tracker-artifacts/pal-tracker-*.jar environment_variables: - WELCOME_MESSAGE: "Hello from Concourse" \ No newline at end of file + WELCOME_MESSAGE: "Hello from the production environment" \ No newline at end of file diff --git a/ci/variables.example.yml b/ci/variables.example.yml index 801cec689..d151262c6 100644 --- a/ci/variables.example.yml +++ b/ci/variables.example.yml @@ -1,9 +1,60 @@ -cf-api-url: CF_API_URL -cf-username: CF_USERNAME -cf-password: CF_PASSWORD -cf-org: CF_ORG +cf-api-url: https://api.sys.longs.pal.pivotal.io +cf-username: rkeizer@itq.nl +cf-password: 230adab9 +cf-org: rkeizer-pal +aws-access-key-id: AKIAI3ZZPZHLEYLSDKVA +aws-secret-access-key: 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 +aws-bucket: student-artifacts-columbia github-repository: git@github.com:ruurdk/pal-tracker.git github-private-key: | -----BEGIN RSA PRIVATE KEY----- - REPLACE WITH YOUR PRIVATE KEY HERE + MIIJKAIBAAKCAgEAsd4QjG3I3asm1WJpVXwu6Gk4r6NRZd70Bq36cI8A2osN/ERj + 7dz4dbkeg2Y/Yk2zLS8dNMuJ7H9bqTZscudqW9qrg4cqzByJVmifWY13FGbrQbyo + 4bL58g6ntJQ0No+LED9JO+9nmXnByZwybPcSq1ZMWCpzmy/rfEflScqGF4Qcpjeg + qtpPSuSegQx1cwvzarnOphe/G3u5IvHVjsvWpA4s2D4oEpWwBg01pPJxmznv3K6j + EUGKHI7w4lvTykS5690e8q9Q4eyYGjzrH5f9fks4ZsHoQ1NY8ftWu7VKiacJ9iib + au5oAlpxWcM7ZygNEgyHha8sQ/s7w4oOgQVzCukBtWotgowecZMGyR+fgxZuXx0Y + sUM9w1PKsjsGF2wGgNjLfzbB5haiNANYGWY6PP2TkC5BoXTxRFxFf2kL+xDOckJK + bBmnTNnQVmScZ2Lx3d/y+Q09kpFghv3y54FM0wijw/1ri+trGjpJMHbGFlkPR7+E + VwRjG4WLOM0RWWixCj5bwa61b/VyNV52IblxCO5QlrWFMy0QwzXhnlS06XazAP5T + Z1cP4HMpzbAOVXaB024w8IFvM7qTojRfRbvOI/VJZskFY2W64GqVJGJBKpBlNbhO + o1UwaORlJ9NdK7d72lpK/zFOwYwBbiIXP2tWyyb1dum8/B354HnksE+8XRkCAwEA + AQKCAgEAqXcDW1iqPZD5EeuOonUMGEFQUf+P6ThwVgDjEOUfEhnUvFrIvvN/AmLb + EKSVlWqkYVN3Rzr58Qsy4NjKoPudmWCRc2KQQyKZM2vBD0i6gQTB9sLyQQAOJjMS + U/m0eZtwCo3Rj7o8gbzBBv68CJerAPXbDVF9DgmtGp9BdXnxImysAy4S9VXEoH2M + CzY8nN1xEWlw20zatyu9K+8v4rqRiYM79NfbxEE8Ebf6OWsJFLUxtJfrI+DG3wjN + t014T0oFvhTSjLS+olKSZMVP260rR24Rkc4EJkk+AGfQrk0/dEFjO1ZbJZl6HWvg + fX6GlXVSZrTrVlKdITQ4L8jUotb3UIICWpdHPtSucH+cfRQ02rsMSfNZH1sm94nh + b4H+Yb5ATCYEgRXk8KJ3atyx0T0l9BLWQX7VQATWQrXjU96QmOXd5nIXJbSqQpx0 + Qj23zLlEEf0FlQTV+ufR/vxOxF6wyIvmGgUiiGjO8FC/3qXCOPmvxzxV8kr7Dw23 + d06/USO5xrFsxFt4zuAPC7FPoW3M56K5v3jAXZh9/u/XLNOLYWVvHDnOzvZBtI2F + NSwbZyCyCPLTOHeC48pvg7nMFq4dhtxEnwAZ+0kVtd6bptikPJmYB/bEQsGsgEzK + ijcY8+TjF3yEk6vlfdgm6i/2cel5EHO+wNItpBEgPjw3U84r6QECggEBANt69Gdy + rv8IarVjvQ87MP+5f4uWpgPtFMnrPW6FE3fgX7jzUVuq/2Ghhf5/oNBxvKkIxZtd + /d1DVeQ44MuDy7sk+Nr/t6tFOkljyeIDigvuoxTzkS4Ub/IZQDAf9iZI6PTrxaAW + wRxf3P7E2Loo8NIPUyNPhyk3CRLVPBFBqrCNqRubFpN48+VonuiU4RHW1N+kKLTB + 1u2ohoDwZOsPgoEWWvScD8BpfOEtUJyJuQsz/Ma/GUq+Vi48FI6mC/zRHkDdv9Mw + JbXLykXNyrFbcjMDat9pygCfkHpK2//F6yCF7d4i2QO+9D99LyYxP3pZJiTmFEzn + pL8BXPFHlkrAMuECggEBAM92jpeLH7odIqAzTWWPCqQCLBom+pSJsAosoGMMLKA8 + mm4Yk0sNfnIXsfEp1luCzoLziqgMuGg9GdD2iNqLWR1PY/52wj2FXwtXv1QAfsav + i9F9Uh/R2qqKijNVbTTl7limurlUfZ5952L3FH8PFlEzzHa6h93nw5RuIPN12LsB + HXdxFjGril0Q7n7aakAN/oO4HAMAmDaFNH7U7JpOfk4KOR32EScGLuX3BDooLKPP + 2Mpqd8lBdE2e9chkjqQnY/QTxTbgoxOmp368ZdJB9UPTLiFqZmtgXVCNkGPgeuk3 + Zay2Gi4IVLxHsIa60PxjdxP2oLvw+CTteKvUSjcrKTkCggEAFH4DadE8Y5V+mRcJ + 5O0q4wWH0hmrwHwXT88F+la0faHWbidRtlRd6dix8RQriKrF6aZnBN2ewVTzJQ7m + djoFAKEwFwp/NJ+PFKEbjV21Ou36Dg+7w2inFxyicJvbWLHABnYpCox1VVfiA2uP + rLn72jESGx7myMNZtodwR1AxRrDDVE+DqRtG8ml1RNP5u7eqQoNKRa4/igsNG+bW + FVhqX/EdZu9XTSjMPx4DA8qcG16sQEZBb0gcNc7oQHVicyV3RKkvquE49CNl7GSN + g0e9fuPXy6pN8Qb072Yj6lqpXG0Ey5PHXWEr3u2qRE+e1KaUOCe/R3rtV+QhsGl1 + r98IIQKCAQAJbVe5Zyb/7AcAxJQFw2Q2Y6rhXVgRm9gV/kUjexAkD9zsl81JoclX + IeCCFuaYre74YFyymjFe+zfLGQjK9X4NKOhgTcExznetKkWVaZhZKuztjgZHT9/h + 0/3Hq2AVAUW9XYYqmb5Dj3EOPDAAPg1Dj1kBJCS8XqWyfvacWYSJqtlN7iOWCjd1 + VymfNgmR85DXJ2yObl58S71A7if6MwrmPOyvgdjxrwQ+iuT0R5MPtoghWokq6gCA + 7nI4sukQaIHQO33AqqB+aho+Vg1CaqDcfiIgfpc81donqOcgwriTFGNYX8X8xYAk + NhW9aYvJjDODArf3ElIUS34qQLFGpH1xAoIBACgQEBwgf6meI/zHyD+JR+VPMpW/ + zeqzHyUNIQBsynU4ZZ20cMIvS89dB+F7sXLcS2CuFw6KISZ161NEC7h2s2gMOi02 + xlkOAdjAytJ6TejgV9aHJW/qk9gIPg982Dk2WrVT23c093/Gq2FQt0s702uwJGky + 44ZwNmOZfN8Vxo0nt84I53FNOLqEjzTOfcmAXvQkZhOZXj9fykFygeuNXMY+nHJU + aAfiQO189bXdKGUygdGHy6C2r1UKGxzpR4sYTcHPAgtzZQQ//faqLpIdbhcQx6LO + hfO5ivvkr+pK0f/4oc6fJGIbQr4DlfDwQyTi3rjjTcCjCSlGfAzEQKzF5N0= -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/manifest.yml b/manifest-production.yml similarity index 72% rename from manifest.yml rename to manifest-production.yml index 69ad95367..9c0fc5531 100644 --- a/manifest.yml +++ b/manifest-production.yml @@ -2,4 +2,4 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar - random-route: true \ No newline at end of file + host: rk-pal-tracker diff --git a/manifest-review.yml b/manifest-review.yml new file mode 100644 index 000000000..a93cbaba7 --- /dev/null +++ b/manifest-review.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: pal-tracker + path: build/libs/pal-tracker.jar + host: rk-pal-tracker-review From 12723111904e69a45c7f8bb81abf62b9a3207fea Mon Sep 17 00:00:00 2001 From: Ruurd Keizer Date: Tue, 15 Aug 2017 16:31:40 +0200 Subject: [PATCH 10/18] example --- ci/variables.example.yml | 60 ---------------------------------------- 1 file changed, 60 deletions(-) delete mode 100644 ci/variables.example.yml diff --git a/ci/variables.example.yml b/ci/variables.example.yml deleted file mode 100644 index d151262c6..000000000 --- a/ci/variables.example.yml +++ /dev/null @@ -1,60 +0,0 @@ -cf-api-url: https://api.sys.longs.pal.pivotal.io -cf-username: rkeizer@itq.nl -cf-password: 230adab9 -cf-org: rkeizer-pal -aws-access-key-id: AKIAI3ZZPZHLEYLSDKVA -aws-secret-access-key: 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 -aws-bucket: student-artifacts-columbia -github-repository: git@github.com:ruurdk/pal-tracker.git -github-private-key: | - -----BEGIN RSA PRIVATE KEY----- - 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 - -----END RSA PRIVATE KEY----- \ No newline at end of file From 0aa5514f453c3b76b4df48def039eb51bb505304 Mon Sep 17 00:00:00 2001 From: Ruurd Keizer Date: Wed, 16 Aug 2017 08:57:56 +0200 Subject: [PATCH 11/18] migrations --- databases/tracker/create_databases.sql | 9 +++++++++ databases/tracker/migrations/V1__initial_schema.sql | 11 +++++++++++ 2 files changed, 20 insertions(+) create mode 100644 databases/tracker/create_databases.sql create mode 100644 databases/tracker/migrations/V1__initial_schema.sql diff --git a/databases/tracker/create_databases.sql b/databases/tracker/create_databases.sql new file mode 100644 index 000000000..d98f14cef --- /dev/null +++ b/databases/tracker/create_databases.sql @@ -0,0 +1,9 @@ +DROP DATABASE IF EXISTS tracker_dev; +DROP DATABASE IF EXISTS tracker_test; + +CREATE USER 'tracker'@'localhost' + IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON *.* TO 'tracker' @'localhost'; + +CREATE DATABASE tracker_dev; +CREATE DATABASE tracker_test; \ No newline at end of file diff --git a/databases/tracker/migrations/V1__initial_schema.sql b/databases/tracker/migrations/V1__initial_schema.sql new file mode 100644 index 000000000..ec0951a56 --- /dev/null +++ b/databases/tracker/migrations/V1__initial_schema.sql @@ -0,0 +1,11 @@ +CREATE TABLE time_entries ( + id BIGINT(20) NOT NULL AUTO_INCREMENT, + project_id BIGINT(20), + user_id BIGINT(20), + date VARCHAR(255), + hours INT, + + PRIMARY KEY (id) +) + ENGINE = innodb + DEFAULT CHARSET = utf8; \ No newline at end of file From d615219722fe11a9a5fc575df64b2bf271eb09aa Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Wed, 26 Jul 2017 11:45:04 -0600 Subject: [PATCH 12/18] Add tests for JDBC lab --- .../tracker/JdbcTimeEntryRepositoryTest.java | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java diff --git a/src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java b/src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java new file mode 100644 index 000000000..44d2a2500 --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java @@ -0,0 +1,153 @@ +package test.pivotal.pal.tracker; + + +import io.pivotal.pal.tracker.JdbcTimeEntryRepository; +import io.pivotal.pal.tracker.TimeEntry; +import io.pivotal.pal.tracker.TimeEntryRepository; +import org.junit.Before; +import org.junit.Test; +import org.mariadb.jdbc.MariaDbDataSource; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JdbcTimeEntryRepositoryTest { + private TimeEntryRepository subject; + private JdbcTemplate jdbcTemplate; + + @Before + public void setUp() throws Exception { + DataSource dataSource = new MariaDbDataSource(System.getenv("SPRING_DATASOURCE_URL")); + subject = new JdbcTimeEntryRepository(dataSource); + + jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.execute("DELETE FROM time_entries"); + } + + @Test + public void createInsertsATimeEntryRecord() throws Exception { + TimeEntry newTimeEntry = new TimeEntry(123, 321, "today", 8); + TimeEntry entry = subject.create(newTimeEntry); + + Map foundEntry = jdbcTemplate.queryForMap("Select * from time_entries where id = ?", entry.getId()); + + assertThat(foundEntry.get("id")).isEqualTo(entry.getId()); + assertThat(foundEntry.get("project_id")).isEqualTo(123L); + assertThat(foundEntry.get("user_id")).isEqualTo(321L); + assertThat(foundEntry.get("date")).isEqualTo("today"); + assertThat(foundEntry.get("hours")).isEqualTo(8); + } + + @Test + public void createReturnsTheCreatedTimeEntry() throws Exception { + TimeEntry newTimeEntry = new TimeEntry(123, 321, "today", 8); + TimeEntry entry = subject.create(newTimeEntry); + + assertThat(entry.getId()).isNotNull(); + assertThat(entry.getProjectId()).isEqualTo(123); + assertThat(entry.getUserId()).isEqualTo(321); + assertThat(entry.getDate()).isEqualTo("today"); + assertThat(entry.getHours()).isEqualTo(8); + } + + @Test + public void findFindsATimeEntry() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (999, 123, 321, 'today', 8)" + ); + + TimeEntry timeEntry = subject.find(999L); + + assertThat(timeEntry.getId()).isEqualTo(999L); + assertThat(timeEntry.getProjectId()).isEqualTo(123L); + assertThat(timeEntry.getUserId()).isEqualTo(321L); + assertThat(timeEntry.getDate()).isEqualTo("today"); + assertThat(timeEntry.getHours()).isEqualTo(8); + } + + @Test + public void findReturnsNullWhenNotFound() throws Exception { + TimeEntry timeEntry = subject.find(999L); + + assertThat(timeEntry).isNull(); + } + + @Test + public void listFindsAllTimeEntries() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (999, 123, 321, 'today', 8), (888, 456, 678, 'yesterday', 9)" + ); + + List timeEntries = subject.list(); + assertThat(timeEntries.size()).isEqualTo(2); + + TimeEntry timeEntry = timeEntries.get(0); + assertThat(timeEntry.getId()).isEqualTo(888L); + assertThat(timeEntry.getProjectId()).isEqualTo(456L); + assertThat(timeEntry.getUserId()).isEqualTo(678L); + assertThat(timeEntry.getDate()).isEqualTo("yesterday"); + assertThat(timeEntry.getHours()).isEqualTo(9); + + timeEntry = timeEntries.get(1); + assertThat(timeEntry.getId()).isEqualTo(999L); + assertThat(timeEntry.getProjectId()).isEqualTo(123L); + assertThat(timeEntry.getUserId()).isEqualTo(321L); + assertThat(timeEntry.getDate()).isEqualTo("today"); + assertThat(timeEntry.getHours()).isEqualTo(8); + } + + @Test + public void updateReturnsTheUpdatedRecord() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (1000, 123, 321, 'today', 8)"); + + TimeEntry timeEntryUpdates = new TimeEntry(456, 987, "tomorrow", 10); + + TimeEntry updatedTimeEntry = subject.update(1000L, timeEntryUpdates); + + assertThat(updatedTimeEntry.getId()).isEqualTo(1000L); + assertThat(updatedTimeEntry.getProjectId()).isEqualTo(456L); + assertThat(updatedTimeEntry.getUserId()).isEqualTo(987L); + assertThat(updatedTimeEntry.getDate()).isEqualTo("tomorrow"); + assertThat(updatedTimeEntry.getHours()).isEqualTo(10); + } + + @Test + public void updateUpdatesTheRecord() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (1000, 123, 321, 'today', 8)"); + + TimeEntry updatedTimeEntry = new TimeEntry(456, 322, "tomorrow", 10); + + TimeEntry timeEntry = subject.update(1000L, updatedTimeEntry); + + Map foundEntry = jdbcTemplate.queryForMap("Select * from time_entries where id = ?", timeEntry.getId()); + + assertThat(foundEntry.get("id")).isEqualTo(timeEntry.getId()); + assertThat(foundEntry.get("project_id")).isEqualTo(456L); + assertThat(foundEntry.get("user_id")).isEqualTo(322L); + assertThat(foundEntry.get("date")).isEqualTo("tomorrow"); + assertThat(foundEntry.get("hours")).isEqualTo(10); + } + + @Test + public void deleteRemovesTheRecord() throws Exception { + jdbcTemplate.execute( + "INSERT INTO time_entries (id, project_id, user_id, date, hours) " + + "VALUES (999, 123, 321, 'today', 8)" + ); + + subject.delete(999L); + + Map foundEntry = jdbcTemplate.queryForMap("Select count(*) count from time_entries where id = ?", 999); + assertThat(foundEntry.get("count")).isEqualTo(0L); + } +} From 8e430c0e5e24eab00755f0106bd6b61da2ed8a15 Mon Sep 17 00:00:00 2001 From: Ruurd Keizer Date: Wed, 16 Aug 2017 14:18:08 +0200 Subject: [PATCH 13/18] jdbc --- build.gradle | 26 +++++- ci/build.yml | 16 +++- gradle/wrapper/gradle-wrapper.properties | 4 +- manifest-review.yml | 2 + .../pal/tracker/JdbcTimeEntryRepository.java | 83 +++++++++++++++++++ .../pal/tracker/PalTrackerApplication.java | 6 +- .../pal/trackerapi/TimeEntryApiTest.java | 52 +++++++----- 7 files changed, 162 insertions(+), 27 deletions(-) create mode 100644 src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java diff --git a/build.gradle b/build.gradle index e10110928..362984b5c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,9 @@ +import org.flywaydb.gradle.task.FlywayMigrateTask + plugins { id "java" - id 'org.springframework.boot' version '1.5.6.RELEASE' + id "org.springframework.boot" version "1.5.4.RELEASE" + id "org.flywaydb.flyway" version "4.2.0" } repositories { @@ -9,13 +12,32 @@ repositories { dependencies { compile("org.springframework.boot:spring-boot-starter-web") + compile("org.springframework.boot:spring-boot-starter-jdbc") + + compile("org.mariadb.jdbc:mariadb-java-client:2.0.2") + testCompile("org.springframework.boot:spring-boot-starter-test") } +def developmentDbUrl = "jdbc:mariadb://localhost:3306/tracker_dev?user=tracker" bootRun.environment([ "WELCOME_MESSAGE": "hello", + "SPRING_DATASOURCE_URL": developmentDbUrl, ]) +def testDbUrl = "jdbc:mariadb://localhost:3306/tracker_test?user=tracker" test.environment([ "WELCOME_MESSAGE": "Hello from test", -]) \ No newline at end of file + "SPRING_DATASOURCE_URL": testDbUrl, +]) + +flyway { + url = developmentDbUrl + user = "tracker" + password = "" + locations = ["filesystem:databases/tracker/migrations"] +} + +task testMigrate(type: FlywayMigrateTask) { + url = testDbUrl +} \ No newline at end of file diff --git a/ci/build.yml b/ci/build.yml index 46263869c..353c6fcda 100644 --- a/ci/build.yml +++ b/ci/build.yml @@ -18,6 +18,20 @@ run: args: - -exc - | + + function stop_mysql { + service mysql stop + } + trap stop_mysql EXIT + + export DEBIAN_FRONTEND="noninteractive" + + apt-get update && \ + apt-get -y install mariadb-server + service mysql start + cd pal-tracker - ./gradlew -P version=$(cat ../version/number) build + + mysql -uroot < databases/tracker/create_databases.sql + ./gradlew -P version=$(cat ../version/number) testMigrate build cp build/libs/pal-tracker-*.jar ../build-output \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d44a7ae67..5de050932 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Aug 14 14:55:22 CEST 2017 +#Wed Aug 16 14:10:21 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip diff --git a/manifest-review.yml b/manifest-review.yml index a93cbaba7..7a5c1e991 100644 --- a/manifest-review.yml +++ b/manifest-review.yml @@ -3,3 +3,5 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar host: rk-pal-tracker-review + services: + - tracker-database diff --git a/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java new file mode 100644 index 000000000..25d7d2582 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java @@ -0,0 +1,83 @@ +package io.pivotal.pal.tracker; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; + +import javax.sql.DataSource; +import java.sql.PreparedStatement; +import java.util.List; + +public class JdbcTimeEntryRepository implements TimeEntryRepository { + + private final JdbcTemplate jdbcTemplate; + + public JdbcTimeEntryRepository(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public TimeEntry create(TimeEntry timeEntry) { + GeneratedKeyHolder generatedKeyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO time_entries (project_id, user_id, date, hours) " + + "VALUES (?, ?, ?, ?)" + ); + + statement.setLong(1, timeEntry.getProjectId()); + statement.setLong(2, timeEntry.getUserId()); + statement.setString(3, timeEntry.getDate()); + statement.setInt(4, timeEntry.getHours()); + + return statement; + }, generatedKeyHolder); + + return find(generatedKeyHolder.getKey().longValue()); + } + + @Override + public TimeEntry find(Long id) { + return jdbcTemplate.query( + "SELECT id, project_id, user_id, date, hours FROM time_entries WHERE id = ?", + new Object[]{id}, + extractor); + } + + @Override + public List list() { + return jdbcTemplate.query("SELECT id, project_id, user_id, date, hours FROM time_entries", mapper); + } + + @Override + public TimeEntry update(Long id, TimeEntry timeEntry) { + jdbcTemplate.update("UPDATE time_entries " + + "SET project_id = ?, user_id = ?, date = ?, hours = ? " + + "WHERE id = ?", + timeEntry.getProjectId(), + timeEntry.getUserId(), + timeEntry.getDate(), + timeEntry.getHours(), + id); + + return find(id); + } + + @Override + public void delete(Long id) { + jdbcTemplate.update("DELETE FROM time_entries WHERE id = ?", id); + } + + private final RowMapper mapper = (rs, rowNum) -> new TimeEntry( + rs.getLong("id"), + rs.getLong("project_id"), + rs.getLong("user_id"), + rs.getString("date"), + rs.getInt("hours") + ); + + private final ResultSetExtractor extractor = + (rs) -> rs.next() ? mapper.mapRow(rs, 1) : null; +} diff --git a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java index 1e7676004..7f1ebfaef 100644 --- a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -4,6 +4,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import javax.sql.DataSource; + @SpringBootApplication public class PalTrackerApplication { public static void main(String[] args) { @@ -11,7 +13,7 @@ public static void main(String[] args) { } @Bean - public TimeEntryRepository getTimeEntryRepository() { - return new InMemoryTimeEntryRepository(); + public TimeEntryRepository getTimeEntryRepository(DataSource ds) { + return new JdbcTimeEntryRepository(ds); } } diff --git a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java index 36dd6c687..182e906f0 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java @@ -1,24 +1,28 @@ package test.pivotal.pal.trackerapi; -import com.jayway.jsonpath.DocumentContext; -import io.pivotal.pal.tracker.PalTrackerApplication; -import io.pivotal.pal.tracker.TimeEntry; -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.web.client.TestRestTemplate; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.context.junit4.SpringRunner; - -import java.util.Collection; - -import static com.jayway.jsonpath.JsonPath.parse; -import static org.assertj.core.api.Assertions.assertThat; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + import com.jayway.jsonpath.DocumentContext; + import io.pivotal.pal.tracker.PalTrackerApplication; + import io.pivotal.pal.tracker.TimeEntry; + import org.junit.Before; + import org.junit.Test; + import org.junit.runner.RunWith; + import org.mariadb.jdbc.MariaDbDataSource; + import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.boot.test.context.SpringBootTest; + import org.springframework.boot.test.web.client.TestRestTemplate; + import org.springframework.http.HttpEntity; + import org.springframework.http.HttpMethod; + import org.springframework.http.HttpStatus; + import org.springframework.http.ResponseEntity; + import org.springframework.jdbc.core.JdbcTemplate; + import org.springframework.test.context.junit4.SpringRunner; + + import javax.sql.DataSource; + import java.util.Collection; + + import static com.jayway.jsonpath.JsonPath.parse; + import static org.assertj.core.api.Assertions.assertThat; + import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; @RunWith(SpringRunner.class) @SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) @@ -29,6 +33,14 @@ public class TimeEntryApiTest { private TimeEntry timeEntry = new TimeEntry(123, 456, "today", 8); + @Before + public void setUp() throws Exception { + DataSource dataSource = new MariaDbDataSource(System.getenv("SPRING_DATASOURCE_URL")); + + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.execute("TRUNCATE time_entries"); + } + @Test public void testCreate() throws Exception { ResponseEntity createResponse = restTemplate.postForEntity("/time-entries", timeEntry, String.class); @@ -116,4 +128,4 @@ public void testDelete() throws Exception { private Long createTimeEntry() { return restTemplate.postForObject("/time-entries", timeEntry, TimeEntry.class).getId(); } -} +} \ No newline at end of file From 1f7f4c0150d4983c4635d09a43b46b6ccc03256f Mon Sep 17 00:00:00 2001 From: Ruurd Keizer Date: Wed, 16 Aug 2017 14:19:31 +0200 Subject: [PATCH 14/18] jdbc prod --- manifest-production.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manifest-production.yml b/manifest-production.yml index 9c0fc5531..cf93073a9 100644 --- a/manifest-production.yml +++ b/manifest-production.yml @@ -3,3 +3,5 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar host: rk-pal-tracker + services: + - tracker-database From 6de18a82fce4ad573df0bb2b85841878a6a9b8e3 Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Wed, 26 Jul 2017 12:28:32 -0600 Subject: [PATCH 15/18] Add tests for Actuator lab --- .../pivotal/pal/trackerapi/HealthApiTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java diff --git a/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java new file mode 100644 index 000000000..b3eef23cc --- /dev/null +++ b/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java @@ -0,0 +1,38 @@ +package test.pivotal.pal.trackerapi; + +import com.jayway.jsonpath.DocumentContext; +import io.pivotal.pal.tracker.PalTrackerApplication; +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.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; + +import static com.jayway.jsonpath.JsonPath.parse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) +public class HealthApiTest { + + @Autowired + private TestRestTemplate restTemplate; + + @Test + public void healthTest() { + ResponseEntity response = this.restTemplate.getForEntity("/health", String.class); + + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + + DocumentContext healthJson = parse(response.getBody()); + + assertThat(healthJson.read("$.status", String.class)).isEqualTo("UP"); + assertThat(healthJson.read("$.db.status", String.class)).isEqualTo("UP"); + assertThat(healthJson.read("$.diskSpace.status", String.class)).isEqualTo("UP"); + } +} From 6f7ff485054ba546e90ae3f8e03c6f667476fffb Mon Sep 17 00:00:00 2001 From: Ruurd Keizer Date: Wed, 16 Aug 2017 14:56:45 +0200 Subject: [PATCH 16/18] spring actuator --- build.gradle | 6 ++- .../pal/tracker/TimeEntryController.java | 20 +++++++- .../pal/tracker/TimeEntryHealthIndicator.java | 29 ++++++++++++ .../pal/tracker/TimeEntryControllerTest.java | 34 ++++++++------ .../pal/trackerapi/TimeEntryApiTest.java | 46 +++++++++---------- 5 files changed, 95 insertions(+), 40 deletions(-) create mode 100644 src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java diff --git a/build.gradle b/build.gradle index 362984b5c..7037d4289 100644 --- a/build.gradle +++ b/build.gradle @@ -15,20 +15,24 @@ dependencies { compile("org.springframework.boot:spring-boot-starter-jdbc") compile("org.mariadb.jdbc:mariadb-java-client:2.0.2") - + compile("org.springframework.boot:spring-boot-starter-actuator") testCompile("org.springframework.boot:spring-boot-starter-test") } def developmentDbUrl = "jdbc:mariadb://localhost:3306/tracker_dev?user=tracker" + bootRun.environment([ "WELCOME_MESSAGE": "hello", "SPRING_DATASOURCE_URL": developmentDbUrl, + "MANAGEMENT_SECURITY_ENABLED": false, ]) def testDbUrl = "jdbc:mariadb://localhost:3306/tracker_test?user=tracker" + test.environment([ "WELCOME_MESSAGE": "Hello from test", "SPRING_DATASOURCE_URL": testDbUrl, + "MANAGEMENT_SECURITY_ENABLED": false, ]) flyway { diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java index 6262bcafd..19fef3721 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java @@ -1,25 +1,36 @@ package io.pivotal.pal.tracker; +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.GaugeService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.sql.Time; import java.util.List; @RestController @RequestMapping("/time-entries") public class TimeEntryController { + private final CounterService counter; + private final GaugeService gauge; private TimeEntryRepository timeEntriesRepo; - public TimeEntryController(TimeEntryRepository timeEntriesRepo) { + public TimeEntryController( + TimeEntryRepository timeEntriesRepo, + CounterService counter, + GaugeService gauge + ) { this.timeEntriesRepo = timeEntriesRepo; + this.counter = counter; + this.gauge = gauge; } @PostMapping public ResponseEntity create(@RequestBody TimeEntry timeEntry) { TimeEntry createdTimeEntry = timeEntriesRepo.create(timeEntry); + counter.increment("TimeEntry.created"); + gauge.submit("timeEntries.count", timeEntriesRepo.list().size()); return new ResponseEntity<>(createdTimeEntry, HttpStatus.CREATED); } @@ -28,6 +39,7 @@ public ResponseEntity create(@RequestBody TimeEntry timeEntry) { public ResponseEntity read(@PathVariable Long id) { TimeEntry timeEntry = timeEntriesRepo.find(id); if (timeEntry != null) { + counter.increment("TimeEntry.read"); return new ResponseEntity<>(timeEntry, HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); @@ -36,6 +48,7 @@ public ResponseEntity read(@PathVariable Long id) { @GetMapping public ResponseEntity> list() { + counter.increment("TimeEntry.listed"); return new ResponseEntity<>(timeEntriesRepo.list(), HttpStatus.OK); } @@ -43,6 +56,7 @@ public ResponseEntity> list() { public ResponseEntity update(@PathVariable Long id, @RequestBody TimeEntry timeEntry) { TimeEntry updatedTimeEntry = timeEntriesRepo.update(id, timeEntry); if (updatedTimeEntry != null) { + counter.increment("TimeEntry.updated"); return new ResponseEntity<>(updatedTimeEntry, HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); @@ -52,6 +66,8 @@ public ResponseEntity update(@PathVariable Long id, @RequestBody Time @DeleteMapping("{id}") public ResponseEntity delete(@PathVariable Long id) { timeEntriesRepo.delete(id); + counter.increment("TimeEntry.deleted"); + gauge.submit("timeEntries.count", timeEntriesRepo.list().size()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java new file mode 100644 index 000000000..bcc25ea63 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java @@ -0,0 +1,29 @@ +package io.pivotal.pal.tracker; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Component; + +@Component +public class TimeEntryHealthIndicator implements HealthIndicator { + + private static final int MAX_TIME_ENTRIES = 5; + private final TimeEntryRepository timeEntryRepo; + + public TimeEntryHealthIndicator(TimeEntryRepository timeEntryRepo) { + this.timeEntryRepo = timeEntryRepo; + } + + @Override + public Health health() { + Health.Builder builder = new Health.Builder(); + + if(timeEntryRepo.list().size() < MAX_TIME_ENTRIES) { + builder.up(); + } else { + builder.down(); + } + + return builder.build(); + } +} \ No newline at end of file diff --git a/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java index a6f1100b5..09ae732ce 100644 --- a/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java +++ b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java @@ -5,6 +5,8 @@ import io.pivotal.pal.tracker.TimeEntry; import org.junit.Before; import org.junit.Test; +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.GaugeService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -21,19 +23,23 @@ public class TimeEntryControllerTest { private TimeEntryRepository timeEntryRepository; private TimeEntryController controller; + private CounterService counterService; + private GaugeService gaugeService; @Before public void setUp() throws Exception { timeEntryRepository = mock(TimeEntryRepository.class); - controller = new TimeEntryController(timeEntryRepository); + counterService = mock(CounterService.class); + gaugeService = mock(GaugeService.class); + controller = new TimeEntryController(timeEntryRepository, counterService, gaugeService); } @Test public void testCreate() throws Exception { TimeEntry expected = new TimeEntry(1L, 123, 456, "today", 8); doReturn(expected) - .when(timeEntryRepository) - .create(any(TimeEntry.class)); + .when(timeEntryRepository) + .create(any(TimeEntry.class)); ResponseEntity response = controller.create(new TimeEntry(123, 456, "today", 8)); @@ -45,8 +51,8 @@ public void testCreate() throws Exception { public void testRead() throws Exception { TimeEntry expected = new TimeEntry(1L, 123, 456, "today", 8); doReturn(expected) - .when(timeEntryRepository) - .find(1L); + .when(timeEntryRepository) + .find(1L); ResponseEntity response = controller.read(1L); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -56,8 +62,8 @@ public void testRead() throws Exception { @Test public void testRead_NotFound() throws Exception { doReturn(null) - .when(timeEntryRepository) - .find(1L); + .when(timeEntryRepository) + .find(1L); ResponseEntity response = controller.read(1L); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); @@ -66,8 +72,8 @@ public void testRead_NotFound() throws Exception { @Test public void testList() throws Exception { List expected = asList( - new TimeEntry(1, 123, 456, "today", 8), - new TimeEntry(2, 789, 321, "yesterday", 4) + new TimeEntry(1, 123, 456, "today", 8), + new TimeEntry(2, 789, 321, "yesterday", 4) ); doReturn(expected).when(timeEntryRepository).list(); @@ -80,8 +86,8 @@ public void testList() throws Exception { public void testUpdate() throws Exception { TimeEntry expected = new TimeEntry(1, 987, 654, "yesterday", 4); doReturn(expected) - .when(timeEntryRepository) - .update(eq(1L), any(TimeEntry.class)); + .when(timeEntryRepository) + .update(eq(1L), any(TimeEntry.class)); ResponseEntity response = controller.update(1L, expected); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); @@ -91,8 +97,8 @@ public void testUpdate() throws Exception { @Test public void testUpdate_NotFound() throws Exception { doReturn(null) - .when(timeEntryRepository) - .update(eq(1L), any(TimeEntry.class)); + .when(timeEntryRepository) + .update(eq(1L), any(TimeEntry.class)); ResponseEntity response = controller.update(1L, new TimeEntry()); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); @@ -104,4 +110,4 @@ public void testDelete() throws Exception { verify(timeEntryRepository).delete(1L); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); } -} +} \ No newline at end of file diff --git a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java index 182e906f0..bcf6e36ad 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java @@ -1,28 +1,28 @@ package test.pivotal.pal.trackerapi; - import com.jayway.jsonpath.DocumentContext; - import io.pivotal.pal.tracker.PalTrackerApplication; - import io.pivotal.pal.tracker.TimeEntry; - import org.junit.Before; - import org.junit.Test; - import org.junit.runner.RunWith; - import org.mariadb.jdbc.MariaDbDataSource; - import org.springframework.beans.factory.annotation.Autowired; - import org.springframework.boot.test.context.SpringBootTest; - import org.springframework.boot.test.web.client.TestRestTemplate; - import org.springframework.http.HttpEntity; - import org.springframework.http.HttpMethod; - import org.springframework.http.HttpStatus; - import org.springframework.http.ResponseEntity; - import org.springframework.jdbc.core.JdbcTemplate; - import org.springframework.test.context.junit4.SpringRunner; - - import javax.sql.DataSource; - import java.util.Collection; - - import static com.jayway.jsonpath.JsonPath.parse; - import static org.assertj.core.api.Assertions.assertThat; - import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import com.jayway.jsonpath.DocumentContext; +import io.pivotal.pal.tracker.PalTrackerApplication; +import io.pivotal.pal.tracker.TimeEntry; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mariadb.jdbc.MariaDbDataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.junit4.SpringRunner; + +import javax.sql.DataSource; +import java.util.Collection; + +import static com.jayway.jsonpath.JsonPath.parse; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; @RunWith(SpringRunner.class) @SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) From a7565d6f081f5ad585fa92a82b6490be58e528db Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Wed, 26 Jul 2017 12:50:47 -0600 Subject: [PATCH 17/18] Add tests for Security lab --- .../pal/trackerapi/SecurityApiTest.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/test/java/test/pivotal/pal/trackerapi/SecurityApiTest.java diff --git a/src/test/java/test/pivotal/pal/trackerapi/SecurityApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/SecurityApiTest.java new file mode 100644 index 000000000..72099994b --- /dev/null +++ b/src/test/java/test/pivotal/pal/trackerapi/SecurityApiTest.java @@ -0,0 +1,52 @@ +package test.pivotal.pal.trackerapi; + +import io.pivotal.pal.tracker.PalTrackerApplication; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) +public class SecurityApiTest { + + @LocalServerPort + private String port; + private TestRestTemplate authorizedRestTemplate; + + @Autowired + private TestRestTemplate unAuthorizedRestTemplate; + + @Before + public void setUp() throws Exception { + RestTemplateBuilder builder = new RestTemplateBuilder() + .rootUri("http://localhost:" + port) + .basicAuthorization("user", "password"); + + authorizedRestTemplate = new TestRestTemplate(builder); + } + + @Test + public void unauthorizedTest() { + ResponseEntity response = this.unAuthorizedRestTemplate.getForEntity("/", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + public void authorizedTest() { + ResponseEntity response = this.authorizedRestTemplate.getForEntity("/", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } +} From aeff1973f8b6e2b5707d33715afbc429c321d748 Mon Sep 17 00:00:00 2001 From: Ruurd Keizer Date: Wed, 16 Aug 2017 15:31:07 +0200 Subject: [PATCH 18/18] Security --- build.gradle | 1 + manifest-production.yml | 2 ++ manifest-review.yml | 3 ++ .../pal/tracker/JdbcTimeEntryRepository.class | Bin 0 -> 5710 bytes .../pal/tracker/PalTrackerApplication.class | Bin 993 -> 1085 bytes .../pal/tracker/SecurityConfiguration.class | Bin 0 -> 4862 bytes .../io/pivotal/pal/tracker/TimeEntry.class | Bin 2127 -> 2419 bytes .../pal/tracker/TimeEntryController.class | Bin 2613 -> 4289 bytes .../tracker/TimeEntryHealthIndicator.class | Bin 0 -> 1394 bytes .../tracker/JdbcTimeEntryRepositoryTest.class | Bin 0 -> 6033 bytes .../pal/tracker/TimeEntryControllerTest.class | Bin 5066 -> 5569 bytes .../pal/trackerapi/HealthApiTest.class | Bin 0 -> 3804 bytes .../pal/trackerapi/SecurityApiTest.class | Bin 0 -> 3404 bytes .../pal/trackerapi/TimeEntryApiTest.class | Bin 6313 -> 7649 bytes .../pal/trackerapi/WelcomeApiTest.class | Bin 1704 -> 2726 bytes .../pal/tracker/SecurityConfiguration.java | 32 ++++++++++++++++++ .../pivotal/pal/trackerapi/HealthApiTest.java | 18 ++++++++-- .../pal/trackerapi/TimeEntryApiTest.java | 11 +++++- .../pal/trackerapi/WelcomeApiTest.java | 15 +++++++- 19 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 out/production/classes/io/pivotal/pal/tracker/JdbcTimeEntryRepository.class create mode 100644 out/production/classes/io/pivotal/pal/tracker/SecurityConfiguration.class create mode 100644 out/production/classes/io/pivotal/pal/tracker/TimeEntryHealthIndicator.class create mode 100644 out/test/classes/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.class create mode 100644 out/test/classes/test/pivotal/pal/trackerapi/HealthApiTest.class create mode 100644 out/test/classes/test/pivotal/pal/trackerapi/SecurityApiTest.class create mode 100644 src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java diff --git a/build.gradle b/build.gradle index 7037d4289..aa6efbaaf 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ dependencies { compile("org.mariadb.jdbc:mariadb-java-client:2.0.2") compile("org.springframework.boot:spring-boot-starter-actuator") + compile("org.springframework.boot:spring-boot-starter-security") testCompile("org.springframework.boot:spring-boot-starter-test") } diff --git a/manifest-production.yml b/manifest-production.yml index cf93073a9..394da3f77 100644 --- a/manifest-production.yml +++ b/manifest-production.yml @@ -5,3 +5,5 @@ applications: host: rk-pal-tracker services: - tracker-database + env: + SECURITY_FORCE_HTTPS: true diff --git a/manifest-review.yml b/manifest-review.yml index 7a5c1e991..af57acbd8 100644 --- a/manifest-review.yml +++ b/manifest-review.yml @@ -5,3 +5,6 @@ applications: host: rk-pal-tracker-review services: - tracker-database + env: + SECURITY_FORCE_HTTPS: true + diff --git a/out/production/classes/io/pivotal/pal/tracker/JdbcTimeEntryRepository.class b/out/production/classes/io/pivotal/pal/tracker/JdbcTimeEntryRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..62a9f09ebdf023fd765e9274b03bea3b4013a588 GIT binary patch literal 5710 zcmbtYiGS1P8GhtAk>dym4o5>XFl`Esoncy_(1o<6!GME9XdJ>)+A+lz!NjqQEOT_- zx_j&Ht-HGW?v8G2w-N}9uDiFcyYKtH&%a>5J)b1YN^Rrpw;x3M^yxjH_xN7#aW9@9#0&7k zAgNvyz>8(PM7(~fjF$!Qau0Foi$L)_mfINue(yF2h6ytf1I!~4bM1LE>Q86T4IpakVa zb}BQj%@*^jsY&Qh>Be-tR5Wsh>0^dEtDV-3nYf^dXLLi04|}KwB(%+{#iC|N=xf3^ zrk@_MNXgWm&P^9ovuse`$YS|+rgD0`m^-DLYCc}%pJ}L>8O?}~=VrA;!8Fbe+$>~k zXT%Frr%g9BldYA?c{8npQ+e)$QhG+sPpC#tJUg3y^LVZ#VRIvBtYB5( z8Pke-DMw(=(#v4h)hl6DeR~+)jGgO=|_spb^>q z+Sz@2J}Z)KTjLm&%Eh8?nDMvRg@swVe9$1v^z8rZwEv!7O zl$y_#OhzcuXTK|(xqLjuJ!-tMW^=LGQZ6z?4E#imvCUyylsj!%Ew2d;T{Rj+CMtP# zb}Fm(7PQm7H%Qpw5Q&@}tJKXRaWj~sD{O2QvP)Q!Hd)2&3|2<74Kg(oXEIvR%;|-a zj1SBB$UETat_O+R#t4up~%HmYL(Do#wrpB^+8rqE|f`(hCJGBTOX{qNc5q zK4@q~)zGqZ&D3VKg2;nl#wC@ovP$cbz~U%XI3)B`Y3jU@(hNx?Jl zQReM!x^9-(8jB;Ed0fwymbKwyop^}1k9Xo(3O<2PD%gUpop^Yej7K_gNWrJ@X$9M{ zLqc@%r-D#8px}BiYqm6GYh0(Dc$7w+Q!t0~3J&6ef_Yq&aY?~va9PG<3Oqf*vNIx+6?Oc?5V zXFmuJ?UmyuNftP!;ETAd;7j82Wqd`!SMfC&PY4*lE&zT~!gad|BQ;+cAsOFL@J)P6 z#qLsuG=36UV;r#IDq#L^>QH6wd!X89!F=6Z}-h&lLO|zfkZ?{7S*E@f!)78pN+b z*)ZE1Ft~?Z^(>=J>~W;39)*)pH+41$lsO8Xr+>ex+TyWrtJC0I~+S^mp zb3lxdzqf}l&R0~))25!*6Ylut9m67Z^*D{pQw5fqKS=0sMyuT%9VBEMcFG2@vPCwr zt#Yh*$FVIQBR$RXitSdw@<$1)?Y3UiJ|*1Q&~`V%O|7jFJ0xZ$-C(nyjx_PKA#Y>8 zO22fcpn*OfU~SWy$sHcAl{v$Tn>{(w5>H37_jt(UIq*!V`7#dM_t z7}!2a*ietgwFV2KHWFFRl8X+=_>+P^U6U7slzIq)VqSQiHvK?Um`Uo~>T|2(uY2ymRK{PctDg#&T~P*FN0HXO6J$^G*CEMiL_`uRBR` z9DY&(z0vcKq8G*b99p7W`NgU=dI@clvGb76A@GPLagf(lXyvt&G)XMM?O2WFxR#o3 zw%%Fta0_nb6D4C8Zlg8{L)h(T&C}i%zT0pK?UUyb9Em=PK|vqRtJLzOXIH)wcl&?4h0j)?zRAky8@GvZHx~zaa_xx`oEj za{pz7CR<`@KDExF>v628Dr+MP5c4?I?JDWUex&%GeHjNRP(ZnZh6s+Omgtqdw#;GG zQ{`>3(a^Qdz1O*4H;)Zsf8#tNKAcBiHNZB;NvaygGCH=JDYA|U zvXLngXAV}YW3y!#m9cV<;@LD&Smo#a2yJ2ub!@tid!kOZ$3oG0#C#a(_eYrn^XT`Z zzm>~wd)db2nt43E1^qIYSI=V;msQi&5GN)*i+?SD6kW%EVMLh(+gP^M&~LZGOhTqa z23eZZj!n>TjOMro(x7Ql_}DuB#(R)df5AWSS4uy@XOToR{L5n&1#5K@21?fIZafEf F{}<)aNcsQ( literal 0 HcmV?d00001 diff --git a/out/production/classes/io/pivotal/pal/tracker/PalTrackerApplication.class b/out/production/classes/io/pivotal/pal/tracker/PalTrackerApplication.class index 3c1388584cfe8dd41f4ea774e74310cde4ab7242..d907f8c994a4c421447735756548012258ed3713 100644 GIT binary patch delta 295 zcmaFJzL!Jk)W2Q(7#J8#7}U5J*chbQ8DzK^WEtex8RWSbSQr%885Acfot1FW@X1On zORUf@F3iz)Ni0bW&Mz%WPPLx+QD2%VrI?XH46FQPS;lvqN<0k83@VHadXw)mipzMV zBqfJr=BB#ll@wJ5r55BDXO`p_Rq`;fGpJ5>V_GA_$RLTsGR?5bpP0-Txh88e+p~f+ z@J`NUmQS!{U}j(fdVzB{152d#b_UiBKn@cF7m#FQU<8ta4BS9c45EmEhk+N!11e__ aVGsq%ivignb>cvpi9rHPO2S2@7#IL~>^v_3 delta 211 zcmdnX@sM5V)W2Q(7#J8#7?ije*cc?)8Kk%vq#0z`8DzN_SU`NaiArZ#^fWYmCJQo& zPgYx&g>$V&DRjY+!MI25umm b2ciZ@^8$GyP+3tR&BP!ECdJ{R5)2Fg5>_Ht diff --git a/out/production/classes/io/pivotal/pal/tracker/SecurityConfiguration.class b/out/production/classes/io/pivotal/pal/tracker/SecurityConfiguration.class new file mode 100644 index 0000000000000000000000000000000000000000..868f62a203bab5f5531f90b04a938500fd6ca0a7 GIT binary patch literal 4862 zcmd5=TXz#x6#h<|cET`iX$s|zpcUFu884_{5okl8#e_oA3-@}GoF+rlnJ_bHDWE7S zUhw*+Yw3^hCAwToSD$?FH@RHylgu<}sYnt)yH;oBoSd`wx4-?JJv;g1uU~!xa1h_9 z*oDJEyrE(cM}l}$#d;i75yCMQ$1$WJtRje$a^{qRkswA@oW@%UVnK|lXvbMOKBpiq zmn2k-V?sVns+hug6&G+Zh)XIi<4O=$RZQb;dEp9@3Q`QMM-0<&k1{m(_DwPb!d6-^ zbVLnPoGHvDg+0!bIhqMYtrX8qa@&w&Z!+L!4Ts@C)UfotF>kp%r|0S4wRvh**m^>w z3bx@chAnf($P{eu8kRZ8urB3Ikl|Qw)Uq?WleY~sGh_2Pv0&M=y5nut%S&}`nq*$? z*B3-mPZo?^TG)<0;<|a?&0r~lEX^>SetcFL)EUc83F(TW4MnfnIVUC!#~`^3EhmroJG3T-q~NBb&+wFM*^AZLrqHYf z!xm{`Sxs04O2kP+Dsr2j_7&7aK_wX@gx8Mj{!6zC%#eIfgo_M5!l~(YD zhO4-*;Y)nQFi=ZHWVljC2)-`LYOT^~h038c&%45A=&Iy)xhk#9_@d*AIeHyrge%N> zhP}NHmbtzMr)UQWkLT!IZ>rugmb@lX?qJ_Vs(bcIX)^5eL@MPe95=?@R94t=kue{wqr>Rt2sJ?b` z?|cdm73FwW8c|ua77j||-3FPeIH(&1!+LJ|>L&8` z^ zV{Do9-9aDqTuxA5aYDG<$T?pBzJl6YHB$RH-BX*hV7OU{kebRWpq{80zJDAAA#=dlP}9$7 zjt%^dZ3Ce|5iQ@LbAJ)7MJP)M7O`%MMrz6JDp^ puFA8A=pLc}enJzZNKeqpc0_T8?#F37i%yCv3Wvti_#B^s{S7dlTu1-_ literal 0 HcmV?d00001 diff --git a/out/production/classes/io/pivotal/pal/tracker/TimeEntry.class b/out/production/classes/io/pivotal/pal/tracker/TimeEntry.class index f099c9219686c42810807121ed0e6f343d7f8214..98f7546a5b42afc010d9cc878657b5836877f197 100644 GIT binary patch delta 784 zcmZuvOHUI~7(MrnGj}=;*fg5KXhSVZu$C5~il}`kP}_YPz;!`!>mD}v-A(MhqbKPq(XhWyV zvc~GMB7@newHlMn6_>&A+}8RNpK^Y4!S`05)}_vK#xia*#5Q(gVSzg~#*ndb5hDza ze5N__1&^3j+_h1~J^6|c^;L17ERwN?x-6^m4{TgUYV$h}GiGn@Y9CmT07INMmj)*W z=!y5x-Vos!AT-*^I8G9`|1{tf1}S1lqMP_Yr+Bcdco-?NhzI19Ci3rU{T(ItU!CZx zPGdx=JH*E84soih_$)^MigS=#`f+^}I>zKzyg31^L!+hR@+VnM7%+Zls|QLrGCw@*Bv+ zAK*VQA#Pk4*KS<7ac%qqE;ZIO$mTsW@0|0TbLLm{HFfai@5fKT7|#RJl)5q8bu#H> z%E`2o8TWe6XEtDt`%dP49t51GWLR+Vp_8(qVt8bzy4#{*$;HP`p7<;)^wx$#RTSO3 z?bdeV#o@sZ`(8DoS%qoFd(FAlMnmD{hRZgq`_A;{hO680eq4AN?`#%UUbVM&HcPgs zrxH(DQDhE(XHq_^Ay*j6qbPwJt5LQhhWyg{jS%yZ}tWwam#raEH%KWpXSvZDQIb_b@9L7_OXd;(I^ ILb93o2Z<9+r~m)} diff --git a/out/production/classes/io/pivotal/pal/tracker/TimeEntryController.class b/out/production/classes/io/pivotal/pal/tracker/TimeEntryController.class index 10440409564195418e09e192e7d577b1370a1f66..007dd034b8795f7c5856ff5e8be270df355c50f3 100644 GIT binary patch literal 4289 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 G1zrJi1Hdo< diff --git a/out/production/classes/io/pivotal/pal/tracker/TimeEntryHealthIndicator.class b/out/production/classes/io/pivotal/pal/tracker/TimeEntryHealthIndicator.class new file mode 100644 index 0000000000000000000000000000000000000000..d57d41d2118e841add7e1e9049ea34dce348a5ad GIT binary patch literal 1394 zcmb7ETTc@~6#iyg*j|>4MNvRh5TTUH3L+pNicN`43MSALeInC#gn{kuW_K2XFa8%_ zeAPrHXyOm>HyPvEQoIlo(uX~B&e?OmbDjDA^UF5?GkB6f5~FeG7}GJHKm?;nWH6CL z7I$?_h8i;@_d+tIV_L_3hVHeM7u%)6T7EmfUfL++HyK!gp(E$mzT&njb1M)LiqJ+% zCF>$@EAMDSxDG?MC>_(42ae*F>C&e>Ufma-S^C?~m&);u78%-?q%GAlgPCr{alFhB z%{eu}&{dSSSP$wI;gxvBB2B93RJm2=o($!tGOBi^&oFgilxKolYPVq5WR+8rVUD{E z6~lCTyy$p4rtf;v-g)Kmx;S*aeY4^?%H&lQa3#!BWAhY-y!A>Tt(x$NdwRwh1=|*0 z&f>l=D5?1tQAe6Ugx;~KsJ(04ZInqvW=L#0fmao)GVI0R@3x<-YjTetaE88(zz!!^ zmcFEBt=KjVPDvUxL*~C}`AT@gQAe&Yb57lLY+(W_0I%W;b zVP3~W0}G(?%!`)kTvY~^@W{Y&NFHN=Ryi!ww79)vKCkSFs$!UHExF&Dzz|7?dpFsd zKlSmVBEp+VcA$StK2Gqw`-48T;}z4Vm(y>!}4U!$Qj|de(fx zkJg++o1v|-u^6IfBMAlr8v49L`B15L~~P0Z|9L|z^vTFiWeb`rik zCunDgy+v~3Gh$os5Ti4`MI`Z_up)#WMrb42gBa!4Ptq{O8YApK(X-9yF^u3kZc@}+ kzd75-+EbT^x6Nw;a*S}H>YvPrhx(ieRR&SrKt zZ2$oQf#L%n%xOK$;r%tZ9UlnegZNMw zAI7l|?g-(|5bg@$?l3-rd&HxA!}usZCf0sjj6NYopA;XT5>G!J#%FL$Og}58_l0pk zJ{Q90T|X*b7W+RStUnmS7sTX?HTV)95+7gol#X~_KjRJp_lNM65FYjvUJT(8Z~Tfd z@>Op|tC&3+!eb#!260?LuuvR4sHbfOv4mm9^TxPor?T-p|Jzn7eMqJhfZyCAaAuBbiA2zK+aiJ4Wo0cBm?bGJ! zMZ1igVPB}ABHB2hpt8-((8w7HBd7NiM+bE)nHtPe5=ofpRCXX`8Dj1VEA0`ZpkQml z)(iG2U6#Aeb~{OZZeCT;?f$%ia3*D^`plx0=1%pAgQ@YoM(4flk4StWmI?_1Uav*U*^*HJ%MQtM#Eua0wmW{3_9| zh1nH2*A(+i4!sPjU(V^G6SG=x-wEddU8!}J_;J@eB3Fgfc+2ZEdR7dZVI7GJ{UZb&E;+U^$N!EbqI#x2xxp1MMxzmcBjx zy=@%_`g;>WOsjYtCxoT1DOf*83z)+ZvFZsG33MxHI{o@r(Stn-RbP(TC@ojuZ#WVP>f`E|;;#n2X;d?=RU&Rmbd=NiW z@gw}0^_RCy(LN6tOw(YI6)s1WB6;ZE2%8JL#!rO3pNgd~1o1N!KgTar{1Q(KBQL1< zl^DH^k+N$`3qhS;uRHt6?8m@zp-bOR1~EDS3aJI4|mJ<8EVboGoH^IF%v~=2eb-*0CrK?Vx@{PZvd=uZ-^AJ=`t2Mg3Ob_;x9P@(Rak*!qKl=;YsPK`OUeW$%-k>`jHj|iea{fNjqdgn zBOdb@s2MBj)BH@wldc`#F}zsr>j~%$y?Rw}X0G+MBzkd{XsxM& zxIP?oZd0ke5pR(KaV=pRa-JqfQnY)K-_4TUQ`;CUh+CfRXgLL1!nxV~0zPRJD`S}6MIyEebXq=YpnSV3t!0cbC(1+qvn92IGsq z1A72x^Cfi!-$MiZ<1I>#csJsA7>c;u@w1n+IKLHsS2Z1n@~~9vL{Qe z1Dq+?%f%IPaRHSyPa`J~*cYjoLgjH(C7MoP(Z1&62tI}o$JJsSehf8DkD?}`PGWI+ zn|g@X$CX%&Rs36xb8#ltV~yOl-ZAZNyBKTn7VP8pZ{>jlJHHj|_uNvJ zBSa10-V<1|uMTI_VQFMpZRj}a67(U|JdNc6bW3;YE1$rMeHF1utenECzDc~HXI*6V zG|miQ3Ts^PB;L4pUF0lJN{iHWgj!j+v?M&+6GoOfT5CN)ZDO5fw4I?< z5jjVqQ!<;lA?hd?_K3qYiL0ROz`Vpctei86bN6z1-W1LuBa>(lv#6Xcb!UxJXc8PW zi$}33tg8)8V!Z?(kl@#HLy?0KY#@k@guDq``2$Ql-M)gO{kT8^*BrM>z`IMpTV3FD zfg34-dx$nfvhJ6Gk24QNGSp!M<2H?rER$xL1w^w!_|KUS8)rUjoRzV0GB>uJ4DZFv z?N)}gji}oB*}?kjWY%8d%k%v%wpDy;$e_-J=wJy^(nYj#77*s{BsP`nY>DLWGSspY ztl&G6C~K#_R=WGvG7|Ied&t{YNzj@|larFk%`TJE*rJfbX>3(kJL{@Mq?;?IaX|nl zv83D0nmTNAlfxl*$Be|;H4E%0XLqs0?t_j;SaMIXhy}|nB*Mskuc_xu$s^FGNEHk?J(5^@7JGe?DF$ z8(*8`=hcnx(g38q%Q*>`R=i9=;vh^kM|OMXR<+AS`Pgz!?%N6S7|-uJ*~9N*kGtE~ aS=wAcan#8pNh#&NBhlx{eVeodQ2Z}dZwf^K literal 0 HcmV?d00001 diff --git a/out/test/classes/test/pivotal/pal/tracker/TimeEntryControllerTest.class b/out/test/classes/test/pivotal/pal/tracker/TimeEntryControllerTest.class index 0fa10b6b91d8f5cc28d961d741e1c4fa8f672f41..627d6ae14bf4099435df224f4ee2f998a88aae20 100644 GIT binary patch literal 5569 zcmbVQiC+}w8GdFt2G+60g%~w98jT8gtce&k5G^7Y$pQ^5SQ8B$mSGuPcGlThl%_{| zB)wzryG`#ay^<<|Hi^B{^uCWY|4M&--kD)`a2Hq;e%blv`@Z*mpXa^4Z+!WGS6=|o zidUmJjI@f0C`_EG$03|mk&)-5imW^>6}c$taZX-r75N$p%72gf1LdVUI8kUQh>och zIFAcaOyeyp5W!=z@K%w3TNH1{JEC|T@2tnW@a`zygC}I}Ntt_Z6z`MQ_u~U1_Cb;O zknH=g{5&;pGvW`bZk;#YQHP6Bd;}jAv5$$^$5nhnRDLpwPvO(^0!ve9COm>$~yM+fRVHEreoRD3U($;E1ol_EJx48 zbNt`2_0(CzjwefZw<(CGENZYa8N;qbwlmmG+1I74Le??tLBpOhQ-*?r3Cm8$^Eunh zrpIl4(l~F~XXB%m<;3-rQ_vkFK500%naamI7l=}smevbtBT%)!a#hDdf>c!7Ze~sA zkbAAG{c!#!b<6xVcj<6-Jh&>Cd|BodlHV3cb0Zz z(G^MVrQw>q;SA;I(yj|BBj=b_Hm~9<3f2wKOLYFQnKx;vBb#Mx++`O8J$9x*#PK7> zxMdqu6cyv0wn2X>s536)jFe-HDcHPhXenH08~GefQ?$9P4<{Ta7a#C9F?~#Dnqs%O zdp#gNpE`jtVCb~GW}&a9bMyY{t<*&O^4Jd%#_D;SBYl?B%^aa|7e~U(JA(3zKBdPC zj+u$e3?*7`n5xjwNaes6nyg5_P;sL;lpAxS*dV;Tmk={|%TXg^NT8#GR>4jg-KHex zhM-y(rrB-*qu~J@SI|;gTRb12X6Fjfs1-vLL*H!#f#m_YS9KX=;OZ-a1ukn(G-{7i-mvL1= z{WPO%*tD+Vw;Fzj-)r~-{wP0xVw+r;K%(-`D*hry{;J_`_`8OG;GgpIFAe_|&HuqO zGI~YBe?|QsNuHZ&Ml2N9W=7AZ;WEV-e%I-C^nd=hV7h*r+A*m^_&^+a3|zMW}o#|lM_0N;#fE; zoz`Sdx8*iK*Ok{ZnwRU8Q{3rkrr|A%k}Kj9#V`i^jR+P8I2Ykn!m2 zPmXl=5A`wTURQQ+FCDh6Pcs{9^}Khr+1m6-sh53T;rmW?#=Qx}=7SPm+p$!cl)&5T zlPhXiB6f?qhf(_f-?gLm1#lunxM&k^!<85hX^zxCZTHv=J*Cu`0Iu7TSy@ zcFdwdaLl1G0%DG7v$*v-v^lJa-~?Azh)`JOwi&GL-8qAGvsnKeZlA%16MTF9EbiDb zgN-MO6gH!dMz^B@O}HJ+*oqe1jh%GXZoG-U9>X4&&L$6>Pp5%2hA>PTJ(N1Zoe`8$ zJn2$=IG}jlre5yO%-Pk6k#(zx$X? zZFGElh*2_N)ZsHa7%+N-+9Z{{nNv$}JRNYnNIyj=w@@E|o>xj={4_V(`LbeBc zS+2(k_#ys{FvGHpm5}vv&+^IcW&DP?F2!_*pL;@ET;i?0Iat#)@XjN-k_#bN>Oukz?hcuDI(8P1Cp-3i24IO}ne9phmp=_$teG-*G|zmX8} z5ubRBPp{~X1}VB2xl};zqT67>uhP_2)u}cKe7lb_(fS#iH?MmMR*9dLtPBKP}eQ@Bjb+ literal 5066 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 literal 0 HcmV?d00001 diff --git a/out/test/classes/test/pivotal/pal/trackerapi/SecurityApiTest.class b/out/test/classes/test/pivotal/pal/trackerapi/SecurityApiTest.class new file mode 100644 index 0000000000000000000000000000000000000000..2c3ed40de077de262792c35e7914ce489abfbb25 GIT binary patch literal 3404 zcmbtX3s)0I6#gbWHpCS%Dn4qpzG?tp7sayNA>j1ZbA~+pq?HMJ3F&??)|>|n#n)^{`oh6Yxp^ZL%6Tufrf`EB=D(* zM=5mReiAt~KJLPCOsKa>^?53Z&ytu{uQM9*DeS|nl9*GE`6NEqu%O|IT3<}z3w)Wv zS9scmuVJQ8fTiAw8aP9{=L*S?&aIfMrcpMXWn)GP+gToFI656T(ys71+qVm4o^>2o zn$mV1pJ6!XiepXGNFN~aqr4T6vZzUjF#wcHM1t+1Nz^O+?!C*)V?t2t& zBG(sdz9gk*kj+qNoRUSZlcwHgN344_XWV*QB_li2@wS2(?vHR+9o4-Zb85y7gvIaK zDkKNDdG?aZa)x6KdPLwjTo{z<^R_IJk&acf9MInDv0%e6)-pUqa?O|ttAI3+B2o~) zF)WRy-O;vDieQzCW|d9f=aep&a<*&4B#SX+mh(~GWIeBJTVXkp&2Gwpac9*Qu2Z4n z&`_ea`KC1n{h>lQRx>glOYluN&o*l%+k2+76L%*cKb@MK&g(db4;gN>Y!18UD#Osb z?i>qz=~k$d2xZ%5IJ~`+RqKFZze?93!zimdExEoN(y@YPI^M?#9c5H>IB==L17B5K z(xYflTPy1D@SG~JCA2WC4a`(L8!J&EBY3B9TE`ijW?%*-^LCs~OOu5aZb=c@w$laEx2@VC6jmq0(-yWBFo-`R&d2#UMwDSMft#UwN8m>X?|F)8%gqm{!Xse{^GPM9 zI;xN>bSn|uP^FL!7ouBCD*Bo)nRMFSZ=^luRyr=EhZj&NC*j5L$sGjD;9 zygoaToy|X(oPPLZ{4T?}w`6rOo>dDp3|+QA{yd;_gF4$_BgLkuwWPO2Dlz$p;aVKC zHys(&_>R-{7HFgUD)m}WXlT|+;|DY*RAUTB>WvNG&eB>t&FMs;=XshLG%^}H`ZvLT z3AbeEsT6?>^wCp~1kjHGnlW6U&28c4SEMXKYlE+mSZMnTZJTI+jgEy)bf&cpB>R6u z7Y!RoZD3DYPw(A8cdq|6_ALx-qUU$)uN&;8dpHRDDf-J0<_kEBn}kyV=#7k20bIad zT*M^;V$e6m=JFQzk7$MA3IR-n04r61CkaV9eIR|1Kpxtl+tk1Y4*!88zvAcyj?F6= zZDbbcrV(Vchq#^~5Own>tLA&?2f$VOdBN~8Nh;q&E7$3*7J?h}g%SKX82e*kycY-K zGO4~oFs}X&7;P9NFbNEW;7-unFhLu!a0|C-^2s8Yg*(V{jN=~el5C2;k75~PcaHuC DoEqeP literal 0 HcmV?d00001 diff --git a/out/test/classes/test/pivotal/pal/trackerapi/TimeEntryApiTest.class b/out/test/classes/test/pivotal/pal/trackerapi/TimeEntryApiTest.class index c7489a483a104045093ef2829000af998d931b09..c15d37051ae38eb3f7a9069461f887dbe8f7d8fb 100644 GIT binary patch literal 7649 zcmb_h33yyp75;CwH<`YsWtz66(1kW_(=?OEQYbXt(qvjvvYAX;Vu3iBd2L>p%$vS> z(_)`-ybyr`g42*@to;o~p*r@N)&f@S_V)ONGy5XRGDZFQxil$v?lA zlfRKz{??Bmey8B~0u@QqvIPR2qmcsDt;r;*UR(=^%&N9e8uMH;>< zVj1#1Ggx7d81%on)3MBBt4Pww@79_$q{ZB{GNs!CNdaXzZNy``C2&b;2n!r8batD& zl-m5Uh#l!S(^iyrE^vZ3uBG+%1BPWLWST1Y6UTNJjWL`Z(!#dvCLvD;PiAL* zu~zOqtFN`Yy{Buax2G?xB7$LoWhdi8!fczd$b^*tvx*_SNMLaM zHph%>bW0lW7lDQOf^wTshVTT=kesnW)8dkmN6eJHLB(J3Hx+|;p^CrbA1eNdf04`6 zDM_uqBtyuK6jSkUJWHxAX_}J@1)j+yU=ie+leFwVDz3y;Y%WsEm~02JVa=hWD-@BOLem~IT*|gk_t}5v1q7gc$9pjiV9Jwh$>Ym!l#I8Rrtj$MX0Kn zEou}oM-_9$8L9|~d5Wl2#eA`V=+-w&;;YH4m%Jyi*o&-tEj=bnLs!H?Rh%j6V@8S9l6lQB)3VHi zifB;98qp}Q!Yf=(7rT^c8FD(9jMG|GG>M=pnnj~3G*P39bLF4&#QCbYK&(^6h2kQC zCMV}k@#G*dFOPPvAuD3NK%19Ur%4Ya4PD7Zt6Y*B~F7|1wU^<5a{yMT@sxVmgf&wf#FEX zh_Pm;U!{H#VF$+F6?=)P+Sqk+StMqQ8&5J#j!_m51@u(I@Ql zy#%Se&K;k2#*`Z?`nVoV%R|KMB$;`;X}J|4kn+%0fLV`2L@fm)PFHN%Q!tW|Ih}*# zYOfG@oH=PP@#*K4CY^Gx0IQj4B^S$5cZG@5QYj{n36?$T9-if~zttIVFGzlOHarsH zd2vS}5N+mIGGfUqMT@)LbUniQ-Fl88rfDd-WE1fbHdtfCm^tDQ_A@ zHe@(r%_&|YR(Nf3!d-)XMxMI`b{9IE*{C32vl;BAGS?9q_dKcRJZ&k|>|7{Lk{(hf z2c(nrCGpOSGV6`h4*Fv27L${At)_;Zr?XM0(99gtGpEl;#Ol}gr}acs&l#+p<`N#B zm@5VB67evk0M~3JrXf8;jc2bvE`hP!%6cwifV}mD+IH#?yblf&} z$=h02Bzjh_epKJf*)qiVSAKW|mZAbHq4P;~S7I;r z@l9ZaTFV`+M<`Rlv4NxT50*cOSyNDtV)oz^Y65d6F}LwPoWY+-1ST;rP#c&($qy>r zec|BRDV%vf>W*U3VBoC7IC~0(3f|P1L0<8ka z5Il@?t0!^Z?xw)`(f#X;#r&Jb{(-O*-w-v+y#?Rp#(<<;N*s3ZIu} z@e$joL6*?*96s3s3bAtN%yw4KDFHS;?;I$CrSmRWI`4vkQh66VmAq>tiGZk8lA2cW zbJaRh(iUdj<@7s6#9qhjyo;DWLaZglxRrZ6hy24Cf-G1`23$zyYsY@{F$Px= z)p6GKt;F;mX71yBdm0l?G}gIT<)ZOaCK?Oz1a4v^%c$i}+)NGS)N>17NgWlr?04tJ zIaezCUzLpuo@B+z>>rsa`wyW{*(qA%)T4g zPi|zs-ozGnb3sOqY4FlZ!u-r{8BAm-rJ4hAwK7Z4S9PG+P#R~myd`E88MB3ttg z+8|r=VwW8Gw>nVf_L+fWD4WEt!`OWf1>Ei|`#oqvzB4b*7+KD;l2WgCTG|pmy@9fC jjN(mrGhc4q0};hraGQ>I;GK9oW&NCgEB=VLZLNDAT|Ip8 literal 6313 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 response = this.restTemplate.getForEntity("/health", String.class); @@ -35,4 +47,4 @@ public void healthTest() { assertThat(healthJson.read("$.db.status", String.class)).isEqualTo("UP"); assertThat(healthJson.read("$.diskSpace.status", String.class)).isEqualTo("UP"); } -} +} \ No newline at end of file diff --git a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java index bcf6e36ad..44382617f 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java @@ -8,8 +8,10 @@ import org.junit.runner.RunWith; import org.mariadb.jdbc.MariaDbDataSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.LocalServerPort; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.http.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -28,13 +30,20 @@ @SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) public class TimeEntryApiTest { - @Autowired + @LocalServerPort + private String port; private TestRestTemplate restTemplate; private TimeEntry timeEntry = new TimeEntry(123, 456, "today", 8); @Before public void setUp() throws Exception { + RestTemplateBuilder builder = new RestTemplateBuilder() + .rootUri("http://localhost:" + port) + .basicAuthorization("user", "password"); + + restTemplate = new TestRestTemplate(builder); + DataSource dataSource = new MariaDbDataSource(System.getenv("SPRING_DATASOURCE_URL")); JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); diff --git a/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java index cc7091ed4..548712094 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java @@ -1,11 +1,14 @@ package test.pivotal.pal.trackerapi; import io.pivotal.pal.tracker.PalTrackerApplication; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.LocalServerPort; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.test.context.junit4.SpringRunner; import static org.assertj.core.api.Assertions.assertThat; @@ -15,9 +18,19 @@ @SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) public class WelcomeApiTest { - @Autowired + @LocalServerPort + private String port; private TestRestTemplate restTemplate; + @Before + public void setUp() throws Exception { + RestTemplateBuilder builder = new RestTemplateBuilder() + .rootUri("http://localhost:" + port) + .basicAuthorization("user", "password"); + + restTemplate = new TestRestTemplate(builder); + } + @Test public void exampleTest() { String body = this.restTemplate.getForObject("/", String.class);