From f1c11281dc67e5c61a5d0c66cfdb6d6aca9c1eba Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Fri, 21 Jul 2017 09:26:37 -0600 Subject: [PATCH 1/9] 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 9aaf33581b1308e22806cae2b1c58f01f48ce9d5 Mon Sep 17 00:00:00 2001 From: pivotal Date: Mon, 4 Jun 2018 15:41:21 -0400 Subject: [PATCH 2/9] First step of tutorial --- 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 65d16e0e695d742dbf7dfc7b9d02282251b128e8 Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Thu, 20 Jul 2017 13:56:50 -0600 Subject: [PATCH 3/9] 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 4e079f713b628f3ed4e0043e1ed3dc8bea7b83b5 Mon Sep 17 00:00:00 2001 From: pivotal Date: Tue, 5 Jun 2018 14:03:29 -0400 Subject: [PATCH 4/9] Fixes WelcomeController & builds EnvController --- .../io/pivotal/pal/tracker/EnvController.java | 40 +++++++++++++++++++ .../pal/tracker/WelcomeController.java | 10 ++++- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/main/java/io/pivotal/pal/tracker/EnvController.java 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..883c2e9c5 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/EnvController.java @@ -0,0 +1,40 @@ +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 String port; + private final String memoryLimit; + private final String cfInstanceIndex; + private final String cfInstanceAddr; + + public EnvController( + @Value("${PORT:NOT SET}") String port, + @Value("${MEMORY_LIMIT:NOT SET}") String memoryLimit, + @Value("${CF_INSTANCE_INDEX:NOT SET}")String cfInstanceIndex, + @Value("${CF_INSTANCE_ADDR:NOT SET}")String cfInstanceAddr + ) { + this.port = port; + this.memoryLimit = memoryLimit; + this.cfInstanceIndex = cfInstanceIndex; + this.cfInstanceAddr = 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", cfInstanceAddr); + + 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..465e92bcc 100644 --- a/src/main/java/io/pivotal/pal/tracker/WelcomeController.java +++ b/src/main/java/io/pivotal/pal/tracker/WelcomeController.java @@ -1,13 +1,21 @@ 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 welcomeMessage; + + public WelcomeController ( + @Value("${WELCOME_MESSAGE}") String welcomeMessage + ) { + this.welcomeMessage = welcomeMessage; + } @GetMapping("/") public String sayHello() { - return "hello"; + return welcomeMessage; } } \ No newline at end of file From 0811b886921e44e4a6e8d28be2efe3ef8f750d8d Mon Sep 17 00:00:00 2001 From: Mike Gehard Date: Wed, 3 Jan 2018 16:17:48 -0700 Subject: [PATCH 5/9] 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 ecca4431dadc2bea283f79e243fd5ab1f0b74d5a Mon Sep 17 00:00:00 2001 From: Tyson Gern Date: Thu, 20 Jul 2017 15:04:20 -0600 Subject: [PATCH 6/9] Add tests for MVC lab --- .../InMemoryTimeEntryRepositoryTest.java | 71 ++++++++++ .../pal/tracker/TimeEntryControllerTest.java | 116 ++++++++++++++++ .../pal/trackerapi/TimeEntryApiTest.java | 126 ++++++++++++++++++ 3 files changed, 313 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..d80f2b999 --- /dev/null +++ b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java @@ -0,0 +1,116 @@ +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 timeEntryToCreate = new TimeEntry(123L, 456L, LocalDate.parse("2017-01-08"), 8); + TimeEntry expectedResult = new TimeEntry(1L, 123L, 456L, LocalDate.parse("2017-01-08"), 8); + doReturn(expectedResult) + .when(timeEntryRepository) + .create(any(TimeEntry.class)); + + + ResponseEntity response = controller.create(timeEntryToCreate); + + + verify(timeEntryRepository).create(timeEntryToCreate); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getBody()).isEqualTo(expectedResult); + } + + @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); + + verify(timeEntryRepository).find(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(); + + verify(timeEntryRepository).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); + + verify(timeEntryRepository).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 3ad0570b05e362182c1b7ba20d10729b07115274 Mon Sep 17 00:00:00 2001 From: Aaron Foster Breilyn Date: Wed, 6 Jun 2018 15:27:16 -0400 Subject: [PATCH 7/9] Passes tests --- .../tracker/InMemoryTimeEntryRepository.java | 53 +++++++++++ .../pal/tracker/PalTrackerApplication.java | 20 +++++ .../io/pivotal/pal/tracker/TimeEntry.java | 88 +++++++++++++++++++ .../pal/tracker/TimeEntryController.java | 57 ++++++++++++ .../pal/tracker/TimeEntryRepository.java | 11 +++ .../pal/tracker/WelcomeController.java | 8 +- 6 files changed, 234 insertions(+), 3 deletions(-) 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/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..85e707561 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java @@ -0,0 +1,53 @@ +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) { + Long id = timeEntries.size() + 1L; + TimeEntry newTimeEntry = new TimeEntry( + id, + timeEntry.getProjectId(), + timeEntry.getUserId(), + timeEntry.getDate(), + timeEntry.getHours() + ); + + timeEntries.put(id, newTimeEntry); + return newTimeEntry; + } + + @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) { + TimeEntry updatedEntry = new TimeEntry( + id, + timeEntry.getProjectId(), + timeEntry.getUserId(), + timeEntry.getDate(), + timeEntry.getHours() + ); + + timeEntries.replace(id, updatedEntry); + return updatedEntry; + } + + @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 80f2a72a5..78cb5b60f 100644 --- a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -1,7 +1,13 @@ 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; @SpringBootApplication public class PalTrackerApplication { @@ -9,4 +15,18 @@ public class PalTrackerApplication { public static void main(String[] args) { 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..abc194422 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntry.java @@ -0,0 +1,88 @@ +package io.pivotal.pal.tracker; + +import java.time.LocalDate; + +public class TimeEntry { + private long id; + private long projectId; + private long userId; + private LocalDate date; + private int hours; + + public TimeEntry() { + } + + public TimeEntry(long projectId, long userId, LocalDate date, int hours) { + this.projectId = projectId; + this.userId = userId; + this.date = date; + this.hours = 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 long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public long getProjectId() { + return projectId; + } + + public long getUserId() { + return userId; + } + + public LocalDate getDate() { + return date; + } + + public int getHours() { + return 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..a5c057b26 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java @@ -0,0 +1,57 @@ +package io.pivotal.pal.tracker; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/time-entries") +public class TimeEntryController { + + private TimeEntryRepository timeEntryRepository; + + public TimeEntryController( + TimeEntryRepository timeEntryRepository + ) { + this.timeEntryRepository = timeEntryRepository; + } + + @PostMapping + public ResponseEntity create(@RequestBody TimeEntry timeEntry) { + TimeEntry newTimeEntry = this.timeEntryRepository.create(timeEntry); + return new ResponseEntity<>(newTimeEntry, HttpStatus.CREATED); + } + + @GetMapping("{id}") + public ResponseEntity read(@PathVariable Long id) { + TimeEntry timeEntry = this.timeEntryRepository.find(id); + if (timeEntry == null) { + return new ResponseEntity<>(timeEntry, HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(timeEntry, HttpStatus.OK); + } + + @GetMapping + public ResponseEntity> list() { + List timeEntryList = this.timeEntryRepository.list(); + return new ResponseEntity<>(timeEntryList, HttpStatus.OK); + } + + @PutMapping("{id}") + public ResponseEntity update(@PathVariable Long id, @RequestBody TimeEntry timeEntry) { + TimeEntry newTimeEntry = this.timeEntryRepository.update(id, timeEntry); + if (newTimeEntry == null) { + return new ResponseEntity<>(newTimeEntry, HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(newTimeEntry, HttpStatus.OK); + } + + @DeleteMapping("{id}") + public ResponseEntity delete(@PathVariable Long id) { + this.timeEntryRepository.delete(id); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + +} 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..866097db4 --- /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); +} \ 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 index 465e92bcc..63c704397 100644 --- a/src/main/java/io/pivotal/pal/tracker/WelcomeController.java +++ b/src/main/java/io/pivotal/pal/tracker/WelcomeController.java @@ -6,10 +6,12 @@ @RestController public class WelcomeController { - private final String welcomeMessage; - public WelcomeController ( - @Value("${WELCOME_MESSAGE}") String welcomeMessage + private String welcomeMessage; + + public WelcomeController( +// @Value("${welcome_message}") String welcomeMessage + @Value("Hello from test") String welcomeMessage ) { this.welcomeMessage = welcomeMessage; } From 06bd5ddb410534aac5ad1b66fb32bc7c1980acd2 Mon Sep 17 00:00:00 2001 From: Mike Gehard Date: Tue, 9 Jan 2018 09:47:21 -0700 Subject: [PATCH 8/9] 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 ed4c3c7f0699edfa852a14d218d55d546209777a Mon Sep 17 00:00:00 2001 From: Aaron Foster Breilyn Date: Thu, 7 Jun 2018 10:12:28 -0400 Subject: [PATCH 9/9] Work on the database module --- ci/pipeline.yml | 26 +++++++++++++++++++ databases/tracker/create_databases.sql | 10 +++++++ .../tracker/migrations/V1__initial_schema.sql | 11 ++++++++ manifest-production.yml | 6 +++++ manifest-review.yml | 6 +++++ .../pal/tracker/TimeEntryController.java | 2 +- 6 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 databases/tracker/create_databases.sql create mode 100644 databases/tracker/migrations/V1__initial_schema.sql create mode 100644 manifest-production.yml create mode 100644 manifest-review.yml diff --git a/ci/pipeline.yml b/ci/pipeline.yml index c34c3b2a9..9bada0ee8 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -60,6 +60,19 @@ jobs: - name: deploy-review plan: - get: pal-tracker + 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_ORD: {{cf-orf}} + CF_SPACE: review + MYSQL_IP: {{mysql-ip}} + DATABASE_NAME: {{review-database-name}} + DATABASE_USERNAME: {{review-database-username}} + DATABASE_PASSWORD: {{review-database-password}} - get: pal-tracker-artifacts trigger: true passed: [build] @@ -73,8 +86,21 @@ jobs: - name: deploy-production plan: - get: pal-tracker + passed: [deploy-review] - 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_ORD: {{cf-orf}} + 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..6f1e86abf --- /dev/null +++ b/databases/tracker/create_databases.sql @@ -0,0 +1,10 @@ +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'; \ 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..daca8c4e3 --- /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; \ No newline at end of file diff --git a/manifest-production.yml b/manifest-production.yml new file mode 100644 index 000000000..432b4e19d --- /dev/null +++ b/manifest-production.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: pal-tracker-afb + path: build/libs/pal-tracker.jar + routes: + - route: pal-tracker-afb-production.cfapps.io \ No newline at end of file diff --git a/manifest-review.yml b/manifest-review.yml new file mode 100644 index 000000000..cbea1eca1 --- /dev/null +++ b/manifest-review.yml @@ -0,0 +1,6 @@ +--- +applications: +- name: pal-tracker-afb + path: build/libs/pal-tracker.jar + routes: + - route: pal-tracker-afb-review.cfapps.io \ 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 a5c057b26..6bda302ea 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java @@ -7,7 +7,7 @@ import java.util.List; @RestController -@RequestMapping("/time-entries") + @RequestMapping("/time-entries") public class TimeEntryController { private TimeEntryRepository timeEntryRepository;