diff --git a/build.gradle b/build.gradle index e6dae2a9d..ad300d36e 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 "org.flywaydb.flyway" version "4.2.0" } repositories { @@ -10,13 +13,40 @@ repositories { dependencies { compile("org.springframework.boot:spring-boot-starter-web") compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.1") + compile("org.springframework.boot:spring-boot-starter-jdbc") + compile("org.springframework.boot:spring-boot-starter-data-jpa") + compile("mysql:mysql-connector-java:6.0.6") + compile("org.springframework.boot:spring-boot-starter-actuator") + compile("org.springframework.boot:spring-boot-starter-security") testCompile("org.springframework.boot:spring-boot-starter-test") } +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, + "AUTH_REQUIRED": true, ]) +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, + "AUTH_REQUIRED": true, +]) + +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..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 diff --git a/databases/tracker/V1__initial_schema.sql b/databases/tracker/migrations/V1__initial_schema.sql similarity index 100% rename from databases/tracker/V1__initial_schema.sql rename to databases/tracker/migrations/V1__initial_schema.sql diff --git a/manifest-production.yml b/manifest-production.yml index d22a10f33..54f6f7660 100644 --- a/manifest-production.yml +++ b/manifest-production.yml @@ -3,3 +3,6 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar host: runiu2-pal-tracker + env: + SECURITY_FORCE_HTTPS: true + AUTH_REQUIRED: true \ No newline at end of file diff --git a/manifest-review.yml b/manifest-review.yml index dba79b5b0..c0dbb03c1 100644 --- a/manifest-review.yml +++ b/manifest-review.yml @@ -2,4 +2,9 @@ applications: - name: pal-tracker path: build/libs/pal-tracker.jar - host: runiu2-pal-tracker-review2 + host: runiu2-pal-tracker-review + services: + - tracker-database + env: + SECURITY_FORCE_HTTPS: true + AUTH_REQUIRED: true \ No newline at end of file diff --git a/src/main/java/io/pivotal/pal/tracker/ExampleInfoContributor.java b/src/main/java/io/pivotal/pal/tracker/ExampleInfoContributor.java new file mode 100644 index 000000000..d5f9dae71 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/ExampleInfoContributor.java @@ -0,0 +1,16 @@ +package io.pivotal.pal.tracker; + +import org.springframework.boot.actuate.info.Info; +import org.springframework.boot.actuate.info.InfoContributor; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; + +@Component +public class ExampleInfoContributor implements InfoContributor { + @Override + public void contribute(Info.Builder builder) { + builder.withDetail("example", Collections.singletonMap("author", "runiu")); + } +} 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..4e93ae208 --- /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.PreparedStatementCreator; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.support.GeneratedKeyHolder; +import org.springframework.jdbc.support.KeyHolder; +import org.springframework.util.CollectionUtils; + +import javax.sql.DataSource; +import java.sql.*; +import java.util.List; + +public class JdbcTimeEntryRepository implements TimeEntryRepository { + + private JdbcTemplate jdbcTemplate; + + public JdbcTimeEntryRepository(DataSource ds) { + this.jdbcTemplate = new JdbcTemplate(ds); + } + + private RowMapper rowMapper = new RowMapper() { + + @Override + public TimeEntry mapRow(ResultSet rs, int rowNum) throws SQLException { + TimeEntry timeEntry = new TimeEntry(rs.getLong("id"), rs.getLong("project_id"), rs.getLong("user_id"), rs.getDate("date").toLocalDate(), rs.getInt("hours")); + return timeEntry; + } + }; + + @Override + public TimeEntry create(TimeEntry timeEntry) { + final KeyHolder holder = new GeneratedKeyHolder(); + final PreparedStatementCreator psc = new PreparedStatementCreator() { + @Override + public PreparedStatement createPreparedStatement(final Connection connection) throws SQLException { + final PreparedStatement ps = connection.prepareStatement("insert into time_entries (project_id, user_id,date,hours) values (?,?,?,?)", + Statement.RETURN_GENERATED_KEYS); + ps.setLong(1, timeEntry.getProjectId()); + ps.setLong(2, timeEntry.getUserId()); + ps.setDate(3, Date.valueOf(timeEntry.getDate())); + ps.setInt(4, timeEntry.getHours()); + + return ps; + } + }; + jdbcTemplate.update(psc, holder); + timeEntry.setId(holder.getKey().longValue()); + return timeEntry; + } + + @Override + public TimeEntry find(long id) { + List timeEntryList = jdbcTemplate.query("select * from time_entries where id =?", new Object[]{id}, rowMapper); + if (!CollectionUtils.isEmpty(timeEntryList)) { + return timeEntryList.get(0); + } + return null; + } + + @Override + public List list() { + List timeEntryList = jdbcTemplate.query("select * from time_entries", rowMapper); + return timeEntryList; + } + + @Override + public TimeEntry update(long id, TimeEntry timeEntry) { + TimeEntry timeEntryFound = this.find(id); + if (timeEntryFound != null) { + timeEntry.setId(id); + 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 timeEntry; + } + return null; + } + + @Override + public TimeEntry delete(long id) { + TimeEntry timeEntry = this.find(id); + jdbcTemplate.update("delete from time_entries where id =?", id); + if (timeEntry != null) { + return timeEntry; + } + return null; + } +} diff --git a/src/main/java/io/pivotal/pal/tracker/LocalDateConverter.java b/src/main/java/io/pivotal/pal/tracker/LocalDateConverter.java new file mode 100644 index 000000000..9293f14ad --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/LocalDateConverter.java @@ -0,0 +1,20 @@ +package io.pivotal.pal.tracker; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; +import java.lang.annotation.Annotation; +import java.sql.Date; +import java.time.LocalDate; + +@Converter(autoApply=true) +public class LocalDateConverter implements AttributeConverter { + @Override + public Date convertToDatabaseColumn(LocalDate localDate) { + return Date.valueOf(localDate); + } + + @Override + public LocalDate convertToEntityAttribute(Date date) { + return date.toLocalDate(); + } +} diff --git a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java index a91989c33..2b45bcec8 100644 --- a/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java +++ b/src/main/java/io/pivotal/pal/tracker/PalTrackerApplication.java @@ -7,14 +7,18 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import javax.sql.DataSource; + @SpringBootApplication +@EnableJpaRepositories public class PalTrackerApplication { @Bean - public TimeEntryRepository repo(){ - return new InMemoryTimeEntryRepository(); + public TimeEntryRepository repo(DataSource ds){ + return new JdbcTimeEntryRepository(ds); } @Bean 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..ccdc3d66c --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/SecurityConfiguration.java @@ -0,0 +1,43 @@ +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.builders.WebSecurity; +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 + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers("/time-entries-jpa**"); + super.configure(web); + } + + protected void configure(HttpSecurity http) throws Exception { + String forceHttps = System.getenv("SECURITY_FORCE_HTTPS"); + if (forceHttps != null && forceHttps.equals("true")) { + http.requiresChannel().anyRequest().requiresSecure(); + } + String authRequired = System.getenv("AUTH_REQUIRED"); + if(Boolean.valueOf(authRequired)) { + http + .authorizeRequests().antMatchers("/**").hasRole("USER") + .and() + .httpBasic() + .and() + .csrf().disable(); + }else{ + http.authorizeRequests().antMatchers("/**").anonymous().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/TimeEntry.java b/src/main/java/io/pivotal/pal/tracker/TimeEntry.java index 71e80d9e8..947986a77 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntry.java +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntry.java @@ -1,13 +1,25 @@ package io.pivotal.pal.tracker; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import java.io.Serializable; import java.time.LocalDate; import java.util.Objects; +@Entity(name="time_entries") public class TimeEntry { + @Id + @GeneratedValue private long id; + @Column(name="project_id") private long projectId; + @Column(name="user_id") private long userId; + @Column(name="date") private LocalDate date; + @Column(name="hours") private int hours; public TimeEntry(){ @@ -67,4 +79,19 @@ public int hashCode() { return Objects.hash(id, projectId, userId, date, hours); } + public void setProjectId(long projectId) { + this.projectId = projectId; + } + + public void setUserId(long userId) { + this.userId = userId; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public void setHours(int hours) { + this.hours = hours; + } } diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java index 2019b3d52..eb4589f31 100644 --- a/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryController.java @@ -1,50 +1,66 @@ package io.pivotal.pal.tracker; +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.GaugeService; 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; + private final CounterService counter; + private final GaugeService gauge; - public TimeEntryController(TimeEntryRepository timeEntryRepository) { + public TimeEntryController(TimeEntryRepository timeEntryRepository, CounterService counter, GaugeService gauge + ) { this.timeEntryRepository = timeEntryRepository; + this.counter = counter; + this.gauge = gauge; } - @PostMapping("/time-entries") + @PostMapping("") public ResponseEntity create(@RequestBody TimeEntry timeEntry) { - return ResponseEntity.status(201).body(this.timeEntryRepository.create(timeEntry)); + TimeEntry entry = this.timeEntryRepository.create(timeEntry); + counter.increment("TimeEntry.created"); + gauge.submit("timeEntries.count", timeEntryRepository.list().size()); + return ResponseEntity.status(201).body(entry); } - @GetMapping("/time-entries/{id}") + @GetMapping("{id}") public ResponseEntity read(@PathVariable long id) { TimeEntry timeEntry = this.timeEntryRepository.find(id); if (timeEntry != null) { + counter.increment("TimeEntry.read"); return ResponseEntity.ok(timeEntry); } return ResponseEntity.status(404).body(null); } - @GetMapping("/time-entries") + @GetMapping("") public ResponseEntity> list() { + counter.increment("TimeEntry.listed"); return ResponseEntity.ok(this.timeEntryRepository.list()); } - @PutMapping("/time-entries/{id}") + @PutMapping("{id}") public ResponseEntity update(@PathVariable long id, @RequestBody TimeEntry timeEntry) { TimeEntry update = this.timeEntryRepository.update(id, timeEntry); if (update != null) { + counter.increment("TimeEntry.updated"); return ResponseEntity.ok(update); } return ResponseEntity.status(404).body(null); } - @RequestMapping(method = RequestMethod.DELETE, path = "/time-entries/{id}") + @RequestMapping(method = RequestMethod.DELETE, path = "{id}") public ResponseEntity delete(@PathVariable long id) { this.timeEntryRepository.delete(id); - return ResponseEntity.status(204).body(null); + counter.increment("TimeEntry.deleted"); + gauge.submit("timeEntries.count", timeEntryRepository.list().size()); + return ResponseEntity.status(204).body(null); } } 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..7d942d1a4 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryHealthIndicator.java @@ -0,0 +1,27 @@ +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(); + }} diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryJPAController.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryJPAController.java new file mode 100644 index 000000000..879df7d6d --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryJPAController.java @@ -0,0 +1,64 @@ +package io.pivotal.pal.tracker; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; + +@RestController +@RequestMapping("/time-entries-jpa") +public class TimeEntryJPAController { + private TimeEntryJPARepository timeEntryRepository; + + public TimeEntryJPAController(TimeEntryJPARepository timeEntryRepository) { + this.timeEntryRepository = timeEntryRepository; + } + + @PostMapping + public ResponseEntity create(@RequestBody TimeEntry timeEntry) { + timeEntryRepository.save(timeEntry); + return ResponseEntity.status(201).body(timeEntry); + } + + @GetMapping("{id}") + public ResponseEntity read(@PathVariable long id) { + TimeEntry timeEntry = this.timeEntryRepository.findOne(id); + if (timeEntry != null) { + return ResponseEntity.ok(timeEntry); + } + return ResponseEntity.status(404).body(null); + } + + @GetMapping + public ResponseEntity> list() { + Iterable all = this.timeEntryRepository.findAll(); + List list = new ArrayList<>(); + all.forEach(a -> list.add(a)); + return ResponseEntity.ok(list); + } + + @PutMapping("{id}") + public ResponseEntity update(@PathVariable long id, @RequestBody TimeEntry timeEntry) { + TimeEntry update = this.timeEntryRepository.findOne(id); + if (update != null) { + update.setDate(timeEntry.getDate()); + update.setHours(timeEntry.getHours()); + update.setProjectId(timeEntry.getProjectId()); + update.setUserId(timeEntry.getUserId()); + timeEntryRepository.save(update); + return ResponseEntity.ok(update); + } + return ResponseEntity.status(404).body(null); + } + + @RequestMapping(method = RequestMethod.DELETE, path = "{id}") + public ResponseEntity delete(@PathVariable long id) { + TimeEntry update = this.timeEntryRepository.findOne(id); + if (update != null) { + this.timeEntryRepository.delete(update); + } + return ResponseEntity.status(204).body(null); + } + +} diff --git a/src/main/java/io/pivotal/pal/tracker/TimeEntryJPARepository.java b/src/main/java/io/pivotal/pal/tracker/TimeEntryJPARepository.java new file mode 100644 index 000000000..7411a5288 --- /dev/null +++ b/src/main/java/io/pivotal/pal/tracker/TimeEntryJPARepository.java @@ -0,0 +1,8 @@ +package io.pivotal.pal.tracker; + +import org.springframework.data.repository.CrudRepository; + +import java.util.List; + +public interface TimeEntryJPARepository extends CrudRepository{ +} 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..904894e64 --- /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("TRUNCATE 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 2013c28ec..5eac64f02 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; @@ -16,6 +18,7 @@ import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; public class TimeEntryControllerTest { private TimeEntryRepository timeEntryRepository; @@ -24,7 +27,7 @@ public class TimeEntryControllerTest { @Before public void setUp() throws Exception { timeEntryRepository = mock(TimeEntryRepository.class); - controller = new TimeEntryController(timeEntryRepository); + controller = new TimeEntryController(timeEntryRepository, mock(CounterService.class), mock(GaugeService.class)); } @Test 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..a57db04ea --- /dev/null +++ b/src/test/java/test/pivotal/pal/trackerapi/HealthApiTest.java @@ -0,0 +1,51 @@ +package test.pivotal.pal.trackerapi; + +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; + +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 { + @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 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..42ba4dcfd 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryApiTest.java @@ -1,21 +1,27 @@ 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; @@ -24,12 +30,30 @@ @RunWith(SpringRunner.class) @SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) public class TimeEntryApiTest { + @LocalServerPort + private String port; - @Autowired private TestRestTemplate restTemplate; private TimeEntry timeEntry = new TimeEntry(123L, 456L, LocalDate.parse("2017-01-08"), 8); + @Before + public void setUp() throws Exception { + RestTemplateBuilder builder = new RestTemplateBuilder() + .rootUri("http://localhost:" + port) + .basicAuthorization("user", "password"); + + restTemplate = new TestRestTemplate(builder); + + 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); diff --git a/src/test/java/test/pivotal/pal/trackerapi/TimeEntryJPAApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryJPAApiTest.java new file mode 100644 index 000000000..f55577fc6 --- /dev/null +++ b/src/test/java/test/pivotal/pal/trackerapi/TimeEntryJPAApiTest.java @@ -0,0 +1,150 @@ +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 TimeEntryJPAApiTest { + @LocalServerPort + private String port; + + private TestRestTemplate restTemplate; + + private TimeEntry timeEntry = new TimeEntry(123L, 456L, LocalDate.parse("2017-01-08"), 8); + + @Before + public void setUp() throws Exception { + RestTemplateBuilder builder = new RestTemplateBuilder() + .rootUri("http://localhost:" + port) + .basicAuthorization("user", "password"); + + restTemplate = new TestRestTemplate(builder); + + 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-jpa", 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-jpa", 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-jpa/" + 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-jpa/" + 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-jpa/" + id, HttpMethod.DELETE, null, String.class); + + + assertThat(deleteResponse.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + + ResponseEntity deletedReadResponse = this.restTemplate.getForEntity("/time-entries-jpa/" + id, String.class); + assertThat(deletedReadResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + private Long createTimeEntry() { + HttpEntity entity = new HttpEntity<>(timeEntry); + + ResponseEntity response = restTemplate.exchange("/time-entries-jpa", HttpMethod.POST, entity, TimeEntry.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + + return response.getBody().getId(); + } +} diff --git a/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java index cc7091ed4..19b6984c7 100644 --- a/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java +++ b/src/test/java/test/pivotal/pal/trackerapi/WelcomeApiTest.java @@ -1,11 +1,14 @@ package test.pivotal.pal.trackerapi; import io.pivotal.pal.tracker.PalTrackerApplication; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.LocalServerPort; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.test.context.junit4.SpringRunner; import static org.assertj.core.api.Assertions.assertThat; @@ -15,9 +18,18 @@ @SpringBootTest(classes = PalTrackerApplication.class, webEnvironment = RANDOM_PORT) public class WelcomeApiTest { - @Autowired private TestRestTemplate restTemplate; + @LocalServerPort + private String port; + @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);