From f1c11281dc67e5c61a5d0c66cfdb6d6aca9c1eba Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Fri, 21 Jul 2017 09:26:37 -0600 Subject: [PATCH 01/25] 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 21e9729ce676dcb327bc5f26e0399d1d67293e54 Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Tue, 30 Jan 2018 12:02:08 -0500 Subject: [PATCH 02/25] Simple Spring Boot app --- settings.gradle | 1 + .../pivotal/pal/tracker/PalTrackerApplication.java | 12 ++++++++++++ .../io/pivotal/pal/tracker/WelcomeController.java | 13 +++++++++++++ 3 files changed, 26 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..80f2a72a5 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -0,0 +1,12 @@ +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); + } +} \ No newline at end of file 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..2d477ae71 --- /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"; + } +} \ No newline at end of file From 893444b56610ef368ae462f7e9d9b6d46c7a19d6 Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Thu, 20 Jul 2017 13:56:50 -0600 Subject: [PATCH 03/25] 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 16a632e2de8984b3922582e3a62054410a3afc61 Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Tue, 30 Jan 2018 15:39:10 -0500 Subject: [PATCH 04/25] Simple Spring Boot app --- manifest.yml | 7 ++++ .../io/pivotal/pal/tracker/EnvController.java | 42 +++++++++++++++++++ .../pal/tracker/WelcomeController.java | 11 ++++- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 manifest.yml create mode 100644 src/main/java/io/pivotal/pal/tracker/EnvController.java diff --git a/manifest.yml b/manifest.yml new file mode 100644 index 000000000..7f06f91ee --- /dev/null +++ b/manifest.yml @@ -0,0 +1,7 @@ +--- +applications: +- name: pal-tracker + path: build/libs/pal-tracker.jar + random-route: true + env: + WELCOME_MESSAGE: Hello from Cloud Foundry \ 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..b2ae15414 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/EnvController.java @@ -0,0 +1,42 @@ +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; + +/** + * Created by e048749 on 1/30/18. + */ + +@RestController +public class EnvController { + + + private final String port; + private final String memoryLimit; + private final String cfInstanceIndex; + private final String cfInstanceAddress; + + + public EnvController(@Value("${PORT:NOT SET}") String port, @Value("${MEMORY_LIMIT:NOT SET}") String memoeryLimit, @Value("${CF_INSTANCE_INDEX:NOT SET}") String cfInstanceIndex, @Value("${CF_INSTANCE_ADDR:NOT SET}") String cfInstanceAddr) { + this.port = port; + this.memoryLimit = memoeryLimit; + this.cfInstanceIndex = cfInstanceIndex; + this.cfInstanceAddress = cfInstanceAddr; + } + + @GetMapping("/env") + public Map getEnv() { + + Map env = new HashMap<>(); + env.put("PORT", port); + env.put("MEMORY_LIMIT", memoryLimit); + env.put("CF_INSTANCE_INDEX", cfInstanceIndex); + env.put("CF_INSTANCE_ADDR", cfInstanceAddress); + 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 2d477ae71..ad589a6ba 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 String message; + + + public WelcomeController(@Value("${WELCOME_MESSAGE}") String message) { + this.message = message; + + } + @GetMapping("/") public String sayHello() { - return "hello"; + return message; } } \ No newline at end of file From 1c356965af6ce66134a9a8dd5c5bc280a69bf4f4 Mon Sep 17 00:00:00 2001 From: Mike Gehard Date: Wed, 3 Jan 2018 16:17:48 -0700 Subject: [PATCH 05/25] Add deployment pipeline --- ci/build.yml | 24 ++++++++++++++ ci/pipeline.yml | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 ci/build.yml create mode 100644 ci/pipeline.yml diff --git a/ci/build.yml b/ci/build.yml new file mode 100644 index 000000000..4ba28db53 --- /dev/null +++ b/ci/build.yml @@ -0,0 +1,24 @@ +platform: linux + +image_resource: + type: docker-image + source: + repository: openjdk + tag: '8-jdk' + +inputs: + - name: pal-tracker + - name: version + +outputs: + - name: build-output + +run: + path: bash + args: + - -exc + - | + cd pal-tracker + chmod +x gradlew + ./gradlew -P version=$(cat ../version/number) build + cp build/libs/pal-tracker-*.jar ../build-output diff --git a/ci/pipeline.yml b/ci/pipeline.yml new file mode 100644 index 000000000..c34c3b2a9 --- /dev/null +++ b/ci/pipeline.yml @@ -0,0 +1,83 @@ +--- +resources: +- name: pal-tracker + type: git + source: + uri: {{github-repository}} + branch: master + private_key: {{github-private-key}} + +- 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: production + +jobs: +- name: build + plan: + - get: pal-tracker + trigger: true + - get: version + params: {bump: patch} + - task: build and test + file: pal-tracker/ci/build.yml + - 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-production.yml + path: pal-tracker-artifacts/pal-tracker-*.jar + environment_variables: + WELCOME_MESSAGE: "Hello from the production environment" \ No newline at end of file From 3a1073fa3c848f8529d5808b9f8b0d6942b6e3e0 Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Tue, 30 Jan 2018 16:23:21 -0500 Subject: [PATCH 06/25] Simple Spring Boot app --- manifest.yml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 manifest.yml diff --git a/manifest.yml b/manifest.yml deleted file mode 100644 index 7f06f91ee..000000000 --- a/manifest.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -applications: -- name: pal-tracker - path: build/libs/pal-tracker.jar - random-route: true - env: - WELCOME_MESSAGE: Hello from Cloud Foundry \ No newline at end of file From 026befc1cb66bc80fc8eff304dc9c3836c802bba Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Tue, 30 Jan 2018 16:34:50 -0500 Subject: [PATCH 07/25] added grable files --- gradlew | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++++ gradlew.bat | 84 +++++++++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100755 gradlew create mode 100644 gradlew.bat diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..cccdd3d51 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/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 From ade24b530a38c995bdde44ae650ce941c37f7d97 Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Tue, 30 Jan 2018 16:59:33 -0500 Subject: [PATCH 08/25] ci/cd --- build.gradle | 21 +++++++++++++++++++++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54333 bytes gradle/wrapper/gradle-wrapper.properties | 5 +++++ manifest-production.yml | 5 +++++ manifest-review.yml | 5 +++++ 5 files changed, 36 insertions(+) create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 manifest-production.yml create mode 100644 manifest-review.yml diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..8df60dd51 --- /dev/null +++ b/build.gradle @@ -0,0 +1,21 @@ +plugins { + id "java" + id "org.springframework.boot" version "1.5.4.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 world", +]) + +test.environment([ + "WELCOME_MESSAGE": "Hello from test", +]) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..99340b4ad18d3c7e764794d300ffd35017036793 GIT binary patch literal 54333 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 D&=%~j literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..2c2bbe5f9 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-bin.zip diff --git a/manifest-production.yml b/manifest-production.yml new file mode 100644 index 000000000..cd3361d30 --- /dev/null +++ b/manifest-production.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: pal-tracker + path: build/libs/pal-tracker.jar + host: ps-pal-tracker-review \ No newline at end of file diff --git a/manifest-review.yml b/manifest-review.yml new file mode 100644 index 000000000..cd3361d30 --- /dev/null +++ b/manifest-review.yml @@ -0,0 +1,5 @@ +--- +applications: +- name: pal-tracker + path: build/libs/pal-tracker.jar + host: ps-pal-tracker-review \ No newline at end of file From 409bd517834812b04361cdb369e1bfdc15cd3ecd Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Tue, 30 Jan 2018 17:03:05 -0500 Subject: [PATCH 09/25] ci/cd --- manifest-production.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest-production.yml b/manifest-production.yml index cd3361d30..39543ce42 100644 --- a/manifest-production.yml +++ b/manifest-production.yml @@ -2,4 +2,4 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar - host: ps-pal-tracker-review \ No newline at end of file + host: ps-pal-tracker-production \ No newline at end of file From 3be13c796b934199871652f7b06aa0d9d9c1b346 Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Tue, 30 Jan 2018 17:10:40 -0500 Subject: [PATCH 10/25] udpated yml --- manifest-production.yml | 2 +- manifest-review.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest-production.yml b/manifest-production.yml index 39543ce42..2091fb54c 100644 --- a/manifest-production.yml +++ b/manifest-production.yml @@ -2,4 +2,4 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar - host: ps-pal-tracker-production \ No newline at end of file + host: ps-pal-tracker-dp-production \ No newline at end of file diff --git a/manifest-review.yml b/manifest-review.yml index cd3361d30..4c6623ff6 100644 --- a/manifest-review.yml +++ b/manifest-review.yml @@ -2,4 +2,4 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar - host: ps-pal-tracker-review \ No newline at end of file + host: ps-pal-tracker-dp-review \ No newline at end of file From eff2d822238fc341b2a94abd034787b0d4f9fa35 Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Thu, 20 Jul 2017 15:04:20 -0600 Subject: [PATCH 11/25] Add tests for MVC lab --- .../InMemoryTimeEntryRepositoryTest.java | 71 ++++++++++ .../pal/tracker/TimeEntryControllerTest.java | 106 +++++++++++++++ .../pal/trackerapi/TimeEntryApiTest.java | 126 ++++++++++++++++++ 3 files changed, 303 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..d0ae6cbe6 --- /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.time.LocalDate; +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(123L, 456L, LocalDate.parse("2017-01-08"), 8)); + + TimeEntry expected = new TimeEntry(1L, 123L, 456L, LocalDate.parse("2017-01-08"), 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(123L, 456L, LocalDate.parse("2017-01-08"), 8)); + + TimeEntry expected = new TimeEntry(1L, 123L, 456L, LocalDate.parse("2017-01-08"), 8); + TimeEntry readEntry = repo.find(1L); + assertThat(readEntry).isEqualTo(expected); + } + + @Test + public void list() throws Exception { + InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + repo.create(new TimeEntry(123L, 456L, LocalDate.parse("2017-01-08"), 8)); + repo.create(new TimeEntry(789L, 654L, LocalDate.parse("2017-01-07"), 4)); + + List expected = asList( + new TimeEntry(1L, 123L, 456L, LocalDate.parse("2017-01-08"), 8), + new TimeEntry(2L, 789L, 654L, LocalDate.parse("2017-01-07"), 4) + ); + assertThat(repo.list()).isEqualTo(expected); + } + + @Test + public void update() throws Exception { + InMemoryTimeEntryRepository repo = new InMemoryTimeEntryRepository(); + TimeEntry created = repo.create(new TimeEntry(123L, 456L, LocalDate.parse("2017-01-08"), 8)); + + TimeEntry updatedEntry = repo.update( + created.getId(), + new TimeEntry(321L, 654L, LocalDate.parse("2017-01-09"), 5)); + + TimeEntry expected = new TimeEntry(created.getId(), 321L, 654L, LocalDate.parse("2017-01-09"), 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(123L, 456L, LocalDate.parse("2017-01-08"), 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..2013c28ec --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java @@ -0,0 +1,106 @@ +package test.pivotal.pal.tracker; + +import io.pivotal.pal.tracker.TimeEntry; +import io.pivotal.pal.tracker.TimeEntryController; +import io.pivotal.pal.tracker.TimeEntryRepository; +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.time.LocalDate; +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.*; + +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, 123L, 456L, LocalDate.parse("2017-01-08"), 8); + doReturn(expected) + .when(timeEntryRepository) + .create(any(TimeEntry.class)); + + ResponseEntity response = controller.create(new TimeEntry(123L, 456L, LocalDate.parse("2017-01-08"), 8)); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getBody()).isEqualTo(expected); + } + + @Test + public void testRead() throws Exception { + TimeEntry expected = new TimeEntry(1L, 123L, 456L, LocalDate.parse("2017-01-08"), 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(1L, 123L, 456L, LocalDate.parse("2017-01-08"), 8), + new TimeEntry(2L, 789L, 321L, LocalDate.parse("2017-01-07"), 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(1L, 987L, 654L, LocalDate.parse("2017-01-07"), 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..91e271b45 --- /dev/null +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java @@ -0,0 +1,126 @@ +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.time.LocalDate; +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(123L, 456L, LocalDate.parse("2017-01-08"), 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("2017-01-08"); + 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("2017-01-08"); + assertThat(readJson.read("$.hours", Long.class)).isEqualTo(8); + } + + @Test + public void testUpdate() throws Exception { + Long id = createTimeEntry(); + TimeEntry updatedTimeEntry = new TimeEntry(2L, 3L, LocalDate.parse("2017-01-09"), 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("2017-01-09"); + 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() { + HttpEntity entity = new HttpEntity<>(timeEntry); + + ResponseEntity response = restTemplate.exchange("/time-entries", HttpMethod.POST, entity, TimeEntry.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + return response.getBody().getId(); + } +} From 4118a31029f5d519ba23c7c2c3bbbb4e692332b0 Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Wed, 31 Jan 2018 11:48:53 -0500 Subject: [PATCH 12/25] Added Rest APIs --- build.gradle | 1 + .../tracker/InMemoryTimeEntryRepository.java | 50 ++++++++ .../pal/tracker/PalTrackerApplication.java | 24 ++++ .../io/pivotal/pal/tracker/TimeEntry.java | 114 ++++++++++++++++++ .../pal/tracker/TimeEntryController.java | 67 ++++++++++ .../pal/tracker/TimeEntryRepository.java | 19 +++ 6 files changed, 275 insertions(+) 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/build.gradle b/build.gradle index 8df60dd51..cdfd2df4c 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,7 @@ repositories { dependencies { compile("org.springframework.boot:spring-boot-starter-web") testCompile("org.springframework.boot:spring-boot-starter-test") + compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.1") } bootRun.environment([ 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..0c2e007a1 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java @@ -0,0 +1,50 @@ +package io.pivotal.pal.tracker; + +import io.pivotal.pal.tracker.TimeEntry; + +import java.sql.Time; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Created by e048749 on 1/31/18. + */ +public class InMemoryTimeEntryRepository implements TimeEntryRepository { + + + Map timeEntryMap = new HashMap(); + + @Override + public TimeEntry find(long l) { + return timeEntryMap.get(l); + } + + @Override + public TimeEntry update(long id, TimeEntry timeEntry) { + timeEntryMap.replace(id, timeEntry); + timeEntry.setId(id); + return timeEntry; + } + + @Override + public TimeEntry delete(long id) { + + TimeEntry timeEntry = timeEntryMap.get(id); + timeEntryMap.remove(id); + return timeEntry; + } + + @Override + public List list() { + return new ArrayList<>(timeEntryMap.values()); + } + + @Override + public TimeEntry create(TimeEntry timeEntry) { + timeEntry.setId(timeEntryMap.size() + 1); + timeEntryMap.put(timeEntry.getId(), timeEntry); + return timeEntry; + } +} diff --git a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java index 80f2a72a5..8a4e7c02a 100644 --- a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -1,12 +1,36 @@ package io.pivotal.pal.tracker; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import java.util.TimeZone; @SpringBootApplication public class PalTrackerApplication { public static void main(String[] args) { + + + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); SpringApplication.run(PalTrackerApplication.class, args); } + + @Bean + TimeEntryRepository timeEntryRepository() { + return new InMemoryTimeEntryRepository(); + } + + @Bean + public ObjectMapper jsonObjectMapper() { + return Jackson2ObjectMapperBuilder.json() + .serializationInclusion(JsonInclude.Include.NON_NULL) // Don’t include null values + .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) //ISODate + .modules(new JavaTimeModule()) + .build(); + } } \ No newline at end of file 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..28c3d6a87 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntry.java @@ -0,0 +1,114 @@ +package io.pivotal.pal.tracker; + +import java.time.LocalDate; + +/** + * Created by e048749 on 1/31/18. + */ +public class TimeEntry { + + private long id; + private long projectId; + private long userId; + private LocalDate date; + private int hours; + + + public TimeEntry(long id, long projectId, long userId, LocalDate date, int hours) { + + this.id = id; + this.projectId = projectId; + this.userId = userId; + this.date=date; + this.hours=hours; + + } + + public TimeEntry(long projectId, long userId, LocalDate date, int hours) { + this.projectId = projectId; + this.userId = userId; + this.date=date; + this.hours=hours; + } + + public TimeEntry() { + + + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public long getProjectId() { + return projectId; + } + + public void setProjectId(long projectId) { + this.projectId = projectId; + } + + public long getUserId() { + return userId; + } + + public void setUserId(long userId) { + this.userId = userId; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public int getHours() { + return hours; + } + + public void setHours(int hours) { + this.hours = hours; + } + + @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..3ad85622f --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java @@ -0,0 +1,67 @@ +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; + +/** + * Created by e048749 on 1/31/18. + */ + +@RestController +@RequestMapping("/time-entries") +public class TimeEntryController { + + TimeEntryRepository timeEntryRepository; + + public TimeEntryController(TimeEntryRepository timeEntryRepository) { + this.timeEntryRepository = timeEntryRepository; + } + + @GetMapping("{id}") + public ResponseEntity read(@PathVariable long id) { + + TimeEntry timeEntry = timeEntryRepository.find(id); + if (timeEntry != null) { + + return new ResponseEntity<>(timeEntry, HttpStatus.OK); + } else { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + } + + @PutMapping("{id}") + public ResponseEntity update(@PathVariable long id, @RequestBody TimeEntry timeEntry) { + TimeEntry updatedTimeEntry = timeEntryRepository.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) { + + timeEntryRepository.delete(id); + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @GetMapping + public ResponseEntity> list() { + return ResponseEntity.ok(timeEntryRepository.list()); + } + + @PostMapping + public ResponseEntity create(@RequestBody TimeEntry timeEntry) { + TimeEntry createdTimeEntry = timeEntryRepository.create(timeEntry); + + return new ResponseEntity<>(createdTimeEntry, HttpStatus.CREATED); + } +} 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..9d4a5abc7 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java @@ -0,0 +1,19 @@ +package io.pivotal.pal.tracker; + +import java.util.List; + +/** + * Created by e048749 on 1/31/18. + */ +public interface TimeEntryRepository { + public TimeEntry find(long l) ; + + public TimeEntry update(long eq, TimeEntry any) ; + + public TimeEntry delete(long l); + + public List list(); + + public TimeEntry create(TimeEntry any) ; +} + From 8843428d8a87ac2d4721d48ecfcf3421c7d31605 Mon Sep 17 00:00:00 2001 From: Mike Gehard Date: Tue, 9 Jan 2018 09:47:21 -0700 Subject: [PATCH 13/25] Add task for migrating databases --- ci/migrateDatabase.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 ci/migrateDatabase.yml diff --git a/ci/migrateDatabase.yml b/ci/migrateDatabase.yml new file mode 100644 index 000000000..b2af8c5ef --- /dev/null +++ b/ci/migrateDatabase.yml @@ -0,0 +1,29 @@ +platform: linux + +image_resource: + type: docker-image + source: + repository: openjdk + tag: '8-jdk' + +inputs: + - name: pal-tracker +# - name: version + +#outputs: +# - name: build-output + +run: + path: bash + args: + - -exc + - | + cd pal-tracker + curl -L "https://packages.cloudfoundry.org/stable?release=linux64-binary&source=github" | tar -zx + chmod +x cf + curl -L "https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/5.0.5/flyway-commandline-5.0.5-linux-x64.tar.gz" | tar -zx + chmod +x flyway-5.0.5/flyway + ./cf login -a $CF_API_URL -u $CF_USERNAME -p $CF_PASSWORD -o $CF_ORG -s $CF_SPACE + ./cf ssh -N -L 63306:$MYSQL_IP:3306 pal-tracker & + sleep 2 + ./flyway-5.0.5/flyway -url="jdbc:mysql://127.0.0.1:63306/$DATABASE_NAME" -locations=filesystem:databases/tracker -user=$DATABASE_USERNAME -password=$DATABASE_PASSWORD clean migrate From fa9ada5fd6796a8a26b674499713319e954d5304 Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Thu, 1 Feb 2018 09:27:19 -0500 Subject: [PATCH 14/25] added DB related files --- ci/pipeline.yml | 24 ++++++++++++++++++++++++ databases/tracker/create_databases.sql | 12 ++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 databases/tracker/create_databases.sql diff --git a/ci/pipeline.yml b/ci/pipeline.yml index c34c3b2a9..689608c19 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -63,6 +63,18 @@ jobs: - get: pal-tracker-artifacts trigger: true passed: [build] + - task: migrate database + file: pal-tracker/ci/migrateDatabase.yml + params: + CF_API_URL: {{cf-api-url}} + CF_USERNAME: {{cf-username}} + CF_PASSWORD: {{cf-password}} + CF_ORG: {{cf-org}} + CF_SPACE: review + MYSQL_IP: {{mysql-ip}} + DATABASE_NAME: {{review-database-name}} + DATABASE_USERNAME: {{review-database-username}} + DATABASE_PASSWORD: {{review-database-password}} - put: review-deployment params: manifest: pal-tracker/manifest-review.yml @@ -75,6 +87,18 @@ jobs: - get: pal-tracker - get: pal-tracker-artifacts passed: [deploy-review] + - task: migrate database + file: pal-tracker/ci/migrateDatabase.yml + params: + CF_API_URL: {{cf-api-url}} + CF_USERNAME: {{cf-username}} + CF_PASSWORD: {{cf-password}} + CF_ORG: {{cf-org}} + CF_SPACE: review + MYSQL_IP: {{mysql-ip}} + DATABASE_NAME: {{review-database-name}} + DATABASE_USERNAME: {{review-database-username}} + DATABASE_PASSWORD: {{review-database-password}} - put: production-deployment params: manifest: pal-tracker/manifest-production.yml diff --git a/databases/tracker/create_databases.sql b/databases/tracker/create_databases.sql new file mode 100644 index 000000000..4ef067791 --- /dev/null +++ b/databases/tracker/create_databases.sql @@ -0,0 +1,12 @@ +DROP DATABASE IF EXISTS tracker_dev; +DROP DATABASE IF EXISTS tracker_test; + +CREATE DATABASE tracker_dev; +CREATE DATABASE tracker_test; + +CREATE USER IF NOT EXISTS 'tracker'@'localhost' + IDENTIFIED BY ''; +GRANT ALL PRIVILEGES ON tracker_dev.* TO 'tracker' @'localhost'; +GRANT ALL PRIVILEGES ON tracker_test.* TO 'tracker' @'localhost'; + +COMMIT; \ No newline at end of file From aec6317ffcf5e816ce750bef17999cb7cb8d9a3d Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Wed, 26 Jul 2017 11:45:04 -0600 Subject: [PATCH 15/25] Add tests for JDBC lab --- .../tracker/JdbcTimeEntryRepositoryTest.java | 159 ++++++++++++++++++ 1 file changed, 159 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..e1eac20fc --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/JdbcTimeEntryRepositoryTest.java @@ -0,0 +1,159 @@ +package test.pivotal.pal.tracker; + + +import com.mysql.cj.jdbc.MysqlDataSource; +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.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Date; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JdbcTimeEntryRepositoryTest { + private TimeEntryRepository subject; + private JdbcTemplate jdbcTemplate; + + @Before + public void setUp() throws Exception { + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setUrl(System.getenv("SPRING_DATASOURCE_URL")); + + subject = new JdbcTimeEntryRepository(dataSource); + + jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.execute("DELETE FROM time_entries"); + + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + } + + @Test + public void createInsertsATimeEntryRecord() throws Exception { + TimeEntry newTimeEntry = new TimeEntry(123, 321, LocalDate.parse("2017-01-09"), 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(((Date)foundEntry.get("date")).toLocalDate()).isEqualTo(LocalDate.parse("2017-01-09")); + assertThat(foundEntry.get("hours")).isEqualTo(8); + } + + @Test + public void createReturnsTheCreatedTimeEntry() throws Exception { + TimeEntry newTimeEntry = new TimeEntry(123, 321, LocalDate.parse("2017-01-09"), 8); + TimeEntry entry = subject.create(newTimeEntry); + + assertThat(entry.getId()).isNotNull(); + assertThat(entry.getProjectId()).isEqualTo(123); + assertThat(entry.getUserId()).isEqualTo(321); + assertThat(entry.getDate()).isEqualTo(LocalDate.parse("2017-01-09")); + 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, '2017-01-09', 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(LocalDate.parse("2017-01-09")); + 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, '2017-01-09', 8), (888, 456, 678, '2017-01-08', 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(LocalDate.parse("2017-01-08")); + 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(LocalDate.parse("2017-01-09")); + 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, '2017-01-09', 8)"); + + TimeEntry timeEntryUpdates = new TimeEntry(456, 987, LocalDate.parse("2017-01-10"), 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(LocalDate.parse("2017-01-10")); + 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, '2017-01-09', 8)"); + + TimeEntry updatedTimeEntry = new TimeEntry(456, 322, LocalDate.parse("2017-01-10"), 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(((Date)foundEntry.get("date")).toLocalDate()).isEqualTo(LocalDate.parse("2017-01-10")); + 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, '2017-01-09', 8)" + ); + + subject.delete(999L); + + Map foundEntry = jdbcTemplate.queryForMap("Select count(*) count from time_entries where id = ?", 999); + assertThat(foundEntry.get("count")).isEqualTo(0L); + } +} From 302cffaaa3719d7e1eedf4976ab54f94731e1124 Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Thu, 1 Feb 2018 10:02:02 -0500 Subject: [PATCH 16/25] updated DB related files --- build.gradle | 19 ++++++++- ci/pipeline.yml | 6 +-- .../pal/tracker/JdbcTimeEntryRepository.java | 41 +++++++++++++++++++ 3 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java diff --git a/build.gradle b/build.gradle index cdfd2df4c..094cf9d11 100644 --- a/build.gradle +++ b/build.gradle @@ -11,12 +11,27 @@ dependencies { compile("org.springframework.boot:spring-boot-starter-web") testCompile("org.springframework.boot:spring-boot-starter-test") compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.1") - + compile("org.springframework.boot:spring-boot-starter-jdbc") + compile("mysql:mysql-connector-java:6.0.6") } +def developmentDbUrl = "jdbc:mysql://localhost:3306/tracker_dev?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" bootRun.environment([ - "WELCOME_MESSAGE": "hello world", + "WELCOME_MESSAGE": "hello", + "SPRING_DATASOURCE_URL": developmentDbUrl, ]) +def testDbUrl = "jdbc:mysql://localhost:3306/tracker_test?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" test.environment([ "WELCOME_MESSAGE": "Hello from test", + "SPRING_DATASOURCE_URL": testDbUrl, ]) +flyway { + url = developmentDbUrl + user = "tracker" + password = "" + locations = ["filesystem:databases/tracker/migrations"] +} + +task testMigrate(type: FlywayMigrateTask) { + url = testDbUrl +} diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 689608c19..8255aefa8 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -96,9 +96,9 @@ jobs: CF_ORG: {{cf-org}} CF_SPACE: review MYSQL_IP: {{mysql-ip}} - DATABASE_NAME: {{review-database-name}} - DATABASE_USERNAME: {{review-database-username}} - DATABASE_PASSWORD: {{review-database-password}} + DATABASE_NAME: {{prod-database-name}} + DATABASE_USERNAME: {{prod-database-username}} + DATABASE_PASSWORD: {{prod-database-password}} - put: production-deployment params: manifest: pal-tracker/manifest-production.yml 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..bd9d1354c --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java @@ -0,0 +1,41 @@ +package io.pivotal.pal.tracker; + +import java.util.List; +import javax.sql.DataSource; + +/** + * Created by e048749 on 2/1/18. + */ +public class JdbcTimeEntryRepository implements TimeEntryRepository { + + private final JdbcTemplate jdbcTemplate; + + public JdbcTimeEntryRepository(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public TimeEntry find(long l) { + return null; + } + + @Override + public TimeEntry update(long eq, TimeEntry any) { + return null; + } + + @Override + public TimeEntry delete(long l) { + return null; + } + + @Override + public List list() { + return null; + } + + @Override + public TimeEntry create(TimeEntry any) { + return null; + } +} From 8c2eff5599dc3e9d6903510ae27b688a40c66de1 Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Wed, 26 Jul 2017 11:45:04 -0600 Subject: [PATCH 17/25] added test cases for DB --- build.gradle | 14 +--- .../tracker/InMemoryTimeEntryRepository.java | 11 +-- .../pal/tracker/JdbcTimeEntryRepository.java | 74 ++++++++++++++++--- .../pal/tracker/TimeEntryRepository.java | 8 +- .../pal/trackerapi/TimeEntryApiTest.java | 15 ++++ 5 files changed, 88 insertions(+), 34 deletions(-) diff --git a/build.gradle b/build.gradle index 094cf9d11..0160e5d1d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,8 @@ + plugins { id "java" id "org.springframework.boot" version "1.5.4.RELEASE" + id "org.flywaydb.flyway" version "4.2.0" } repositories { @@ -24,14 +26,4 @@ def testDbUrl = "jdbc:mysql://localhost:3306/tracker_test?user=tracker&useSSL=fa test.environment([ "WELCOME_MESSAGE": "Hello from test", "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/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java index 0c2e007a1..8a40793af 100644 --- a/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java +++ b/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java @@ -1,8 +1,5 @@ package io.pivotal.pal.tracker; -import io.pivotal.pal.tracker.TimeEntry; - -import java.sql.Time; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -17,23 +14,23 @@ public class InMemoryTimeEntryRepository implements TimeEntryRepository { Map timeEntryMap = new HashMap(); @Override - public TimeEntry find(long l) { + public TimeEntry find(Long l) { return timeEntryMap.get(l); } @Override - public TimeEntry update(long id, TimeEntry timeEntry) { + public TimeEntry update(Long id, TimeEntry timeEntry) { timeEntryMap.replace(id, timeEntry); timeEntry.setId(id); return timeEntry; } @Override - public TimeEntry delete(long id) { + public void delete(Long id) { TimeEntry timeEntry = timeEntryMap.get(id); timeEntryMap.remove(id); - return timeEntry; + } @Override diff --git a/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java index bd9d1354c..fb4c93cce 100644 --- a/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java +++ b/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java @@ -1,13 +1,22 @@ package io.pivotal.pal.tracker; -import java.util.List; +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 org.springframework.jdbc.support.KeyHolder; + import javax.sql.DataSource; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.util.List; + +import static java.sql.Statement.RETURN_GENERATED_KEYS; /** * Created by e048749 on 2/1/18. */ public class JdbcTimeEntryRepository implements TimeEntryRepository { - private final JdbcTemplate jdbcTemplate; public JdbcTimeEntryRepository(DataSource dataSource) { @@ -15,27 +24,68 @@ public JdbcTimeEntryRepository(DataSource dataSource) { } @Override - public TimeEntry find(long l) { - return null; + public TimeEntry create(TimeEntry timeEntry) { + KeyHolder generatedKeyHolder = new GeneratedKeyHolder(); + + jdbcTemplate.update(connection -> { + PreparedStatement statement = connection.prepareStatement( + "INSERT INTO time_entries (project_id, user_id, date, hours) " + + "VALUES (?, ?, ?, ?)", + RETURN_GENERATED_KEYS + ); + + statement.setLong(1, timeEntry.getProjectId()); + statement.setLong(2, timeEntry.getUserId()); + statement.setDate(3, Date.valueOf(timeEntry.getDate())); + statement.setInt(4, timeEntry.getHours()); + + return statement; + }, generatedKeyHolder); + + return find(generatedKeyHolder.getKey().longValue()); } @Override - public TimeEntry update(long eq, TimeEntry any) { - return null; + 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 TimeEntry delete(long l) { - return null; + public List list() { + return jdbcTemplate.query("SELECT id, project_id, user_id, date, hours FROM time_entries", mapper); } @Override - public List list() { - return null; + 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(), + Date.valueOf(timeEntry.getDate()), + timeEntry.getHours(), + id); + + return find(id); } @Override - public TimeEntry create(TimeEntry any) { - return null; + 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.getDate("date").toLocalDate(), + 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/TimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java index 9d4a5abc7..b517a120f 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java @@ -6,14 +6,14 @@ * Created by e048749 on 1/31/18. */ public interface TimeEntryRepository { - public TimeEntry find(long l) ; + public TimeEntry find(Long l); - public TimeEntry update(long eq, TimeEntry any) ; + public TimeEntry update(Long eq, TimeEntry any); - public TimeEntry delete(long l); + public void delete(Long l); public List list(); - public TimeEntry create(TimeEntry any) ; + public TimeEntry create(TimeEntry any); } diff --git a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java index 91e271b45..a3368d935 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java @@ -1,8 +1,10 @@ package test.pivotal.pal.trackerapi; import com.jayway.jsonpath.DocumentContext; +import com.mysql.cj.jdbc.MysqlDataSource; 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.springframework.beans.factory.annotation.Autowired; @@ -12,10 +14,12 @@ 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 java.time.LocalDate; import java.util.Collection; +import java.util.TimeZone; import static com.jayway.jsonpath.JsonPath.parse; import static org.assertj.core.api.Assertions.assertThat; @@ -30,6 +34,17 @@ public class TimeEntryApiTest { private TimeEntry timeEntry = new TimeEntry(123L, 456L, LocalDate.parse("2017-01-08"), 8); + @Before + public void setUp() throws Exception { + MysqlDataSource dataSource = new MysqlDataSource(); + dataSource.setUrl(System.getenv("SPRING_DATASOURCE_URL")); + + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.execute("TRUNCATE time_entries"); + + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + } + @Test public void testCreate() throws Exception { ResponseEntity createResponse = restTemplate.postForEntity("/time-entries", timeEntry, String.class); From 36cbbfa407ea5c89611789edf2420f4599940495 Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Wed, 26 Jul 2017 12:28:32 -0600 Subject: [PATCH 18/25] 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 4f27b036292867176355607750c3cd47016f50c3 Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Thu, 1 Feb 2018 10:55:52 -0500 Subject: [PATCH 19/25] added health api --- build.gradle | 9 ++++-- .../pal/tracker/TimeEntryController.java | 21 +++++++++--- .../pal/tracker/TimeEntryHealthIndicator.java | 32 +++++++++++++++++++ .../pal/tracker/TimeEntryControllerTest.java | 8 ++++- 4 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java diff --git a/build.gradle b/build.gradle index 0160e5d1d..652520f54 100644 --- a/build.gradle +++ b/build.gradle @@ -15,15 +15,20 @@ dependencies { compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.1") compile("org.springframework.boot:spring-boot-starter-jdbc") compile("mysql:mysql-connector-java:6.0.6") + compile("org.springframework.boot:spring-boot-starter-actuator") } def developmentDbUrl = "jdbc:mysql://localhost:3306/tracker_dev?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" bootRun.environment([ "WELCOME_MESSAGE": "hello", "SPRING_DATASOURCE_URL": developmentDbUrl, + "MANAGEMENT_SECURITY_ENABLED": false, ]) def testDbUrl = "jdbc:mysql://localhost:3306/tracker_test?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" test.environment([ "WELCOME_MESSAGE": "Hello from test", - "SPRING_DATASOURCE_URL": testDbUrl, -]) \ No newline at end of file + "SPRING_DATASOURCE_URL": testDbUrl,"MANAGEMENT_SECURITY_ENABLED": false, +]) +springBoot { + buildInfo() +} \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java index 3ad85622f..472c68634 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java @@ -1,10 +1,11 @@ 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; /** @@ -16,9 +17,15 @@ public class TimeEntryController { TimeEntryRepository timeEntryRepository; + private final GaugeService gauge; + private final CounterService counter; - public TimeEntryController(TimeEntryRepository timeEntryRepository) { + public TimeEntryController(TimeEntryRepository timeEntryRepository, CounterService counter, + GaugeService gauge) { this.timeEntryRepository = timeEntryRepository; + this.counter = counter; + this.gauge = gauge; + } @GetMapping("{id}") @@ -26,7 +33,7 @@ public ResponseEntity read(@PathVariable long id) { TimeEntry timeEntry = timeEntryRepository.find(id); if (timeEntry != null) { - + counter.increment("TimeEntry.read"); return new ResponseEntity<>(timeEntry, HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); @@ -38,6 +45,7 @@ public ResponseEntity read(@PathVariable long id) { public ResponseEntity update(@PathVariable long id, @RequestBody TimeEntry timeEntry) { TimeEntry updatedTimeEntry = timeEntryRepository.update(id, timeEntry); if (updatedTimeEntry != null) { + counter.increment("TimeEntry.updated"); return new ResponseEntity<>(updatedTimeEntry, HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); @@ -49,19 +57,22 @@ public ResponseEntity update(@PathVariable long id, @RequestBody TimeEntry timeE public ResponseEntity delete(@PathVariable long id) { timeEntryRepository.delete(id); - + counter.increment("TimeEntry.deleted"); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @GetMapping public ResponseEntity> list() { + counter.increment("TimeEntry.listed"); + return ResponseEntity.ok(timeEntryRepository.list()); } @PostMapping public ResponseEntity create(@RequestBody TimeEntry timeEntry) { TimeEntry createdTimeEntry = timeEntryRepository.create(timeEntry); - + counter.increment("TimeEntry.created"); + gauge.submit("timeEntries.count", timeEntryRepository.list().size()); return new ResponseEntity<>(createdTimeEntry, HttpStatus.CREATED); } } 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..09492fbe4 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java @@ -0,0 +1,32 @@ +package io.pivotal.pal.tracker; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Component; + +/** + * Created by e048749 on 2/1/18. + */ +@Component +public class TimeEntryHealthIndicator implements HealthIndicator { + + private static final int MAX_TIME_ENTRIES = 5; + private final TimeEntryRepository timeEntryRepo; + + public TimeEntryHealthIndicator(TimeEntryRepository timeEntryRepository) { + this.timeEntryRepo = timeEntryRepository; + } + + @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(); + } +} diff --git a/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java index 2013c28ec..8d0237dc7 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.TimeEntryRepository; 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; @@ -20,11 +22,15 @@ public class TimeEntryControllerTest { private TimeEntryRepository timeEntryRepository; private TimeEntryController controller; + private GaugeService gauge; + private CounterService counter; @Before public void setUp() throws Exception { timeEntryRepository = mock(TimeEntryRepository.class); - controller = new TimeEntryController(timeEntryRepository); + counter = mock(CounterService.class); + gauge = mock(GaugeService.class); + controller = new TimeEntryController(timeEntryRepository, counter, gauge); } @Test From 84de56251df3e91a37205fb38cc6ae6c8458645f Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Thu, 1 Feb 2018 11:07:15 -0500 Subject: [PATCH 20/25] updated build.yml --- ci/build.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ci/build.yml b/ci/build.yml index 4ba28db53..2455d052b 100644 --- a/ci/build.yml +++ b/ci/build.yml @@ -18,7 +18,18 @@ run: args: - -exc - | + + export DEBIAN_FRONTEND="noninteractive" + + apt-get update + + apt-get -y install mysql-server + service mysql start + cd pal-tracker + mysql -uroot < databases/tracker/create_databases.sql chmod +x gradlew - ./gradlew -P version=$(cat ../version/number) build - cp build/libs/pal-tracker-*.jar ../build-output + ./gradlew -P version=$(cat ../version/number) testMigrate clean build || (service mysql stop && exit 1) + service mysql stop + + cp build/libs/pal-tracker-*.jar ../build-output \ No newline at end of file From a14787a2d33726acc14363ab4a1562db7bbb5133 Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Thu, 1 Feb 2018 11:13:10 -0500 Subject: [PATCH 21/25] updated build.yml --- manifest-review.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manifest-review.yml b/manifest-review.yml index 4c6623ff6..cb681cc48 100644 --- a/manifest-review.yml +++ b/manifest-review.yml @@ -2,4 +2,6 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar - host: ps-pal-tracker-dp-review \ No newline at end of file + host: ps-pal-tracker-dp-review + services: + - tracker-database \ No newline at end of file From 8b2a2a3f9f0069a7fc6cd81096f6251fdbd8d3f2 Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Wed, 26 Jul 2017 12:50:47 -0600 Subject: [PATCH 22/25] 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 87ebc4ca98529e90c3e77858cbaf594608ac3f67 Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Thu, 1 Feb 2018 11:18:58 -0500 Subject: [PATCH 23/25] updated build.gradle --- build.gradle | 13 +++++++++++++ .../pivotal/pal/tracker/SecurityConfiguration.java | 7 +++++++ 2 files changed, 20 insertions(+) create mode 100644 src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java diff --git a/build.gradle b/build.gradle index 652520f54..4c1531fd6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,4 @@ +import org.flywaydb.gradle.task.FlywayMigrateTask plugins { id "java" @@ -16,6 +17,7 @@ dependencies { compile("org.springframework.boot:spring-boot-starter-jdbc") compile("mysql:mysql-connector-java:6.0.6") compile("org.springframework.boot:spring-boot-starter-actuator") + compile("org.springframework.boot:spring-boot-starter-security") } def developmentDbUrl = "jdbc:mysql://localhost:3306/tracker_dev?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" bootRun.environment([ @@ -31,4 +33,15 @@ test.environment([ ]) springBoot { buildInfo() +} + +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/src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java b/src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java new file mode 100644 index 000000000..bea72b39a --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java @@ -0,0 +1,7 @@ +package io.pivotal.pal.tracker; + +/** + * Created by e048749 on 2/1/18. + */ +public class SecurityConfiguration { +} From fead6996b0deee78ab7ab568c408be6c3953fc3c Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Thu, 1 Feb 2018 12:02:59 -0500 Subject: [PATCH 24/25] added security --- build.gradle | 6 +++- manifest-production.yml | 4 ++- manifest-review.yml | 4 ++- .../pal/tracker/SecurityConfiguration.java | 33 ++++++++++++++++++- .../pivotal/pal/trackerapi/HealthApiTest.java | 15 +++++++++ .../pal/trackerapi/TimeEntryApiTest.java | 15 +++++++++ .../pal/trackerapi/WelcomeApiTest.java | 19 +++++++++-- 7 files changed, 90 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 4c1531fd6..63b2f4ed5 100644 --- a/build.gradle +++ b/build.gradle @@ -24,12 +24,16 @@ bootRun.environment([ "WELCOME_MESSAGE": "hello", "SPRING_DATASOURCE_URL": developmentDbUrl, "MANAGEMENT_SECURITY_ENABLED": false, + + ]) def testDbUrl = "jdbc:mysql://localhost:3306/tracker_test?user=tracker&useSSL=false&useTimezone=true&serverTimezone=UTC&useLegacyDatetimeCode=false" test.environment([ "WELCOME_MESSAGE": "Hello from test", - "SPRING_DATASOURCE_URL": testDbUrl,"MANAGEMENT_SECURITY_ENABLED": false, + "SPRING_DATASOURCE_URL": testDbUrl, + "MANAGEMENT_SECURITY_ENABLED": false, + ]) springBoot { buildInfo() diff --git a/manifest-production.yml b/manifest-production.yml index 2091fb54c..274a690fc 100644 --- a/manifest-production.yml +++ b/manifest-production.yml @@ -2,4 +2,6 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar - host: ps-pal-tracker-dp-production \ No newline at end of file + host: ps-pal-tracker-dp-production + env: + SECURITY_FORCE_HTTPS: true \ No newline at end of file diff --git a/manifest-review.yml b/manifest-review.yml index cb681cc48..810ad68de 100644 --- a/manifest-review.yml +++ b/manifest-review.yml @@ -4,4 +4,6 @@ applications: path: build/libs/pal-tracker.jar host: ps-pal-tracker-dp-review services: - - tracker-database \ No newline at end of file + - tracker-database + env: + SECURITY_FORCE_HTTPS: true \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java b/src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java index bea72b39a..80491a097 100644 --- a/src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java +++ b/src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java @@ -1,7 +1,38 @@ package io.pivotal.pal.tracker; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + /** * Created by e048749 on 2/1/18. */ -public class SecurityConfiguration { + +@EnableWebSecurity +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + + String forceHttps = System.getenv("SECURITY_FORCE_HTTPS"); + if (forceHttps != null && forceHttps.equals("true")) { + http.requiresChannel().anyRequest().requiresSecure(); + } + + http + .authorizeRequests().antMatchers("/**").hasRole("USER") + .and() + .httpBasic() + .and() + .csrf().disable(); + } + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth + .inMemoryAuthentication() + .withUser("user").password("password").roles("USER"); + } } diff --git a/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java index b3eef23cc..f40e46ffb 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java @@ -2,11 +2,14 @@ import com.jayway.jsonpath.DocumentContext; 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; @@ -19,9 +22,21 @@ @SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) public class HealthApiTest { + @LocalServerPort + private String port; + @Autowired 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 healthTest() { ResponseEntity response = this.restTemplate.getForEntity("/health", String.class); diff --git a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java index a3368d935..cd5fc4647 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.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.HttpEntity; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -29,6 +31,12 @@ @SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) public class TimeEntryApiTest { + + + @LocalServerPort + private String port; + + @Autowired private TestRestTemplate restTemplate; @@ -43,6 +51,13 @@ public void setUp() throws Exception { jdbcTemplate.execute("TRUNCATE time_entries"); TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + + RestTemplateBuilder builder = new RestTemplateBuilder() + .rootUri("http://localhost:" + port) + .basicAuthorization("user", "password"); + + restTemplate = new TestRestTemplate(builder); + } @Test diff --git a/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java index cc7091ed4..5624171bf 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java @@ -1,23 +1,38 @@ 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; 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 + @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); From bb98529bd845ce120196561f50d477728b23ada1 Mon Sep 17 00:00:00 2001 From: "Pandya, Dipali" Date: Thu, 1 Feb 2018 13:28:58 -0500 Subject: [PATCH 25/25] added sql file --- ci/pipeline.yml | 2 +- databases/tracker/migrations/V1__initial_schema.sql | 11 +++++++++++ manifest-review.yml | 4 ++-- 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 databases/tracker/migrations/V1__initial_schema.sql diff --git a/ci/pipeline.yml b/ci/pipeline.yml index 8255aefa8..e86d417ed 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -94,7 +94,7 @@ jobs: CF_USERNAME: {{cf-username}} CF_PASSWORD: {{cf-password}} CF_ORG: {{cf-org}} - CF_SPACE: review + CF_SPACE: production MYSQL_IP: {{mysql-ip}} DATABASE_NAME: {{prod-database-name}} DATABASE_USERNAME: {{prod-database-username}} diff --git a/databases/tracker/migrations/V1__initial_schema.sql b/databases/tracker/migrations/V1__initial_schema.sql new file mode 100644 index 000000000..3e605c0b5 --- /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 DATE, + hours INT, + + PRIMARY KEY (id) +) + ENGINE = innodb + DEFAULT CHARSET = utf8; diff --git a/manifest-review.yml b/manifest-review.yml index 810ad68de..ac16ff401 100644 --- a/manifest-review.yml +++ b/manifest-review.yml @@ -5,5 +5,5 @@ applications: host: ps-pal-tracker-dp-review services: - tracker-database - env: - SECURITY_FORCE_HTTPS: true \ No newline at end of file + env: + SECURITY_FORCE_HTTPS: true \ No newline at end of file