diff --git a/.gitignore b/.gitignore index d5c2a6a3c..aaf6b661a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ out .gradle .idea *.iml -ci/variables.yml +ci/variables.ymdl diff --git a/build.gradle b/build.gradle index fdc6a55b7..fab2ad967 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,9 @@ +import org.flywaydb.gradle.task.FlywayMigrateTask + plugins { - id 'java' - id 'org.springframework.boot' version '1.5.10.RELEASE' + id "java" + id "org.springframework.boot" version "1.5.4.RELEASE" + id "org.flywaydb.flyway" version "4.2.0" } repositories { @@ -9,14 +12,40 @@ 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") + compile("org.springframework.boot:spring-boot-starter-jdbc") + compile("mysql:mysql-connector-java:6.0.6") + compile("org.springframework.boot:spring-boot-starter-actuator") + testCompile("org.springframework.boot:spring-boot-starter-test") + 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([ "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", -]) \ No newline at end of file + "SPRING_DATASOURCE_URL": testDbUrl, + "MANAGEMENT_SECURITY_ENABLED": false, +]) + +flyway { + url = developmentDbUrl + user = "tracker" + password = "" + locations = ["filesystem:databases/tracker/migrations"] +} + +task testMigrate(type: FlywayMigrateTask) { + url = testDbUrl +} + +springBoot{ + buildInfo() +} \ No newline at end of file diff --git a/ci/build.yml b/ci/build.yml index 4ba28db53..9f5a19a41 100644 --- a/ci/build.yml +++ b/ci/build.yml @@ -18,7 +18,13 @@ 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 diff --git a/ci/migrateDatabase.yml b/ci/migrateDatabase.yml index b2af8c5ef..9f1fdace2 100644 --- a/ci/migrateDatabase.yml +++ b/ci/migrateDatabase.yml @@ -10,6 +10,7 @@ inputs: - name: pal-tracker # - name: version + #outputs: # - name: build-output diff --git a/ci/pipeline.yml b/ci/pipeline.yml index c34c3b2a9..548a3732c 100644 --- a/ci/pipeline.yml +++ b/ci/pipeline.yml @@ -60,9 +60,22 @@ jobs: - name: deploy-review plan: - get: pal-tracker + passed: [build] - 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 @@ -73,8 +86,22 @@ jobs: - name: deploy-production plan: - get: pal-tracker + passed: [deploy-review] - get: pal-tracker-artifacts + trigger: true 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: production + MYSQL_IP: {{mysql-ip}} + DATABASE_NAME: {{production-database-name}} + DATABASE_USERNAME: {{production-database-username}} + DATABASE_PASSWORD: {{production-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..8b5ed7ba5 --- /dev/null +++ b/databases/tracker/create_databases.sql @@ -0,0 +1,11 @@ +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'; + diff --git a/databases/tracker/migrations/V1__initial.schema.sql b/databases/tracker/migrations/V1__initial.schema.sql new file mode 100644 index 000000000..d529ac1b1 --- /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-production.yml b/manifest-production.yml index 340b53421..3ae5ad9e4 100644 --- a/manifest-production.yml +++ b/manifest-production.yml @@ -2,4 +2,8 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar - host: ps-pal-tracker \ No newline at end of file + host: ps-pal-tracker + services: + - tracker-database + env: + SECURITY_FORCE_HTTPS: true \ No newline at end of file diff --git a/manifest-review.yml b/manifest-review.yml index cd3361d30..cbfddd6cd 100644 --- a/manifest-review.yml +++ b/manifest-review.yml @@ -2,4 +2,8 @@ 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-review + services: + - tracker-database + env: + SECURITY_FORCE_HTTPS: true \ 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 72db871dc..07785c3e0 100644 --- a/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java +++ b/src/main/java/io/pivotal/pal/tracker/InMemoryTimeEntryRepository.java @@ -5,7 +5,6 @@ import java.util.List; public class InMemoryTimeEntryRepository implements TimeEntryRepository { - private HashMap timeEntries = new HashMap<>(); @Override @@ -16,24 +15,24 @@ public TimeEntry create(TimeEntry timeEntry) { } @Override - public TimeEntry find(long id) { + public TimeEntry find(Long id) { return timeEntries.get(id); } @Override public List list() { - return new ArrayList(timeEntries.values()); + return new ArrayList<>(timeEntries.values()); } @Override - public TimeEntry update(long id, TimeEntry timeEntry) { - timeEntry.setId(id); + public TimeEntry update(Long id, TimeEntry timeEntry) { timeEntries.replace(id, timeEntry); + timeEntry.setId(id); return timeEntry; } @Override - public void delete(long id) { + public void delete(Long id) { timeEntries.remove(id); } -} +} \ No newline at end of file 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..72173dd4f --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/JdbcTimeEntryRepository.java @@ -0,0 +1,88 @@ +package io.pivotal.pal.tracker; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import 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; + +public class JdbcTimeEntryRepository implements TimeEntryRepository { + + private final JdbcTemplate jdbcTemplate; + + public JdbcTimeEntryRepository(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + 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 find(Long id) { + return jdbcTemplate.query( + "SELECT id, project_id, user_id, date, hours FROM time_entries WHERE id = ?", + new Object[]{id}, + extractor); + } + + @Override + public List list() { + return jdbcTemplate.query("SELECT id, project_id, user_id, date, hours FROM time_entries", mapper); + } + + @Override + public TimeEntry update(Long id, TimeEntry timeEntry) { + jdbcTemplate.update("UPDATE time_entries " + + "SET project_id = ?, user_id = ?, date = ?, hours = ? " + + "WHERE id = ?", + timeEntry.getProjectId(), + timeEntry.getUserId(), + Date.valueOf(timeEntry.getDate()), + timeEntry.getHours(), + id); + + return find(id); + } + + @Override + public void delete(Long id) { + jdbcTemplate.update("DELETE FROM time_entries WHERE id = ?", id); + } + + private final RowMapper mapper = (rs, rowNum) -> new TimeEntry( + rs.getLong("id"), + rs.getLong("project_id"), + rs.getLong("user_id"), + rs.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/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java index 3cd26e2a5..22884d4f4 100644 --- a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -9,16 +9,21 @@ import org.springframework.context.annotation.Bean; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import javax.sql.DataSource; +import java.util.TimeZone; + @SpringBootApplication public class PalTrackerApplication { public static void main(String[] args) { + // Make sure the application runs as UTC + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); SpringApplication.run(PalTrackerApplication.class, args); } @Bean - TimeEntryRepository timeEntryRepository() { - return new InMemoryTimeEntryRepository(); + TimeEntryRepository timeEntryRepository(DataSource dataSource) { + return new JdbcTimeEntryRepository(dataSource); } @Bean @@ -29,5 +34,4 @@ public ObjectMapper jsonObjectMapper() { .modules(new JavaTimeModule()) .build(); } - -} +} \ 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..8548a54b7 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java @@ -0,0 +1,35 @@ +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; + +@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/main/java/io/pivotal/pal/tracker/TimeEntryController.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java index cf9276799..427ed4aaf 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java @@ -1,5 +1,7 @@ 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.*; @@ -10,32 +12,43 @@ @RequestMapping("/time-entries") public class TimeEntryController { + private final CounterService counter; + private final GaugeService gauge; private TimeEntryRepository timeEntriesRepo; - public TimeEntryController(TimeEntryRepository timeEntriesRepo) { + public TimeEntryController( + TimeEntryRepository timeEntriesRepo, + CounterService counter, + GaugeService gauge + ) { this.timeEntriesRepo = timeEntriesRepo; + this.counter = counter; + this.gauge = gauge; } - @PostMapping public ResponseEntity create(@RequestBody TimeEntry timeEntry) { - TimeEntry createdTimeEntry = timeEntriesRepo.create(timeEntry); - return new ResponseEntity<>(createdTimeEntry, HttpStatus.CREATED); + TimeEntry createdTimeEntry = timeEntriesRepo.create(timeEntry); + counter.increment("TimeEntry.created"); + gauge.submit("timeEntries.count", timeEntriesRepo.list().size()); + + return new ResponseEntity<>(createdTimeEntry, HttpStatus.CREATED); } @GetMapping("{id}") - public ResponseEntity read(@PathVariable long id) { + public ResponseEntity read(@PathVariable Long id) { TimeEntry timeEntry = timeEntriesRepo.find(id); if (timeEntry != null) { + counter.increment("TimeEntry.read"); return new ResponseEntity<>(timeEntry, HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } } - @GetMapping public ResponseEntity> list() { + counter.increment("TimeEntry.listed"); return new ResponseEntity<>(timeEntriesRepo.list(), HttpStatus.OK); } @@ -43,6 +56,7 @@ public ResponseEntity> list() { public ResponseEntity update(@PathVariable Long id, @RequestBody TimeEntry timeEntry) { TimeEntry updatedTimeEntry = timeEntriesRepo.update(id, timeEntry); if (updatedTimeEntry != null) { + counter.increment("TimeEntry.updated"); return new ResponseEntity<>(updatedTimeEntry, HttpStatus.OK); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); @@ -52,6 +66,8 @@ public ResponseEntity update(@PathVariable Long id, @RequestBody Time @DeleteMapping("{id}") public ResponseEntity delete(@PathVariable Long id) { timeEntriesRepo.delete(id); + counter.increment("TimeEntry.deleted"); + gauge.submit("timeEntries.count", timeEntriesRepo.list().size()); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java new file mode 100644 index 000000000..49b1f175d --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java @@ -0,0 +1,29 @@ +package io.pivotal.pal.tracker; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.stereotype.Component; + +@Component +public class TimeEntryHealthIndicator implements HealthIndicator { + + private static final int MAX_TIME_ENTRIES = 5; + private final TimeEntryRepository timeEntryRepo; + + public TimeEntryHealthIndicator(TimeEntryRepository timeEntryRepo) { + this.timeEntryRepo = timeEntryRepo; + } + + @Override + public Health health() { + Health.Builder builder = new Health.Builder(); + + if(timeEntryRepo.list().size() < MAX_TIME_ENTRIES) { + builder.up(); + } else { + builder.down(); + } + + return builder.build(); + } +} \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java index c2ff55d14..866097db4 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryRepository.java @@ -1,18 +1,11 @@ package io.pivotal.pal.tracker; -import org.springframework.http.ResponseEntity; - import java.util.List; public interface TimeEntryRepository { - - TimeEntry create(TimeEntry any); - - TimeEntry find(long l); - + TimeEntry create(TimeEntry timeEntry); + TimeEntry find(Long id); List list(); - - TimeEntry update(long eq, TimeEntry any); - - void delete(long l); -} + TimeEntry update(Long id, TimeEntry timeEntry); + void delete(Long id); +} \ No newline at end of file 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); + } +} diff --git a/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java b/src/test/java/test/pivotal/pal/tracker/TimeEntryControllerTest.java index 49c54aa02..5956ecd88 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 CounterService counterService; + private GaugeService gaugeService; @Before public void setUp() throws Exception { timeEntryRepository = mock(TimeEntryRepository.class); - controller = new TimeEntryController(timeEntryRepository); + counterService = mock(CounterService.class); + gaugeService = mock(GaugeService.class); + controller = new TimeEntryController(timeEntryRepository, counterService, gaugeService); } @Test @@ -34,7 +40,7 @@ public void testCreate() throws Exception { .when(timeEntryRepository) .create(any(TimeEntry.class)); - ResponseEntity response = controller.create(new TimeEntry(123L, 456L, LocalDate.parse("2017-01-08"), 80)); + 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); @@ -103,4 +109,4 @@ public void testDelete() throws Exception { verify(timeEntryRepository).delete(1L); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); } -} +} \ No newline at end of file diff --git a/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java new file mode 100644 index 000000000..d72d80066 --- /dev/null +++ b/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java @@ -0,0 +1,59 @@ +package test.pivotal.pal.trackerapi; + +import com.jayway.jsonpath.DocumentContext; +import com.mysql.cj.jdbc.MysqlDataSource; +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.jdbc.core.JdbcTemplate; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.TimeZone; + +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; + + @LocalServerPort + private String port; + + @Before + public void setUp(){ + 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); + + + 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"); + + + } +} 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); + } +} diff --git a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java index 91e271b45..18319e22e 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java @@ -1,26 +1,30 @@ 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; +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; 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; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; - @RunWith(SpringRunner.class) @SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) public class TimeEntryApiTest { @@ -30,6 +34,26 @@ public class TimeEntryApiTest { private TimeEntry timeEntry = new TimeEntry(123L, 456L, LocalDate.parse("2017-01-08"), 8); + @LocalServerPort + private String port; + + @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")); + + RestTemplateBuilder builder = new RestTemplateBuilder(). + rootUri("http://localhost:"+port). + basicAuthorization("user","password"); + + restTemplate = new TestRestTemplate(builder); + } + @Test public void testCreate() throws Exception { ResponseEntity createResponse = restTemplate.postForEntity("/time-entries", timeEntry, String.class); diff --git a/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java index cc7091ed4..804f1a7e0 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java @@ -1,11 +1,14 @@ package test.pivotal.pal.trackerapi; import io.pivotal.pal.tracker.PalTrackerApplication; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.LocalServerPort; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.test.context.junit4.SpringRunner; import static org.assertj.core.api.Assertions.assertThat; @@ -18,6 +21,18 @@ public class WelcomeApiTest { @Autowired private TestRestTemplate restTemplate; + @LocalServerPort + private String port; + + @Before + public void setUp(){ + 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);