From cb1153610dd9d4a64779f6349cac30ef165cc7e2 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Sun, 25 Mar 2018 22:06:46 +0100 Subject: [PATCH 01/21] Adding functionalilty required to expose the services, unit testing, working with spring security, docker image uses the jar and generate the service and docker compose to bring everything up. Still couple of things to improve but I hope this is woth as a MVP :) --- .gitignore | 102 +++++++++- Dockerfile | 7 + README.MD | 22 +++ build.gradle | 12 ++ docker-compose.yml | 21 ++ src/main/java/user/ApplicationError.java | 13 ++ src/main/java/user/DoubleStatistics.java | 59 ++++++ src/main/java/user/User.java | 49 ++++- src/main/java/user/UserController.java | 144 ++++++++++---- src/main/java/user/UserRepository.java | 20 ++ src/main/resources/application.properties | 25 +++ src/test/java/user/UserControllerTests.java | 208 +++++++++++++++++++- src/test/java/user/UserRepositoryTest.java | 45 +++++ 13 files changed, 667 insertions(+), 60 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 src/main/java/user/ApplicationError.java create mode 100644 src/main/java/user/DoubleStatistics.java create mode 100644 src/main/java/user/UserRepository.java create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/user/UserRepositoryTest.java diff --git a/.gitignore b/.gitignore index 015c1ec..5725525 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,70 @@ +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries +.idea/ + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Ruby plugin and RubyMine +/.rakeTasks + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Java ### # Compiled class file *.class @@ -21,15 +88,36 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* -# IntellijIDEA -.idea/ +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Gradle ### +.gradle +**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar -# Gradle stuff -.gradle/ -build/ -out/ -gradle/ +# Cache of project +.gradletasknamecache +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d0540b0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:8-jdk-alpine +EXPOSE 8080 +RUN mkdir -p /app/ +ADD build/libs/java-restful-test-0.1.0.jar /app/java-restful-test-0.1.0.jar +#ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"] +#ENTRYPOINT ["java", "-jar", "/app/java-restful-test-0.1.0.jar"] +ENTRYPOINT ["java", "-Dspring.data.mongodb.uri=mongodb://mongo_db:27017/users","-Djava.security.egd=file:/dev/./urandom","-jar","/app/java-restful-test-0.1.0.jar"] \ No newline at end of file diff --git a/README.MD b/README.MD index 9141782..9607a11 100644 --- a/README.MD +++ b/README.MD @@ -127,3 +127,25 @@ Publish your work in a GitHub repository. Feel free to modify this readme to giv If you need more than 1 day to do this, you might be overthinking, feel free to add improvement notes in your README file, show-off there, we prefer better quality code if it takes longer, but you must justify this. + +**Improvement Notes** +* Currently the application does not have profiles (dev, test, prod) to be able to deploy/test the application accordingly. +With the profile definitions will be good to modify port of the application to use, log level, mongo utl, etc. +* Also some load testing will be good maybe with jmeter to be able to see how much the application can scale or possible buttle necks. +* Profiling the application can be also important now that it is running. +* With the profiles in place, it would be handy to provide the specific profile as a property to the docker file and then to the spring boot application +this will allow the app to behave differently. +* Maybe some code conventions will be useful in big project to standarize things a bit, it brings benefits, obviously define pipelines to make sure code is always running, code reviews, etc. +* Also maybe to have the infrastructure in place for the docker hub to store images and be able to pull them constantly. + +**Answers** +* Answering the questions, I guess we will have a phone call, mut what I did was basically split the functionality in 3 main parts. +1. Create all possible combinations of the users. This allows me to have in memory all different combinations of User -> User to I can calculate the distance +2. Iterate the previous Set of pairs to calculate the distance and store the distance result +3. Based on the distance now I can do all the simple maths for the average, min, max, etc. + +This approach allows me for example to use some sort of map reduce mechanism, where I can for instance, generate an independent micro service to calculare the distance (assuming this is the most CPU intensive calculation), then I can use the results from 1 and invoke that service, since that service is independent, I can scale that up as much as I can to parelalize things and split the load among multiple machines if needed, then collect all the results and reduce that to be able to do the average, min, max, etc. + + +* Normally I dont focus on 100% Test coverage, not really useful to cover setters and getters for instance unless they do something specific. +I rather focus on business logic coverage as high as possible. Saying that, I am totally flexible, if the company requires 100% code coverage I would not have any issue with that. diff --git a/build.gradle b/build.gradle index e711d1c..456dcfc 100644 --- a/build.gradle +++ b/build.gradle @@ -18,6 +18,9 @@ bootJar { version = '0.1.0' } + + + repositories { mavenCentral() } @@ -26,7 +29,16 @@ sourceCompatibility = 1.8 targetCompatibility = 1.8 dependencies { + compile("org.springframework.boot:spring-boot-devtools") + compile('org.springframework.boot:spring-boot-starter-actuator') compile("org.springframework.boot:spring-boot-starter-web") + compile("org.springframework.boot:spring-boot-starter-data-rest") + compile("org.springframework.boot:spring-boot-starter-data-mongodb") + compile("com.google.guava:guava:24.1-jre") + compile("org.gavaghan:geodesy:1.1.3") + + compile("org.springframework.boot:spring-boot-starter-security") + testCompile("org.springframework.security:spring-security-test:4.0.0.RELEASE") testCompile('org.springframework.boot:spring-boot-starter-test') testCompile('com.jayway.jsonpath:json-path') } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..509c901 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.1' +services: + sprinbboot_ws: + build: ./ + ports: + - "8080:8080" + depends_on: + - mongo_db + links: + - mongo_db + restart: always + environment: + SPRING_DATA_MONGODB_URI: mongodb://mongo_db/users + mongo_db: + image: "mongo" + container_name: mongo_db + restart: always +# volumes: +# - ./data:/data/db + ports: + - "27017:27017" \ No newline at end of file diff --git a/src/main/java/user/ApplicationError.java b/src/main/java/user/ApplicationError.java new file mode 100644 index 0000000..94cd20a --- /dev/null +++ b/src/main/java/user/ApplicationError.java @@ -0,0 +1,13 @@ +package user; + +public class ApplicationError { + private String error; + + public ApplicationError(String err){ + this.error = err; + } + + public String getError() { + return error; + } +} diff --git a/src/main/java/user/DoubleStatistics.java b/src/main/java/user/DoubleStatistics.java new file mode 100644 index 0000000..e461906 --- /dev/null +++ b/src/main/java/user/DoubleStatistics.java @@ -0,0 +1,59 @@ +package user; + +import java.util.DoubleSummaryStatistics; +import java.util.stream.Collector; + +/** + * Not the author of this class. It seems there was a proposal to integrate this in the DoubleSummaryStatistics as part of jdk + * but it was decided not do it.I was using the standard then I discovered it did not have std deviation. + */ +public class DoubleStatistics extends DoubleSummaryStatistics { + + private double sumOfSquare = 0.0d; + private double sumOfSquareCompensation; // Low order bits of sum + private double simpleSumOfSquare; // Used to compute right sum for + // non-finite inputs + + @Override + public void accept(double value) { + super.accept(value); + double squareValue = value * value; + simpleSumOfSquare += squareValue; + sumOfSquareWithCompensation(squareValue); + } + + public DoubleStatistics combine(DoubleStatistics other) { + super.combine(other); + simpleSumOfSquare += other.simpleSumOfSquare; + sumOfSquareWithCompensation(other.sumOfSquare); + sumOfSquareWithCompensation(other.sumOfSquareCompensation); + return this; + } + + private void sumOfSquareWithCompensation(double value) { + double tmp = value - sumOfSquareCompensation; + double velvel = sumOfSquare + tmp; // Little wolf of rounding error + sumOfSquareCompensation = (velvel - sumOfSquare) - tmp; + sumOfSquare = velvel; + } + + public double getSumOfSquare() { + double tmp = sumOfSquare + sumOfSquareCompensation; + if (Double.isNaN(tmp) && Double.isInfinite(simpleSumOfSquare)) { + return simpleSumOfSquare; + } + return tmp; + } + + public final double getStandardDeviation() { + long count = getCount(); + double sumOfSquare = getSumOfSquare(); + double average = getAverage(); + return count > 0 ? Math.sqrt((sumOfSquare - count * Math.pow(average, 2)) / (count - 1)) : 0.0d; + } + + public static Collector collector() { + return Collector.of(DoubleStatistics::new, DoubleStatistics::accept, DoubleStatistics::combine); + } + +} \ No newline at end of file diff --git a/src/main/java/user/User.java b/src/main/java/user/User.java index f49d789..593743b 100644 --- a/src/main/java/user/User.java +++ b/src/main/java/user/User.java @@ -1,25 +1,35 @@ package user; -import java.nio.DoubleBuffer; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document(collection = "users") public class User { - private final long id; + @Id + private int userId; private String firstName; private String lastName; private Double latitude; private Double longitude; - public User(long id, String firstName, String lastName, Double latitude, Double longitude) { - this.id = id; + public User(){ + } + + public User(String firstName, String lastName, Double latitude, Double longitude) { this.firstName = firstName; this.lastName = lastName; this.latitude = latitude; this.longitude = longitude; } - public long getId() { - return id; + public User(int userId, String firstName, String lastName, Double latitude, Double longitude) { + this.userId = userId; + this.firstName = firstName; + this.lastName = lastName; + this.latitude = latitude; + this.longitude = longitude; } public String getFirstName() { return this.firstName; } @@ -27,4 +37,31 @@ public long getId() { public Double getLatitude() { return this.latitude; } public Double getLongitude() { return this.longitude; } + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public void setLatitude(Double latitude) { + this.latitude = latitude; + } + + public void setLongitude(Double longitude) { + this.longitude = longitude; + } + + @Override + public String toString() { + return this.firstName; + } } diff --git a/src/main/java/user/UserController.java b/src/main/java/user/UserController.java index f58cad4..1d13e25 100644 --- a/src/main/java/user/UserController.java +++ b/src/main/java/user/UserController.java @@ -1,71 +1,137 @@ package user; -import java.util.Collection; -import java.util.concurrent.atomic.AtomicLong; - +import com.google.common.collect.Sets; +import org.gavaghan.geodesy.Ellipsoid; +import org.gavaghan.geodesy.GeodeticCalculator; +import org.gavaghan.geodesy.GlobalPosition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.web.bind.annotation.*; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.validation.Valid; +import java.util.*; + @RestController -public class UserController { +public class UserController extends WebSecurityConfigurerAdapter { + private GeodeticCalculator geoCalc = new GeodeticCalculator(); + private Ellipsoid reference = Ellipsoid.WGS84; + + @Override + protected void configure (HttpSecurity http) throws Exception { + http.csrf().disable(); + } + + @Autowired + UserRepository userRepository; @RequestMapping(method = RequestMethod.GET, value="/jrt/api/v1.0/users") - public Collection users() { - /** - * Update this to return a json stream defining a listing of the users - * Note: Always return the appropriate response for the action requested. - * - */ - //TODO: Implement this - return null; + public ResponseEntity> users() { + Collection users = userRepository.findAll(); + if (users.isEmpty()) { + return new ResponseEntity(users, HttpStatus.NO_CONTENT); + } + return new ResponseEntity>(users, HttpStatus.OK); } - @RequestMapping(method = RequestMethod.GET, value="/jrt/api/v1.0/user") + @RequestMapping(method = RequestMethod.GET, value="/jrt/api/v1.0/user/{userId}") public ResponseEntity get_user(@PathVariable String userId) { - //TODO: Implement this - return null; + User user = userRepository.findUserByUserId(Integer.valueOf(userId)); + if (null == user) { + return new ResponseEntity(new ApplicationError("User with id " + userId + + " not found"), HttpStatus.NOT_FOUND); + } + return new ResponseEntity(user, HttpStatus.OK); } @RequestMapping(method = RequestMethod.POST, value="/jrt/api/v1.0/user") - public ResponseEntity add_user(@RequestBody User input) { - /** - * Should add a new user to the users collection, with validation - * note: Always return the appropriate response for the action requested. - */ - //TODO: Implement this - return null; + public ResponseEntity add_user(@Valid @RequestBody User input) { + User userFound = userRepository.findUserByUserId(input.getUserId()); + if(userFound != null){ + return new ResponseEntity(new ApplicationError("Unable to create user. Record already exist"),HttpStatus.CONFLICT); + } + User user = userRepository.save(input); + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(UriComponentsBuilder.fromPath("/jrt/api/v1.0/user/{id}").buildAndExpand(user.getUserId()).toUri()); + return new ResponseEntity(headers, HttpStatus.CREATED); } - @RequestMapping(method = RequestMethod.PUT, value="/jrt/api/v1.0/user") + @RequestMapping(method = RequestMethod.PUT, value="/jrt/api/v1.0/user/{userId}") public ResponseEntity update_user(@PathVariable String userId, @RequestBody User input) { - /** - * Update user specified with user ID and return updated user contents - * Note: Always return the appropriate response for the action requested. - */ - //TODO: Implement this - return null; + User user = userRepository.findUserByUserId(Integer.valueOf(userId)); + if (null == user) { + return new ResponseEntity(new ApplicationError("User not found."),HttpStatus.NOT_FOUND); + } + if(input.getFirstName() != null){ + user.setFirstName(input.getFirstName()); + } + if(input.getLastName() != null){ + user.setLastName(input.getLastName()); + } + if(input.getLatitude() != null){ + user.setLatitude(input.getLatitude()); + } + if(input.getLongitude() != null){ + user.setLongitude(input.getLongitude()); + } + User updatedUser = userRepository.save(user); + return new ResponseEntity(updatedUser, HttpStatus.OK); } - @RequestMapping(method = RequestMethod.DELETE, value="/jrt/api/v1.0/user") + @RequestMapping(method = RequestMethod.DELETE, value="/jrt/api/v1.0/user/{userId}") public ResponseEntity delete_user(@PathVariable String userId) { - /** - * Delete user specified with user ID and return updated user contents - * Note: Always return the appropriate response for the action requested. - */ - //TODO: Implement this - return null; + User user = userRepository.findUserByUserId(Integer.valueOf(userId)); + if (null == user) { + return new ResponseEntity(new ApplicationError("User not found."),HttpStatus.NOT_FOUND); + } + userRepository.delete(user); + return new ResponseEntity(user, HttpStatus.OK); + } + + @RequestMapping(method = RequestMethod.DELETE, value="/jrt/api/v1.0/user") + public ResponseEntity delete_all() { + userRepository.deleteAll(); + return new ResponseEntity(HttpStatus.OK); } + /** + * Distance verified manually using https://www.movable-type.co.uk/scripts/latlong.html + */ @RequestMapping(method = RequestMethod.GET, value="/jrt/api/v1.0/distances") - public String distances() { + public ResponseEntity distances() { /** * Each user has a lat/lon associated with them. Determine the distance * between each user pair, and provide the min/max/average/std as a json response. * This should be GET only. * */ - //TODO: Implement this - return null; + double min=Double.MAX_VALUE, max=Double.MIN_VALUE, average= 0.0, std = 0.0; + + Map distances = new HashMap<>(); + Set set = new HashSet(userRepository.findAll()); + Sets.combinations(set, 2).forEach(p -> { + List list = new ArrayList(p); + User userA = list.get(0); + User userB = list.get(1); + GlobalPosition pointA = new GlobalPosition(userA.getLatitude(), userA.getLongitude(), 0.0); + GlobalPosition pointB = new GlobalPosition(userB.getLatitude(), userB.getLongitude(), 0.0); + double distance = geoCalc.calculateGeodeticCurve(reference, pointB, pointA).getEllipsoidalDistance(); + + StringBuilder sb = new StringBuilder(); + sb.append(userA.getUserId()).append("-").append(userB.getUserId()); + distances.put(sb.toString(), distance); + }); + + DoubleStatistics stats = distances.values().stream().collect( + DoubleStatistics.collector()); + + return new ResponseEntity(stats, HttpStatus.OK); } + } diff --git a/src/main/java/user/UserRepository.java b/src/main/java/user/UserRepository.java new file mode 100644 index 0000000..d907e1f --- /dev/null +++ b/src/main/java/user/UserRepository.java @@ -0,0 +1,20 @@ +package user; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + + +@Repository +public interface UserRepository extends MongoRepository { + @Query("{ 'userId' : ?0 }") + User findUserByUserId(int userId); + + long countByLastName(String lastName); + + @Query(value="{ 'firstName' : ?0 }", fields="{ 'firstName' : 1, 'lastName' : 1}") + List findByTheUserFirstName(String firstName); +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..da3f01e --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,25 @@ +#Mongo +spring.data.mongodb.database=users +spring.data.mongodb.host=mongo_db +spring.data.mongodb.port=27017 + +server.port=8080 +spring.application.name=users + +#loging +logging.level.org.springframework.web=INFO +logging.file=users.log + + +management.security.enabled=false +management.endpoints.web.expose=* + +#management.security.enabled=false +#security.basic.enabled=false + +management.endpoints.web.exposure.include=* +spring.security.user.name=user +spring.security.user.password=password +spring.security.user.roles=USER + + diff --git a/src/test/java/user/UserControllerTests.java b/src/test/java/user/UserControllerTests.java index 635f706..a17f6bb 100644 --- a/src/test/java/user/UserControllerTests.java +++ b/src/test/java/user/UserControllerTests.java @@ -3,6 +3,7 @@ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. + * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -15,19 +16,37 @@ */ package user; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.json.JacksonTester; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.number.IsCloseTo.closeTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc @@ -36,10 +55,183 @@ public class UserControllerTests { @Autowired private MockMvc mockMvc; + @Autowired + ObjectMapper objectMapper; + + @Autowired + private UserController userController; + + @MockBean + private UserRepository userRepository; + + private JacksonTester jsonTester; + + @Before + public void setup() { + JacksonTester.initFields(this, objectMapper); + } + + @After + public void cleanup(){ + userController.delete_all(); + } + + @Test + @WithMockUser(username = "user", password = "password", roles = "USER") + public void getNonExistentUser() throws Exception { + given(userRepository.findUserByUserId(1)).willReturn(null); + mockMvc.perform(get("/jrt/api/v1.0/user/{userId}", 1)) + .andExpect(status().is(HttpStatus.NOT_FOUND.value())); + } + @Test - public void someUnitTest() throws Exception { - //TODO: Do something meaninful here - assert(true); + @WithMockUser(username = "user", password = "password", roles = "USER") + public void getExistentUser() throws Exception { + User user = new User(5, "Carlos", "Patino", 53.4239330, -7.9406900); + given(userRepository.findUserByUserId(1)).willReturn(user); + mockMvc.perform(get("/jrt/api/v1.0/user/{userId}", 1)) + .andExpect(status().is(HttpStatus.OK.value())) + .andExpect(jsonPath("$.firstName", is(user.getFirstName()))) + .andExpect(jsonPath("$.lastName", is(user.getLastName()))) + .andExpect(jsonPath("$.latitude", is(user.getLatitude()))) + .andExpect(jsonPath("$.longitude", is(user.getLongitude()))); } + @Test + @WithMockUser(username = "user", password = "password", roles = "USER") + public void addUserWhenPreviousUserAlreadyExists() throws Exception { + User user = new User(5, "Carlos", "Patino", 53.4239330, -7.9406900); + final String userJson = jsonTester.write(user).getJson(); + + given(userRepository.findUserByUserId(user.getUserId())).willReturn(user); + mockMvc.perform(post("/jrt/api/v1.0/user") + .contentType(APPLICATION_JSON) + .content(userJson)) + .andExpect(status().is(HttpStatus.CONFLICT.value())); + } + + @Test + @WithMockUser(username = "user", password = "password", roles = "USER") + public void addUserSuccessfully() throws Exception { + User user = new User(5, "Carlos", "Patino", 53.4239330, -7.9406900); + final String userJson = jsonTester.write(user).getJson(); + + given(userRepository.findUserByUserId(user.getUserId())).willReturn(null); + given(userRepository.save(any(User.class))).willReturn(user); + mockMvc.perform(post("/jrt/api/v1.0/user") + .contentType(APPLICATION_JSON) + .content(userJson)) + .andExpect(status().is(HttpStatus.CREATED.value())); + } + + @Test + @WithMockUser(username = "user", password = "password", roles = "USER") + public void updateUserSuccessfully() throws Exception { + User user = new User(5, "Carlos", "Patino", 53.4239330, -7.9406900); + User userUpdated = new User(5, "Luis", "Hernandez", 53.4239330, -7.9406900); + final String userJson = jsonTester.write(userUpdated).getJson(); + + given(userRepository.findUserByUserId(user.getUserId())).willReturn(user); + given(userRepository.save(any(User.class))).willReturn(user); + mockMvc.perform(put("/jrt/api/v1.0/user/{userId}", user.getUserId()) + .contentType(APPLICATION_JSON) + .content(userJson)) + .andExpect(status().is(HttpStatus.OK.value())) + .andExpect(jsonPath("$.firstName", is(userUpdated.getFirstName()))) + .andExpect(jsonPath("$.lastName", is(userUpdated.getLastName()))) + .andExpect(jsonPath("$.latitude", is(userUpdated.getLatitude()))) + .andExpect(jsonPath("$.longitude", is(userUpdated.getLongitude()))); + } + + @Test + @WithMockUser(username = "user", password = "password", roles = "USER") + public void updateNonExistentUser() throws Exception { + User user = new User(5, "Carlos", "Patino", 53.4239330, -7.9406900); + final String userJson = jsonTester.write(user).getJson(); + given(userRepository.findUserByUserId(user.getUserId())).willReturn(null); + mockMvc.perform(put("/jrt/api/v1.0/user/{userId}", user.getUserId()) + .contentType(APPLICATION_JSON).content(userJson)) + .andExpect(status().is(HttpStatus.NOT_FOUND.value())); + } + + + @Test + @WithMockUser(username = "user", password = "password", roles = "USER") + public void getUsers() throws Exception { + User user = new User(1, "Carlos", "Patino", 53.4239330, -7.9406900); + Collectionusers = singletonList(user); + + given(userRepository.findAll()).willReturn((List) users); + + mockMvc.perform(get("/jrt/api/v1.0/users") + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].firstName", is(user.getFirstName()))) + .andExpect(jsonPath("$[0].lastName", is(user.getLastName()))) + .andExpect(jsonPath("$[0].latitude", is(user.getLatitude()))) + .andExpect(jsonPath("$[0].longitude", is(user.getLongitude()))); + } + + @Test + @WithMockUser(username = "user", password = "password", roles = "USER") + public void getUsersEmpty() throws Exception { + Collectionusers = new ArrayList<>(); + given(userRepository.findAll()).willReturn((List) users); + + mockMvc.perform(get("/jrt/api/v1.0/users") + .contentType(APPLICATION_JSON)) + .andExpect(status().is(HttpStatus.NO_CONTENT.value())) + .andExpect(jsonPath("$", hasSize(0))); + } + + @Test + @WithMockUser(username = "user", password = "password", roles = "USER") + public void deleteUserSuccessfully() throws Exception { + User user = new User(5, "", "", 0.0, 0.0); + final String userJson = jsonTester.write(user).getJson(); + + given(userRepository.findUserByUserId(user.getUserId())).willReturn(user); + mockMvc.perform(delete("/jrt/api/v1.0/user/{userId}", user.getUserId()) + .contentType(APPLICATION_JSON) + .content(userJson)) + .andExpect(status().is(HttpStatus.OK.value())); + } + + @Test + @WithMockUser(username = "user", password = "password", roles = "USER") + public void deleteAllUsers() throws Exception { + mockMvc.perform(delete("/jrt/api/v1.0/user") + .contentType(APPLICATION_JSON)) + .andExpect(status().is(HttpStatus.OK.value())); + } + + @Test + @WithMockUser(username = "user", password = "password", roles = "USER") + public void deleteNonExistentUser() throws Exception { + User user = new User(5, "", "", 0.0, 0.0); + final String userJson = jsonTester.write(user).getJson(); + + given(userRepository.findUserByUserId(user.getUserId())).willReturn(null); + mockMvc.perform(delete("/jrt/api/v1.0/user/{userId}", user.getUserId()) + .contentType(APPLICATION_JSON).content(userJson)) + .andExpect(status().is(HttpStatus.NOT_FOUND.value())); + } + + @Test + @WithMockUser(username = "user", password = "password", roles = "USER") + public void getDistance() throws Exception { + User uno = new User(1, "Carlos", "", 53.421543, -7.942274); + User dos = new User(2, "Luis", "", 53.422303, -7.942306 ); + User tres = new User(3, "Pedro", "", 53.422287, -7.942070 ); + User cuatro = new User(4, "Jose", "", 53.422156, -7.942016 ); + List users = Arrays.asList(uno, dos, tres, cuatro); + given(userRepository.findAll()).willReturn(users); + mockMvc.perform(get("/jrt/api/v1.0/distances")) + .andExpect(status().is(HttpStatus.OK.value())) + .andExpect(jsonPath("$.sum", closeTo(294, 2))) + .andExpect(jsonPath("$.min", closeTo(15, 2))) + .andExpect(jsonPath("$.average", closeTo(49, 2))) + .andExpect(jsonPath("$.max", closeTo(84, 2))); + } } diff --git a/src/test/java/user/UserRepositoryTest.java b/src/test/java/user/UserRepositoryTest.java new file mode 100644 index 0000000..8266d21 --- /dev/null +++ b/src/test/java/user/UserRepositoryTest.java @@ -0,0 +1,45 @@ +package user; + +import org.junit.Before; +import org.junit.Ignore; +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.test.context.junit4.SpringRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(SpringRunner.class) +@SpringBootTest +@Ignore +// Test designed to verify operations against mongo. Will fail without mongo running and configuration updated. +public class UserRepositoryTest { + + @Autowired + private UserRepository repository; + + private User carlos, pedro, luis; + + @Before + public void setup(){ + repository.deleteAll(); + carlos = repository.save(new User(1, "Carlos", "Patino", 53.4239330, -7.9406900)); + pedro = repository.save(new User(2, "Pedro", "Rodriguez", 53.4239330, -7.9406900)); + luis = repository.save(new User(3, "Luis", "Garcia", 53.4239330, -7.9406900)); + } + + @Test + public void setsIdOnSave() { + User david = repository.save(new User("Dave", "Matthews", 53.4239330, -7.9406900)); + assertNotNull(david.getUserId()); + } + + @Test + public void findByUserId() { + User david = repository.save(new User(4, "David", "Martinez", 53.4239330, -7.9406900)); + User founded = repository.findUserByUserId(david.getUserId()); + assertEquals(david.getFirstName(), founded.getFirstName()); + } +} \ No newline at end of file From ca7373284967b283dc87c71f387b93b0bc127674 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Mon, 26 Mar 2018 11:00:01 +0100 Subject: [PATCH 02/21] Adding some validations and test for corner cases. --- .gitignore | 2 + build.gradle | 4 +- src/main/java/user/User.java | 20 ++++++++ src/main/java/user/UserController.java | 15 ++++-- src/main/resources/application.properties | 15 +++--- src/test/java/user/UserControllerTests.java | 55 ++++++++++++++++----- 6 files changed, 84 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index 5725525..5ecd4d8 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ .idea/**/dynamic.xml .idea/**/uiDesigner.xml +*.gz + # Gradle: .idea/**/gradle.xml .idea/**/libraries diff --git a/build.gradle b/build.gradle index 456dcfc..9859b4a 100644 --- a/build.gradle +++ b/build.gradle @@ -36,9 +36,9 @@ dependencies { compile("org.springframework.boot:spring-boot-starter-data-mongodb") compile("com.google.guava:guava:24.1-jre") compile("org.gavaghan:geodesy:1.1.3") - + compile("org.apache.commons:commons-lang3:3.7") compile("org.springframework.boot:spring-boot-starter-security") - testCompile("org.springframework.security:spring-security-test:4.0.0.RELEASE") + testCompile("org.springframework.security:spring-security-test") testCompile('org.springframework.boot:spring-boot-starter-test') testCompile('com.jayway.jsonpath:json-path') } diff --git a/src/main/java/user/User.java b/src/main/java/user/User.java index 593743b..f046207 100644 --- a/src/main/java/user/User.java +++ b/src/main/java/user/User.java @@ -3,15 +3,35 @@ import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; +import javax.validation.constraints.*; +import javax.validation.constraints.Size; @Document(collection = "users") public class User { + private static final int MAX_LENGTH_NAME = 30; + private static final int MIN_LENGTH_NAME = 2; + private static final int INTEGERS_IN_GEOPOSITION = 5; + private static final int DECIMALS_IN_GEOPOSITION = 20; + + @Id private int userId; + + @NotNull + @NotBlank + @Size(min=MIN_LENGTH_NAME, max=MAX_LENGTH_NAME) private String firstName; + + @NotNull + @NotBlank + @Size(min=MIN_LENGTH_NAME, max=MAX_LENGTH_NAME) private String lastName; + + @Digits(integer=INTEGERS_IN_GEOPOSITION,fraction=DECIMALS_IN_GEOPOSITION) private Double latitude; + + @Digits(integer=INTEGERS_IN_GEOPOSITION,fraction=DECIMALS_IN_GEOPOSITION) private Double longitude; public User(){ diff --git a/src/main/java/user/UserController.java b/src/main/java/user/UserController.java index 1d13e25..30ea274 100644 --- a/src/main/java/user/UserController.java +++ b/src/main/java/user/UserController.java @@ -1,6 +1,7 @@ package user; import com.google.common.collect.Sets; +import org.apache.commons.lang3.math.NumberUtils; import org.gavaghan.geodesy.Ellipsoid; import org.gavaghan.geodesy.GeodeticCalculator; import org.gavaghan.geodesy.GlobalPosition; @@ -22,10 +23,10 @@ public class UserController extends WebSecurityConfigurerAdapter { private GeodeticCalculator geoCalc = new GeodeticCalculator(); private Ellipsoid reference = Ellipsoid.WGS84; - @Override - protected void configure (HttpSecurity http) throws Exception { - http.csrf().disable(); - } +// @Override +// protected void configure (HttpSecurity http) throws Exception { +// http.csrf().disable(); +// } @Autowired UserRepository userRepository; @@ -41,6 +42,10 @@ public ResponseEntity> users() { @RequestMapping(method = RequestMethod.GET, value="/jrt/api/v1.0/user/{userId}") public ResponseEntity get_user(@PathVariable String userId) { + if (!NumberUtils.isCreatable(userId)) { + return new ResponseEntity(new ApplicationError("Invalid user id: " + userId + + "."), HttpStatus.BAD_REQUEST); + } User user = userRepository.findUserByUserId(Integer.valueOf(userId)); if (null == user) { return new ResponseEntity(new ApplicationError("User with id " + userId @@ -57,7 +62,7 @@ public ResponseEntity add_user(@Valid @RequestBody User input) { } User user = userRepository.save(input); HttpHeaders headers = new HttpHeaders(); - headers.setLocation(UriComponentsBuilder.fromPath("/jrt/api/v1.0/user/{id}").buildAndExpand(user.getUserId()).toUri()); + headers.setLocation(UriComponentsBuilder.fromPath("/jrt/api/v1.0/user/{userId}").buildAndExpand(user.getUserId()).toUri()); return new ResponseEntity(headers, HttpStatus.CREATED); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index da3f01e..b681e31 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,6 +1,7 @@ #Mongo spring.data.mongodb.database=users -spring.data.mongodb.host=mongo_db +#spring.data.mongodb.host=mongo_db +spring.data.mongodb.host=localhost spring.data.mongodb.port=27017 server.port=8080 @@ -10,16 +11,14 @@ spring.application.name=users logging.level.org.springframework.web=INFO logging.file=users.log +management.security.enabled=true +security.basic.enabled=true -management.security.enabled=false -management.endpoints.web.expose=* - -#management.security.enabled=false -#security.basic.enabled=false - -management.endpoints.web.exposure.include=* +#management.endpoints.web.expose=* +#management.endpoints.web.exposure.include=* spring.security.user.name=user spring.security.user.password=password spring.security.user.roles=USER + diff --git a/src/test/java/user/UserControllerTests.java b/src/test/java/user/UserControllerTests.java index a17f6bb..61e22bd 100644 --- a/src/test/java/user/UserControllerTests.java +++ b/src/test/java/user/UserControllerTests.java @@ -43,6 +43,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -76,20 +77,36 @@ public void cleanup(){ userController.delete_all(); } + @Test + public void getUserWithAnNonAuthorizedConsumer() throws Exception { + given(userRepository.findUserByUserId(1)).willReturn(null); + mockMvc.perform(get("/jrt/api/v1.0/users")) + .andExpect(status().is(HttpStatus.FOUND.value())); + } + @Test @WithMockUser(username = "user", password = "password", roles = "USER") public void getNonExistentUser() throws Exception { given(userRepository.findUserByUserId(1)).willReturn(null); - mockMvc.perform(get("/jrt/api/v1.0/user/{userId}", 1)) + mockMvc.perform(get("/jrt/api/v1.0/user/{userId}", 1).with(csrf())) .andExpect(status().is(HttpStatus.NOT_FOUND.value())); } + @Test + @WithMockUser(username = "user", password = "password", roles = "USER") + public void getUserInvalidIdFormat() throws Exception { + given(userRepository.findUserByUserId(1)).willReturn(null); + mockMvc.perform(get("/jrt/api/v1.0/user/{userId}", "X").with(csrf())) + .andExpect(status().is(HttpStatus.BAD_REQUEST.value())); + } + + @Test @WithMockUser(username = "user", password = "password", roles = "USER") public void getExistentUser() throws Exception { User user = new User(5, "Carlos", "Patino", 53.4239330, -7.9406900); given(userRepository.findUserByUserId(1)).willReturn(user); - mockMvc.perform(get("/jrt/api/v1.0/user/{userId}", 1)) + mockMvc.perform(get("/jrt/api/v1.0/user/{userId}", 1).with(csrf())) .andExpect(status().is(HttpStatus.OK.value())) .andExpect(jsonPath("$.firstName", is(user.getFirstName()))) .andExpect(jsonPath("$.lastName", is(user.getLastName()))) @@ -104,7 +121,7 @@ public void addUserWhenPreviousUserAlreadyExists() throws Exception { final String userJson = jsonTester.write(user).getJson(); given(userRepository.findUserByUserId(user.getUserId())).willReturn(user); - mockMvc.perform(post("/jrt/api/v1.0/user") + mockMvc.perform(post("/jrt/api/v1.0/user").with(csrf()) .contentType(APPLICATION_JSON) .content(userJson)) .andExpect(status().is(HttpStatus.CONFLICT.value())); @@ -118,12 +135,26 @@ public void addUserSuccessfully() throws Exception { given(userRepository.findUserByUserId(user.getUserId())).willReturn(null); given(userRepository.save(any(User.class))).willReturn(user); - mockMvc.perform(post("/jrt/api/v1.0/user") + mockMvc.perform(post("/jrt/api/v1.0/user").with(csrf()) .contentType(APPLICATION_JSON) .content(userJson)) .andExpect(status().is(HttpStatus.CREATED.value())); } + @Test + @WithMockUser(username = "user", password = "password", roles = "USER") + public void addUserInvalidDetails() throws Exception { + User user = new User(5, "Very long name that should fail since this is not a valid name otherwise this person will be in the guiness record", "Patino", 53.4239330, -7.9406900); + final String userJson = jsonTester.write(user).getJson(); + + given(userRepository.findUserByUserId(user.getUserId())).willReturn(null); + given(userRepository.save(any(User.class))).willReturn(user); + mockMvc.perform(post("/jrt/api/v1.0/user").with(csrf()) + .contentType(APPLICATION_JSON) + .content(userJson)) + .andExpect(status().is(HttpStatus.BAD_REQUEST.value())); + } + @Test @WithMockUser(username = "user", password = "password", roles = "USER") public void updateUserSuccessfully() throws Exception { @@ -133,7 +164,7 @@ public void updateUserSuccessfully() throws Exception { given(userRepository.findUserByUserId(user.getUserId())).willReturn(user); given(userRepository.save(any(User.class))).willReturn(user); - mockMvc.perform(put("/jrt/api/v1.0/user/{userId}", user.getUserId()) + mockMvc.perform(put("/jrt/api/v1.0/user/{userId}", user.getUserId()).with(csrf()) .contentType(APPLICATION_JSON) .content(userJson)) .andExpect(status().is(HttpStatus.OK.value())) @@ -149,7 +180,7 @@ public void updateNonExistentUser() throws Exception { User user = new User(5, "Carlos", "Patino", 53.4239330, -7.9406900); final String userJson = jsonTester.write(user).getJson(); given(userRepository.findUserByUserId(user.getUserId())).willReturn(null); - mockMvc.perform(put("/jrt/api/v1.0/user/{userId}", user.getUserId()) + mockMvc.perform(put("/jrt/api/v1.0/user/{userId}", user.getUserId()).with(csrf()) .contentType(APPLICATION_JSON).content(userJson)) .andExpect(status().is(HttpStatus.NOT_FOUND.value())); } @@ -163,7 +194,7 @@ public void getUsers() throws Exception { given(userRepository.findAll()).willReturn((List) users); - mockMvc.perform(get("/jrt/api/v1.0/users") + mockMvc.perform(get("/jrt/api/v1.0/users").with(csrf()) .contentType(APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$", hasSize(1))) @@ -179,7 +210,7 @@ public void getUsersEmpty() throws Exception { Collectionusers = new ArrayList<>(); given(userRepository.findAll()).willReturn((List) users); - mockMvc.perform(get("/jrt/api/v1.0/users") + mockMvc.perform(get("/jrt/api/v1.0/users").with(csrf()) .contentType(APPLICATION_JSON)) .andExpect(status().is(HttpStatus.NO_CONTENT.value())) .andExpect(jsonPath("$", hasSize(0))); @@ -192,7 +223,7 @@ public void deleteUserSuccessfully() throws Exception { final String userJson = jsonTester.write(user).getJson(); given(userRepository.findUserByUserId(user.getUserId())).willReturn(user); - mockMvc.perform(delete("/jrt/api/v1.0/user/{userId}", user.getUserId()) + mockMvc.perform(delete("/jrt/api/v1.0/user/{userId}", user.getUserId()).with(csrf()) .contentType(APPLICATION_JSON) .content(userJson)) .andExpect(status().is(HttpStatus.OK.value())); @@ -201,7 +232,7 @@ public void deleteUserSuccessfully() throws Exception { @Test @WithMockUser(username = "user", password = "password", roles = "USER") public void deleteAllUsers() throws Exception { - mockMvc.perform(delete("/jrt/api/v1.0/user") + mockMvc.perform(delete("/jrt/api/v1.0/user").with(csrf()) .contentType(APPLICATION_JSON)) .andExpect(status().is(HttpStatus.OK.value())); } @@ -213,7 +244,7 @@ public void deleteNonExistentUser() throws Exception { final String userJson = jsonTester.write(user).getJson(); given(userRepository.findUserByUserId(user.getUserId())).willReturn(null); - mockMvc.perform(delete("/jrt/api/v1.0/user/{userId}", user.getUserId()) + mockMvc.perform(delete("/jrt/api/v1.0/user/{userId}", user.getUserId()).with(csrf()) .contentType(APPLICATION_JSON).content(userJson)) .andExpect(status().is(HttpStatus.NOT_FOUND.value())); } @@ -227,7 +258,7 @@ public void getDistance() throws Exception { User cuatro = new User(4, "Jose", "", 53.422156, -7.942016 ); List users = Arrays.asList(uno, dos, tres, cuatro); given(userRepository.findAll()).willReturn(users); - mockMvc.perform(get("/jrt/api/v1.0/distances")) + mockMvc.perform(get("/jrt/api/v1.0/distances").with(csrf())) .andExpect(status().is(HttpStatus.OK.value())) .andExpect(jsonPath("$.sum", closeTo(294, 2))) .andExpect(jsonPath("$.min", closeTo(15, 2))) From 8d35ebb9613a69602696efd1bca66d1e4113a667 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Tue, 27 Mar 2018 15:51:17 +0100 Subject: [PATCH 03/21] Adding a bit of paralellism to the distance calculation. Some performance tunning may be needed to select the right threadpooling implementation --- src/main/java/user/PairOfIdsToDistance.java | 27 +++++++++++++++++++++ src/main/java/user/UserController.java | 20 +++++++-------- 2 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 src/main/java/user/PairOfIdsToDistance.java diff --git a/src/main/java/user/PairOfIdsToDistance.java b/src/main/java/user/PairOfIdsToDistance.java new file mode 100644 index 0000000..69eaf66 --- /dev/null +++ b/src/main/java/user/PairOfIdsToDistance.java @@ -0,0 +1,27 @@ +package user; + +public class PairOfIdsToDistance{ + private String fromToIds; + private Double distance; + + public PairOfIdsToDistance(String fromToIds, double distance) { + this.fromToIds = fromToIds; + this.distance = distance; + } + + public String getFromToIds() { + return fromToIds; + } + + public void setFromToIds(String fromToIds) { + this.fromToIds = fromToIds; + } + + public Double getDistance() { + return distance; + } + + public void setDistance(Double distance) { + this.distance = distance; + } +} diff --git a/src/main/java/user/UserController.java b/src/main/java/user/UserController.java index 30ea274..3c39664 100644 --- a/src/main/java/user/UserController.java +++ b/src/main/java/user/UserController.java @@ -16,6 +16,7 @@ import javax.validation.Valid; import java.util.*; +import java.util.stream.Collectors; @RestController @@ -106,7 +107,9 @@ public ResponseEntity delete_all() { /** - * Distance verified manually using https://www.movable-type.co.uk/scripts/latlong.html + * Distance verified manually using https://www.movable-type.co.uk/scripts/latlong.html. + * Some profiling or performance tunning may be needed here based on the architecture of the computer, + * number of cores, thread pool best suited for the use case we are running. */ @RequestMapping(method = RequestMethod.GET, value="/jrt/api/v1.0/distances") public ResponseEntity distances() { @@ -116,12 +119,10 @@ public ResponseEntity distances() { * This should be GET only. * */ - double min=Double.MAX_VALUE, max=Double.MIN_VALUE, average= 0.0, std = 0.0; - - Map distances = new HashMap<>(); Set set = new HashSet(userRepository.findAll()); - Sets.combinations(set, 2).forEach(p -> { - List list = new ArrayList(p); + + Map mapOfIdsAndDistances = Sets.combinations(set, 2).parallelStream().map(s -> { + List list = new ArrayList(s); User userA = list.get(0); User userB = list.get(1); GlobalPosition pointA = new GlobalPosition(userA.getLatitude(), userA.getLongitude(), 0.0); @@ -130,12 +131,11 @@ public ResponseEntity distances() { StringBuilder sb = new StringBuilder(); sb.append(userA.getUserId()).append("-").append(userB.getUserId()); - distances.put(sb.toString(), distance); - }); - DoubleStatistics stats = distances.values().stream().collect( - DoubleStatistics.collector()); + return new PairOfIdsToDistance(sb.toString(), distance); + }).collect(Collectors.toMap(PairOfIdsToDistance::getFromToIds, PairOfIdsToDistance::getDistance)); + DoubleStatistics stats = mapOfIdsAndDistances.values().stream().collect(DoubleStatistics.collector()); return new ResponseEntity(stats, HttpStatus.OK); } From c4fd6256cad6c96b07054294324bccc33f7b1730 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Tue, 27 Mar 2018 15:51:17 +0100 Subject: [PATCH 04/21] Adding a bit of paralellism to the distance calculation. Some performance tunning may be needed to select the right threadpooling implementation --- src/main/java/user/PairOfIdsToDistance.java | 27 +++++++++++++++++++++ src/main/java/user/UserController.java | 22 +++++++++-------- 2 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 src/main/java/user/PairOfIdsToDistance.java diff --git a/src/main/java/user/PairOfIdsToDistance.java b/src/main/java/user/PairOfIdsToDistance.java new file mode 100644 index 0000000..69eaf66 --- /dev/null +++ b/src/main/java/user/PairOfIdsToDistance.java @@ -0,0 +1,27 @@ +package user; + +public class PairOfIdsToDistance{ + private String fromToIds; + private Double distance; + + public PairOfIdsToDistance(String fromToIds, double distance) { + this.fromToIds = fromToIds; + this.distance = distance; + } + + public String getFromToIds() { + return fromToIds; + } + + public void setFromToIds(String fromToIds) { + this.fromToIds = fromToIds; + } + + public Double getDistance() { + return distance; + } + + public void setDistance(Double distance) { + this.distance = distance; + } +} diff --git a/src/main/java/user/UserController.java b/src/main/java/user/UserController.java index 30ea274..8177ec6 100644 --- a/src/main/java/user/UserController.java +++ b/src/main/java/user/UserController.java @@ -16,6 +16,7 @@ import javax.validation.Valid; import java.util.*; +import java.util.stream.Collectors; @RestController @@ -106,7 +107,9 @@ public ResponseEntity delete_all() { /** - * Distance verified manually using https://www.movable-type.co.uk/scripts/latlong.html + * Distance verified manually using https://www.movable-type.co.uk/scripts/latlong.html. + * Some profiling or performance tuning may be needed here based on the architecture of the computer, + * number of cores, thread pool best suited for the use case we are running. */ @RequestMapping(method = RequestMethod.GET, value="/jrt/api/v1.0/distances") public ResponseEntity distances() { @@ -116,12 +119,12 @@ public ResponseEntity distances() { * This should be GET only. * */ - double min=Double.MAX_VALUE, max=Double.MIN_VALUE, average= 0.0, std = 0.0; - - Map distances = new HashMap<>(); + // In a real case scenario you dont really map everything against everything, maybe the search can be narrowed per country, + // or per block and so on. Then we can optimize this call, instead of find all. Set set = new HashSet(userRepository.findAll()); - Sets.combinations(set, 2).forEach(p -> { - List list = new ArrayList(p); + + Map mapOfIdsAndDistances = Sets.combinations(set, 2).parallelStream().map(s -> { + List list = new ArrayList(s); User userA = list.get(0); User userB = list.get(1); GlobalPosition pointA = new GlobalPosition(userA.getLatitude(), userA.getLongitude(), 0.0); @@ -130,12 +133,11 @@ public ResponseEntity distances() { StringBuilder sb = new StringBuilder(); sb.append(userA.getUserId()).append("-").append(userB.getUserId()); - distances.put(sb.toString(), distance); - }); - DoubleStatistics stats = distances.values().stream().collect( - DoubleStatistics.collector()); + return new PairOfIdsToDistance(sb.toString(), distance); + }).collect(Collectors.toMap(PairOfIdsToDistance::getFromToIds, PairOfIdsToDistance::getDistance)); + DoubleStatistics stats = mapOfIdsAndDistances.values().stream().collect(DoubleStatistics.collector()); return new ResponseEntity(stats, HttpStatus.OK); } From a9ec9b550f98418c9887f97c9638373d4a9f0323 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Tue, 27 Mar 2018 16:07:37 +0100 Subject: [PATCH 05/21] Adding some rational behind the code --- src/main/java/user/UserController.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/user/UserController.java b/src/main/java/user/UserController.java index ff60228..e072d72 100644 --- a/src/main/java/user/UserController.java +++ b/src/main/java/user/UserController.java @@ -108,11 +108,7 @@ public ResponseEntity delete_all() { /** * Distance verified manually using https://www.movable-type.co.uk/scripts/latlong.html. -<<<<<<< HEAD * Some profiling or performance tuning may be needed here based on the architecture of the computer, -======= - * Some profiling or performance tunning may be needed here based on the architecture of the computer, ->>>>>>> 8d35ebb9613a69602696efd1bca66d1e4113a667 * number of cores, thread pool best suited for the use case we are running. */ @RequestMapping(method = RequestMethod.GET, value="/jrt/api/v1.0/distances") From 3db361d134de6571a6c646e806e8b7be68b21a51 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Tue, 27 Mar 2018 16:58:22 +0100 Subject: [PATCH 06/21] Adding the right name for the host and in the properties for docker compose --- src/main/resources/application.properties | 4 ++-- src/test/java/user/UserControllerTests.java | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b681e31..b5ca733 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,7 @@ #Mongo spring.data.mongodb.database=users -#spring.data.mongodb.host=mongo_db -spring.data.mongodb.host=localhost +spring.data.mongodb.host=mongo_db +#spring.data.mongodb.host=localhost spring.data.mongodb.port=27017 server.port=8080 diff --git a/src/test/java/user/UserControllerTests.java b/src/test/java/user/UserControllerTests.java index 61e22bd..e05877e 100644 --- a/src/test/java/user/UserControllerTests.java +++ b/src/test/java/user/UserControllerTests.java @@ -132,7 +132,6 @@ public void addUserWhenPreviousUserAlreadyExists() throws Exception { public void addUserSuccessfully() throws Exception { User user = new User(5, "Carlos", "Patino", 53.4239330, -7.9406900); final String userJson = jsonTester.write(user).getJson(); - given(userRepository.findUserByUserId(user.getUserId())).willReturn(null); given(userRepository.save(any(User.class))).willReturn(user); mockMvc.perform(post("/jrt/api/v1.0/user").with(csrf()) From 5d1dbf13f9eb011d9363cfe4af6541e77400e297 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Thu, 12 Apr 2018 17:39:56 +0100 Subject: [PATCH 07/21] Adding travis for CI --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..767b3dd --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: java +before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ \ No newline at end of file From 778330c11015352ccd498f47f9bbe38d7c1f7efe Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Thu, 12 Apr 2018 17:51:13 +0100 Subject: [PATCH 08/21] Adding the build status tag from travis --- README.MD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.MD b/README.MD index 9607a11..4b0837f 100644 --- a/README.MD +++ b/README.MD @@ -1,6 +1,6 @@ Welcome to the Java RESTful API test ==================================== - +[![Build Status](https://travis-ci.org/carlospatinos/Java_RESTful_test.svg?branch=master)](https://travis-ci.org/carlospatinos/Java_RESTful_test) The objetive of this test if to help us evalute your skills with: From c4e8a9b8b556331f32b96b7bcf3de03c4a43c813 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Fri, 13 Apr 2018 12:06:22 +0100 Subject: [PATCH 09/21] Adding test coverage to try it --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 767b3dd..f4f6cf6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,7 @@ before_cache: cache: directories: - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ \ No newline at end of file + - $HOME/.gradle/wrapper/ + +after_success: + - ./gradlew test jacocoTestReport coveralls \ No newline at end of file From f2abcb27b67b08ec97c845f4554b1f92010607b8 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Sat, 14 Apr 2018 17:58:51 +0100 Subject: [PATCH 10/21] Adding test coverage badge --- README.MD | 14 ++++++++------ build.gradle | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/README.MD b/README.MD index 4b0837f..7a2df6e 100644 --- a/README.MD +++ b/README.MD @@ -2,9 +2,13 @@ Welcome to the Java RESTful API test ==================================== [![Build Status](https://travis-ci.org/carlospatinos/Java_RESTful_test.svg?branch=master)](https://travis-ci.org/carlospatinos/Java_RESTful_test) -The objetive of this test if to help us evalute your skills with: +[![Coverage Status](https://coveralls.io/repos/github/carlospatinos/Java_RESTful_test/badge.svg?branch=master)](https://coveralls.io/github/carlospatinos/Java_RESTful_test?branch=master) -* Problem Solving +**Why?** + +The objetive of this project is: + +* Use some Problem Solving addressing scaling up scenarios * Web Server API Design * Request-time data manipulation * Testing strategies @@ -13,16 +17,14 @@ The objetive of this test if to help us evalute your skills with: **Instructions** * Fork the repo into a private repo. -* You will need Gradle +* Install Gradle * You can import the build.gradle file directly on your preferred IDE * Spring Boot should be used to complete the test, although, if you feel you want to use something different, feel free * Implement the required API endpoints -* Let us know when you have finished. **Tasks** -The idea here is for us to see how you design a minimalistic API. This API will be -used to perform CRUD operations on a model called User. +The idea here is for us to see how you design a minimalistic API. This API will be used to perform CRUD operations on a model called User. You're free to design this model as you want, but, at a minimum should have: diff --git a/build.gradle b/build.gradle index 9859b4a..1624b50 100644 --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,12 @@ buildscript { } } +plugins { + id 'jacoco' + id 'com.github.kt3k.coveralls' version '2.6.3' +} + +apply plugin: 'jacoco' apply plugin: 'java' apply plugin: 'eclipse' apply plugin: 'idea' @@ -18,8 +24,12 @@ bootJar { version = '0.1.0' } - - +jacocoTestReport { + reports { + xml.enabled = true // coveralls plugin depends on xml format report + html.enabled = true + } +} repositories { mavenCentral() @@ -38,6 +48,8 @@ dependencies { compile("org.gavaghan:geodesy:1.1.3") compile("org.apache.commons:commons-lang3:3.7") compile("org.springframework.boot:spring-boot-starter-security") + compile("de.flapdoodle.embed:de.flapdoodle.embed.mongo") + //testCompile("cz.jirutka.spring:embedmongo-spring:RELEASE") testCompile("org.springframework.security:spring-security-test") testCompile('org.springframework.boot:spring-boot-starter-test') testCompile('com.jayway.jsonpath:json-path') From 2c16f7f4c3aa6c453533f182bdd8d97c62a3739d Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Sat, 14 Apr 2018 18:03:33 +0100 Subject: [PATCH 11/21] Removing dependency to load mongo embedded --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1624b50..49dcf2f 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ dependencies { compile("org.gavaghan:geodesy:1.1.3") compile("org.apache.commons:commons-lang3:3.7") compile("org.springframework.boot:spring-boot-starter-security") - compile("de.flapdoodle.embed:de.flapdoodle.embed.mongo") + //compile("de.flapdoodle.embed:de.flapdoodle.embed.mongo") //testCompile("cz.jirutka.spring:embedmongo-spring:RELEASE") testCompile("org.springframework.security:spring-security-test") testCompile('org.springframework.boot:spring-boot-starter-test') From 22564a5b460dd91310ae3abbe5b375e04d06938d Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Sat, 14 Apr 2018 18:11:00 +0100 Subject: [PATCH 12/21] Using covertura instread of jacoco --- .travis.yml | 2 +- build.gradle | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index f4f6cf6..6a53159 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,4 +8,4 @@ cache: - $HOME/.gradle/wrapper/ after_success: - - ./gradlew test jacocoTestReport coveralls \ No newline at end of file + - ./gradlew cobertura coveralls \ No newline at end of file diff --git a/build.gradle b/build.gradle index 49dcf2f..eb05c25 100644 --- a/build.gradle +++ b/build.gradle @@ -8,8 +8,10 @@ buildscript { } plugins { - id 'jacoco' - id 'com.github.kt3k.coveralls' version '2.6.3' + //id 'jacoco' + //id 'com.github.kt3k.coveralls' version '2.6.3' + id 'net.saliman.cobertura' version '2.3.1' + id 'com.github.kt3k.coveralls' version '2.8.2' } apply plugin: 'jacoco' @@ -24,12 +26,14 @@ bootJar { version = '0.1.0' } -jacocoTestReport { - reports { - xml.enabled = true // coveralls plugin depends on xml format report - html.enabled = true - } -} +// jacocoTestReport { +// reports { +// xml.enabled = true // coveralls plugin depends on xml format report +// html.enabled = true +// } +// } + +cobertura.coverageFormats = ['html', 'xml'] repositories { mavenCentral() From 9cfd60ff947880ff8dc63cfb89ab1babac9c0e0e Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Sat, 14 Apr 2018 18:17:25 +0100 Subject: [PATCH 13/21] Still cobertura not working, returning to jacoco --- .travis.yml | 2 +- build.gradle | 20 ++++++++------------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6a53159..f4f6cf6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,4 +8,4 @@ cache: - $HOME/.gradle/wrapper/ after_success: - - ./gradlew cobertura coveralls \ No newline at end of file + - ./gradlew test jacocoTestReport coveralls \ No newline at end of file diff --git a/build.gradle b/build.gradle index eb05c25..49dcf2f 100644 --- a/build.gradle +++ b/build.gradle @@ -8,10 +8,8 @@ buildscript { } plugins { - //id 'jacoco' - //id 'com.github.kt3k.coveralls' version '2.6.3' - id 'net.saliman.cobertura' version '2.3.1' - id 'com.github.kt3k.coveralls' version '2.8.2' + id 'jacoco' + id 'com.github.kt3k.coveralls' version '2.6.3' } apply plugin: 'jacoco' @@ -26,14 +24,12 @@ bootJar { version = '0.1.0' } -// jacocoTestReport { -// reports { -// xml.enabled = true // coveralls plugin depends on xml format report -// html.enabled = true -// } -// } - -cobertura.coverageFormats = ['html', 'xml'] +jacocoTestReport { + reports { + xml.enabled = true // coveralls plugin depends on xml format report + html.enabled = true + } +} repositories { mavenCentral() From 9fc5059fce0dde3aaa5c9d388a6ea3b55ff720a8 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Sun, 15 Apr 2018 01:17:01 +0100 Subject: [PATCH 14/21] Adding information to the readme about docker-compose up --- .gitignore | 62 +++++++++++++++++++++ README.MD | 9 +++ src/main/java/user/UserController.java | 13 ++++- src/main/resources/application.properties | 2 +- src/test/java/user/UserControllerTests.java | 2 +- 5 files changed, 83 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 5ecd4d8..52960e0 100644 --- a/.gitignore +++ b/.gitignore @@ -123,3 +123,65 @@ gradle-app.setting +### Eclipse ### + +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +### Eclipse Patch ### +# Eclipse Core +.project + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Annotation Processing +.apt_generated + diff --git a/README.MD b/README.MD index 7a2df6e..e4d15e7 100644 --- a/README.MD +++ b/README.MD @@ -140,6 +140,15 @@ this will allow the app to behave differently. * Maybe some code conventions will be useful in big project to standarize things a bit, it brings benefits, obviously define pipelines to make sure code is always running, code reviews, etc. * Also maybe to have the infrastructure in place for the docker hub to store images and be able to pull them constantly. + +**How to run** +To run the code you have to execute. +```sh +docker-compose up +``` +This will start mongodb and this service in docker, then you can start sending requests. Then you can use postman or curl to call the end points (url with the format http://host:port). Default port is 8080 + + **Answers** * Answering the questions, I guess we will have a phone call, mut what I did was basically split the functionality in 3 main parts. 1. Create all possible combinations of the users. This allows me to have in memory all different combinations of User -> User to I can calculate the distance diff --git a/src/main/java/user/UserController.java b/src/main/java/user/UserController.java index e072d72..ab2ac7c 100644 --- a/src/main/java/user/UserController.java +++ b/src/main/java/user/UserController.java @@ -8,8 +8,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.web.bind.annotation.*; import org.springframework.web.util.UriComponentsBuilder; @@ -26,7 +26,11 @@ public class UserController extends WebSecurityConfigurerAdapter { // @Override // protected void configure (HttpSecurity http) throws Exception { -// http.csrf().disable(); +// //http.csrf().disable(); +// http.csrf().disable().authorizeRequests().anyRequest(). +// authenticated().and().formLogin().loginPage("/login"). +// permitAll().and().logout().deleteCookies("javarest"). +// permitAll().and().rememberMe().tokenValiditySeconds(60); // } @Autowired @@ -55,7 +59,7 @@ public ResponseEntity get_user(@PathVariable String userId) { return new ResponseEntity(user, HttpStatus.OK); } - @RequestMapping(method = RequestMethod.POST, value="/jrt/api/v1.0/user") + @RequestMapping(method = RequestMethod.POST, value="/jrt/api/v1.0/user", consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity add_user(@Valid @RequestBody User input) { User userFound = userRepository.findUserByUserId(input.getUserId()); if(userFound != null){ @@ -122,6 +126,9 @@ public ResponseEntity distances() { // In a real case scenario you dont really map everything against everything, maybe the search can be narrowed per country, // or per block and so on. Then we can optimize this call, instead of find all. Set set = new HashSet(userRepository.findAll()); + if(set == null || set.size() < 2) { + return new ResponseEntity(new ApplicationError("Not enough records to generate output"), HttpStatus.NOT_FOUND); + } Map mapOfIdsAndDistances = Sets.combinations(set, 2).parallelStream().map(s -> { List list = new ArrayList(s); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b5ca733..4a9b772 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,7 +8,7 @@ server.port=8080 spring.application.name=users #loging -logging.level.org.springframework.web=INFO +logging.level.org.springframework.web=debug logging.file=users.log management.security.enabled=true diff --git a/src/test/java/user/UserControllerTests.java b/src/test/java/user/UserControllerTests.java index e05877e..2ffa90e 100644 --- a/src/test/java/user/UserControllerTests.java +++ b/src/test/java/user/UserControllerTests.java @@ -35,6 +35,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import user.UserController; import static java.util.Collections.singletonList; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; @@ -79,7 +80,6 @@ public void cleanup(){ @Test public void getUserWithAnNonAuthorizedConsumer() throws Exception { - given(userRepository.findUserByUserId(1)).willReturn(null); mockMvc.perform(get("/jrt/api/v1.0/users")) .andExpect(status().is(HttpStatus.FOUND.value())); } From e214da42d8708a242cf75182dcad5afe543cbd47 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Sun, 15 Apr 2018 01:28:54 +0100 Subject: [PATCH 15/21] Adding postman files to improve verification --- postman/Java_Restful.postman_collection.json | 358 +++++++++++++++++++ src/main/java/user/UserController.java | 16 +- src/test/java/user/UserControllerTests.java | 2 +- 3 files changed, 367 insertions(+), 9 deletions(-) create mode 100644 postman/Java_Restful.postman_collection.json diff --git a/postman/Java_Restful.postman_collection.json b/postman/Java_Restful.postman_collection.json new file mode 100644 index 0000000..06bb52f --- /dev/null +++ b/postman/Java_Restful.postman_collection.json @@ -0,0 +1,358 @@ +{ + "info": { + "_postman_id": "9c57c8d9-bb02-4d3e-a80b-c1414f7bd3c2", + "name": "Java_Restful", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "login", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "user", + "type": "string" + }, + { + "key": "password", + "value": "password", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "_csrf", + "value": "{{_csrf}}" + }, + { + "key": "username", + "value": "user" + }, + { + "key": "password", + "value": "password" + } + ], + "body": {}, + "url": { + "raw": "{{url}}/login", + "host": [ + "{{url}}" + ], + "path": [ + "login" + ] + }, + "description": "Find all users" + }, + "response": [] + }, + { + "name": "find_users", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "user", + "type": "string" + }, + { + "key": "password", + "value": "password", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "_csrf", + "value": "{{_csrf}}" + } + ], + "body": {}, + "url": { + "raw": "{{url}}/jrt/api/v1.0/users", + "host": [ + "{{url}}" + ], + "path": [ + "jrt", + "api", + "v1.0", + "users" + ] + }, + "description": "Find all users" + }, + "response": [] + }, + { + "name": "find_distances", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "user", + "type": "string" + }, + { + "key": "password", + "value": "password", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "origin", + "value": "localhost:8080" + } + ], + "body": {}, + "url": { + "raw": "{{url}}/jrt/api/v1.0/distances", + "host": [ + "{{url}}" + ], + "path": [ + "jrt", + "api", + "v1.0", + "distances" + ] + }, + "description": "Find all users" + }, + "response": [] + }, + { + "name": "delete_users", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "user", + "type": "string" + }, + { + "key": "password", + "value": "password", + "type": "string" + } + ] + }, + "method": "DELETE", + "header": [ + { + "key": "origin", + "value": "http://localhost:8080", + "disabled": true + }, + { + "key": "_csrf", + "value": "{{_csrf}}", + "disabled": true + } + ], + "body": {}, + "url": { + "raw": "{{url}}/jrt/api/v1.0/user", + "host": [ + "{{url}}" + ], + "path": [ + "jrt", + "api", + "v1.0", + "user" + ] + }, + "description": "Find all users" + }, + "response": [] + }, + { + "name": "get_user", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "user", + "type": "string" + }, + { + "key": "password", + "value": "password", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "url": { + "raw": "{{url}}/jrt/api/v1.0/user/1", + "host": [ + "{{url}}" + ], + "path": [ + "jrt", + "api", + "v1.0", + "user", + "1" + ] + }, + "description": "Find all users" + }, + "response": [] + }, + { + "name": "add_user", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "user", + "type": "string" + }, + { + "key": "password", + "value": "password", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "_csrf", + "value": "9b15a3f3-db83-4db6-99cf-d899fcad01b9", + "disabled": true + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"userId\":1,\"firstName\":\"Luis\",\"lastName\":\"Patino\",\"latitude\":53.423933,\"longitude\":-7.94069}" + }, + "url": { + "raw": "{{url}}/jrt/api/v1.0/user", + "host": [ + "{{url}}" + ], + "path": [ + "jrt", + "api", + "v1.0", + "user" + ] + }, + "description": "Find all users" + }, + "response": [] + }, + { + "name": "update_user", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "user", + "type": "string" + }, + { + "key": "password", + "value": "password", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "_csrf", + "value": "9b15a3f3-db83-4db6-99cf-d899fcad01b9", + "disabled": true + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\"userId\":2,\"firstName\":\"Pedro\",\"lastName\":\"Patino\",\"latitude\":53.423933,\"longitude\":-7.94069}" + }, + "url": { + "raw": "{{url}}/jrt/api/v1.0/user/2", + "host": [ + "{{url}}" + ], + "path": [ + "jrt", + "api", + "v1.0", + "user", + "2" + ] + }, + "description": "Find all users" + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "id": "721af903-acd1-4002-a019-99f69a4f4bb3", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "9bbd3e27-8181-445e-b2ea-3f9f30380432", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "id": "44c22c6f-06d8-4a6d-a9b9-c853c1ac52ed", + "key": "url", + "value": "http://localhost:8081", + "type": "string", + "description": "" + } + ] +} \ No newline at end of file diff --git a/src/main/java/user/UserController.java b/src/main/java/user/UserController.java index ab2ac7c..9518ad9 100644 --- a/src/main/java/user/UserController.java +++ b/src/main/java/user/UserController.java @@ -10,6 +10,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.web.bind.annotation.*; import org.springframework.web.util.UriComponentsBuilder; @@ -24,14 +25,13 @@ public class UserController extends WebSecurityConfigurerAdapter { private GeodeticCalculator geoCalc = new GeodeticCalculator(); private Ellipsoid reference = Ellipsoid.WGS84; -// @Override -// protected void configure (HttpSecurity http) throws Exception { -// //http.csrf().disable(); -// http.csrf().disable().authorizeRequests().anyRequest(). -// authenticated().and().formLogin().loginPage("/login"). -// permitAll().and().logout().deleteCookies("javarest"). -// permitAll().and().rememberMe().tokenValiditySeconds(60); -// } + @Override + protected void configure (HttpSecurity http) throws Exception { + //http.csrf().disable(); + http.authorizeRequests().anyRequest().fullyAuthenticated(); + http.httpBasic(); + http.csrf().disable(); + } @Autowired UserRepository userRepository; diff --git a/src/test/java/user/UserControllerTests.java b/src/test/java/user/UserControllerTests.java index 2ffa90e..5d24b79 100644 --- a/src/test/java/user/UserControllerTests.java +++ b/src/test/java/user/UserControllerTests.java @@ -81,7 +81,7 @@ public void cleanup(){ @Test public void getUserWithAnNonAuthorizedConsumer() throws Exception { mockMvc.perform(get("/jrt/api/v1.0/users")) - .andExpect(status().is(HttpStatus.FOUND.value())); + .andExpect(status().is(HttpStatus.UNAUTHORIZED.value())); } @Test From f8de69776a471d82617581e5ec38adeada20a535 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Sun, 15 Apr 2018 08:40:27 +0100 Subject: [PATCH 16/21] Adding basic performance testing --- README.MD | 9 + performance-testing/data.csv | 10 + performance-testing/multiple-users.jmx | 399 +++++++++++++++++++++++++ 3 files changed, 418 insertions(+) create mode 100644 performance-testing/data.csv create mode 100644 performance-testing/multiple-users.jmx diff --git a/README.MD b/README.MD index e4d15e7..5fe3e2e 100644 --- a/README.MD +++ b/README.MD @@ -146,8 +146,17 @@ To run the code you have to execute. ```sh docker-compose up ``` + +or to make sure you are buildoing all the time with latest changes: + +To run the code you have to execute. +```sh +docker-compose down && docker-compose build --no-cache && docker-compose up +``` + This will start mongodb and this service in docker, then you can start sending requests. Then you can use postman or curl to call the end points (url with the format http://host:port). Default port is 8080 +You can use postman file provided in the /postman of the project to execute basic calls. Also there is a jemeter project provided which will create 10 users with different coordinates and then call distance 1000 times and then delete everything. **Answers** * Answering the questions, I guess we will have a phone call, mut what I did was basically split the functionality in 3 main parts. diff --git a/performance-testing/data.csv b/performance-testing/data.csv new file mode 100644 index 0000000..52c5c8d --- /dev/null +++ b/performance-testing/data.csv @@ -0,0 +1,10 @@ +1,Woodrow,Goodman,53.423933,-7.9406 +2,Cynthia,Bradley,53.4239330,-7.9406900 +3,Monica,Casey,53.4270272,-7.9436082 +4,Albert,Gomez,53.4232937,-7.9455823 +5,Sylvester,Tran,53.4267459,-7.9497451 +6,Shirley,Mccormick,53.4279733,-7.9437370 +7,Brandy,Barker,53.4290728,-7.9299182 +8,Lorena,Becker,53.4316041,-7.9416341 +9,Jessica,Clark,53.4312718,-7.9244251 +10,Rosalie,Shelton,53.4343399,-7.9423637 \ No newline at end of file diff --git a/performance-testing/multiple-users.jmx b/performance-testing/multiple-users.jmx new file mode 100644 index 0000000..2598a4a --- /dev/null +++ b/performance-testing/multiple-users.jmx @@ -0,0 +1,399 @@ + + + + + + false + true + true + + + + + + + + continue + + false + 1 + + 10 + 1 + false + + + + + + + + Content-Type + application/json + + + + + + + + server + localhost + = + + + port + 8080 + = + + + + + + , + + ./data.csv + false + false + true + shareMode.all + false + id,name,lastname,lat,lon + + + + + + + localhost + 8080 + + + /jrt/api/v1.0/users + 6 + + + + + + + + http://${server}:${port}/jrt/api/v1.0/user + user + password + + + + + + + + true + + + + false + { + "userId":${id}, + "firstName":"${name}", + "lastName":"${lastname}", + "latitude":${lat}, + "longitude":${lon} +} + = + + + + localhost + 8080 + + + /jrt/api/v1.0/user + POST + true + false + true + false + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + + + + + + + + continue + + false + 1 + + 1000 + 1 + false + + + + + + + + Content-Type + application/json + + + + + + + + server + localhost + = + + + port + 8080 + = + + + + + + + + + localhost + 8080 + + + /jrt/api/v1.0/distances + 6 + + + + + + + + http://${server}:${port}/jrt/api/v1.0/distances + user + password + + + + + + + + + + + localhost + 8080 + + + /jrt/api/v1.0/distances + GET + true + false + true + false + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + + + + + + + + continue + + false + 1 + + 1 + 1 + false + + + + + + + + Content-Type + application/json + + + + + + + + server + localhost + = + + + port + 8080 + = + + + + + + + + + localhost + 8080 + + + /jrt/api/v1.0/user + 6 + + + + + + + + http://${server}:${port}/jrt/api/v1.0/user + user + password + + + + + + + + + + + localhost + 8080 + + + /jrt/api/v1.0/user + DELETE + true + false + true + false + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + + + + + + + + + From 2ba230ca7e05fc85280b11bcf0d908dbec238edd Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Sun, 15 Apr 2018 09:07:13 +0100 Subject: [PATCH 17/21] Adding more records for the distance calculation 50 records with 10,000 request on calculate distance --- performance-testing/data.csv | 42 ++++++++++++++++++++++- performance-testing/multiple-users.jmx | 42 +++++++++++++++++++++-- src/main/resources/application.properties | 2 +- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/performance-testing/data.csv b/performance-testing/data.csv index 52c5c8d..c2da5b7 100644 --- a/performance-testing/data.csv +++ b/performance-testing/data.csv @@ -7,4 +7,44 @@ 7,Brandy,Barker,53.4290728,-7.9299182 8,Lorena,Becker,53.4316041,-7.9416341 9,Jessica,Clark,53.4312718,-7.9244251 -10,Rosalie,Shelton,53.4343399,-7.9423637 \ No newline at end of file +10,Rosalie,Shelton,53.4343399,-7.9423637 +11,Maureen,Vaughn,53.4193531,-7.9268659, +12,Gwendolyn,Hamilton,53.4200692,-7.9229177 +13,Cecilia,Zimmerman,53.4189950,-7.9184545 +14,Misty,Barrett,53.4184324,-7.9140771 +15,Angie,Lane,53.4174093,-7.9089273 +16,Elias,Mcdaniel,53.4197111,-7.9072965 +17,Elisa,Mills,53.4211945,-7.9009451 +18,Joshua,Keller,53.4211945,-7.9087556 +19,Faye,Walsh,53.4218083,-7.9160513 +20,Dexter,Cummings,53.4218594,-7.9200853 +21,Shane,Jackson,53.4221663,-7.9252351 +22,Joan,Silva,53.4182789,-7.9005159 +23,Marlene,Guzman,53.4207341,-7.8990568 +24,Bessie,Tate,53.4236496,-7.9009451 +25,Sandra,Curry,53.4266159,-7.9015459 +26,Erica,Morris,53.4267694,-7.9109872 +27,Patti,Foster,53.4267182,-7.9181112 +28,Francis,Obrien,53.4262068,-7.9254926 +29,Nicholas,Webb,53.4254908,-7.9337324 +30,Sherri,Patrick,53.4274853,-7.9315866 +31,Meredith,Payne,53.4298378,-7.9285825 +32,Benny,Parsons,53.4298889,-7.9235185 +33,Stephen,Flowers,53.4291218,-7.9169954 +34,Gene,Bridges,53.4271273,-7.9113306 +35,Arlene,Love,53.4262579,-7.9027475 +36,Nichole,Wong,53.4253885,-7.8974260 +37,Megan,Taylor,53.4287127,-7.8995718 +38,Eula,Owens,53.4321901,-7.9015459 +39,Terri,Saunders,53.4344400,-7.9041208 +40,Daniel,Hunter,53.4332639,-7.9102148 +41,Kathleen,Thomas,53.4336219,-7.9156221 +42,Gloria,Hodges,53.4325992,-7.9216302 +43,Willis,Kim,53.4325992,-7.9284967 +44,Troy,Sutton,53.4334173,-7.9334749 +45,Harry,Hunt,53.4348490,-7.9408563 +46,Charlene,Graves,53.4337753,-7.9489244 +47,Wanda,Pena,53.4329571,-7.9540742 +48,Tara,Scott,53.4314742,-7.9580225 +49,Elsa,Burke,53.4267182,-7.9587091 +50,Jake,Valdez,53.4252351,-7.9539026 \ No newline at end of file diff --git a/performance-testing/multiple-users.jmx b/performance-testing/multiple-users.jmx index 2598a4a..1c454eb 100644 --- a/performance-testing/multiple-users.jmx +++ b/performance-testing/multiple-users.jmx @@ -18,7 +18,7 @@ false 1 - 10 + 50 1 false @@ -162,7 +162,7 @@ false 1 - 1000 + 10000 1 false @@ -238,7 +238,43 @@ - + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + + + + + + false saveConfig diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4a9b772..9b63ded 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,7 +8,7 @@ server.port=8080 spring.application.name=users #loging -logging.level.org.springframework.web=debug +logging.level.org.springframework.web=WARN logging.file=users.log management.security.enabled=true From 3fb92244c2087957d122152798c7741d9a55d982 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Sun, 15 Apr 2018 09:19:37 +0100 Subject: [PATCH 18/21] Adding a test when there is no records to generate distance --- src/main/java/user/UserController.java | 1 - src/test/java/user/UserControllerTests.java | 13 ++++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/user/UserController.java b/src/main/java/user/UserController.java index 9518ad9..514d9bf 100644 --- a/src/main/java/user/UserController.java +++ b/src/main/java/user/UserController.java @@ -27,7 +27,6 @@ public class UserController extends WebSecurityConfigurerAdapter { @Override protected void configure (HttpSecurity http) throws Exception { - //http.csrf().disable(); http.authorizeRequests().anyRequest().fullyAuthenticated(); http.httpBasic(); http.csrf().disable(); diff --git a/src/test/java/user/UserControllerTests.java b/src/test/java/user/UserControllerTests.java index 5d24b79..cfe252b 100644 --- a/src/test/java/user/UserControllerTests.java +++ b/src/test/java/user/UserControllerTests.java @@ -35,7 +35,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -import user.UserController; import static java.util.Collections.singletonList; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; @@ -264,4 +263,16 @@ public void getDistance() throws Exception { .andExpect(jsonPath("$.average", closeTo(49, 2))) .andExpect(jsonPath("$.max", closeTo(84, 2))); } + + + @Test + @WithMockUser(username = "user", password = "password", roles = "USER") + public void getDistanceWithoutEnoughRecords() throws Exception { + User uno = new User(1, "Carlos", "", 53.421543, -7.942274); + List users = Arrays.asList(uno); + given(userRepository.findAll()).willReturn(users); + mockMvc.perform(get("/jrt/api/v1.0/distances").with(csrf())) + .andExpect(status().is(HttpStatus.NOT_FOUND.value())) + .andExpect(jsonPath("$.error", is("Not enough records to generate output"))); + } } From 350bfe8098ee136870a8c61496ef27e128baaa32 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Sun, 15 Apr 2018 18:57:57 +0100 Subject: [PATCH 19/21] Adding some details about building the project. --- .env | 0 config/httpbeat.yml | 0 docker-compose-full.yml | 21 +++++++++++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 .env create mode 100644 config/httpbeat.yml create mode 100644 docker-compose-full.yml diff --git a/.env b/.env new file mode 100644 index 0000000..e69de29 diff --git a/config/httpbeat.yml b/config/httpbeat.yml new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose-full.yml b/docker-compose-full.yml new file mode 100644 index 0000000..509c901 --- /dev/null +++ b/docker-compose-full.yml @@ -0,0 +1,21 @@ +version: '3.1' +services: + sprinbboot_ws: + build: ./ + ports: + - "8080:8080" + depends_on: + - mongo_db + links: + - mongo_db + restart: always + environment: + SPRING_DATA_MONGODB_URI: mongodb://mongo_db/users + mongo_db: + image: "mongo" + container_name: mongo_db + restart: always +# volumes: +# - ./data:/data/db + ports: + - "27017:27017" \ No newline at end of file From c7f5daafdb43b862613c8b42b3a772e1a62f3075 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Mon, 16 Apr 2018 13:41:58 +0100 Subject: [PATCH 20/21] Pulling docker image from docker hub instead of building that locally --- Dockerfile | 11 +- README.MD | 11 +- docker-compose.yml | 3 +- postman/Java_Restful.postman_collection.json | 235 +++++++++++++++---- src/main/resources/application.properties | 5 +- 5 files changed, 202 insertions(+), 63 deletions(-) diff --git a/Dockerfile b/Dockerfile index d0540b0..3724bcc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,16 @@ FROM openjdk:8-jdk-alpine EXPOSE 8080 +EXPOSE 9010 RUN mkdir -p /app/ ADD build/libs/java-restful-test-0.1.0.jar /app/java-restful-test-0.1.0.jar #ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"] #ENTRYPOINT ["java", "-jar", "/app/java-restful-test-0.1.0.jar"] -ENTRYPOINT ["java", "-Dspring.data.mongodb.uri=mongodb://mongo_db:27017/users","-Djava.security.egd=file:/dev/./urandom","-jar","/app/java-restful-test-0.1.0.jar"] \ No newline at end of file +ENTRYPOINT ["java", \ + "-Dcom.sun.management.jmxremote", \ + "-Dcom.sun.management.jmxremote.port=9010", \ + "-Dcom.sun.management.jmxremote.local.only=false", \ + "-Dcom.sun.management.jmxremote.authenticate=false", \ + "-Dcom.sun.management.jmxremote.ssl=false",\ + "-Dspring.data.mongodb.uri=mongodb://mongo_db:27017/users",\ + "-Djava.security.egd=file:/dev/./urandom",\ + "-jar","/app/java-restful-test-0.1.0.jar"] \ No newline at end of file diff --git a/README.MD b/README.MD index 5fe3e2e..fbfc923 100644 --- a/README.MD +++ b/README.MD @@ -147,16 +147,11 @@ To run the code you have to execute. docker-compose up ``` -or to make sure you are buildoing all the time with latest changes: - -To run the code you have to execute. -```sh -docker-compose down && docker-compose build --no-cache && docker-compose up -``` - This will start mongodb and this service in docker, then you can start sending requests. Then you can use postman or curl to call the end points (url with the format http://host:port). Default port is 8080 -You can use postman file provided in the /postman of the project to execute basic calls. Also there is a jemeter project provided which will create 10 users with different coordinates and then call distance 1000 times and then delete everything. +You can use postman file provided in the /postman of the project to execute basic calls. Also there is a jemeter project provided which will create 50 users with different coordinates and then call distance 1000 times and then delete everything. + +it uses Basic Auth as mechanism for security. User (user) and password (password). **Answers** * Answering the questions, I guess we will have a phone call, mut what I did was basically split the functionality in 3 main parts. diff --git a/docker-compose.yml b/docker-compose.yml index 509c901..1455410 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,10 @@ version: '3.1' services: sprinbboot_ws: - build: ./ + image: "carlospatinos/java_restful_test" ports: - "8080:8080" + - "9010:9010" depends_on: - mongo_db links: diff --git a/postman/Java_Restful.postman_collection.json b/postman/Java_Restful.postman_collection.json index 06bb52f..5e14593 100644 --- a/postman/Java_Restful.postman_collection.json +++ b/postman/Java_Restful.postman_collection.json @@ -5,53 +5,6 @@ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ - { - "name": "login", - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "username", - "value": "user", - "type": "string" - }, - { - "key": "password", - "value": "password", - "type": "string" - } - ] - }, - "method": "POST", - "header": [ - { - "key": "_csrf", - "value": "{{_csrf}}" - }, - { - "key": "username", - "value": "user" - }, - { - "key": "password", - "value": "password" - } - ], - "body": {}, - "url": { - "raw": "{{url}}/login", - "host": [ - "{{url}}" - ], - "path": [ - "login" - ] - }, - "description": "Find all users" - }, - "response": [] - }, { "name": "find_users", "request": { @@ -304,10 +257,10 @@ ], "body": { "mode": "raw", - "raw": "{\"userId\":2,\"firstName\":\"Pedro\",\"lastName\":\"Patino\",\"latitude\":53.423933,\"longitude\":-7.94069}" + "raw": "{\"userId\":1,\"firstName\":\"Otro\",\"lastName\":\"Patino\",\"latitude\":53.423933,\"longitude\":-7.94069}" }, "url": { - "raw": "{{url}}/jrt/api/v1.0/user/2", + "raw": "{{url}}/jrt/api/v1.0/user/1", "host": [ "{{url}}" ], @@ -316,12 +269,192 @@ "api", "v1.0", "user", - "2" + "1" ] }, "description": "Find all users" }, "response": [] + }, + { + "name": "get_health", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "user", + "type": "string" + }, + { + "key": "password", + "value": "password", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "url": { + "raw": "http://localhost:8080/actuator/health", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "actuator", + "health" + ] + } + }, + "response": [] + }, + { + "name": "get_beans", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "user", + "type": "string" + }, + { + "key": "password", + "value": "password", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "url": { + "raw": "http://localhost:8080/actuator/beans", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "actuator", + "beans" + ] + } + }, + "response": [] + }, + { + "name": "get_logfile", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "user", + "type": "string" + }, + { + "key": "password", + "value": "password", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "url": { + "raw": "http://localhost:8080/actuator/logfile", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "actuator", + "logfile" + ] + } + }, + "response": [] + }, + { + "name": "get_env", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "user", + "type": "string" + }, + { + "key": "password", + "value": "password", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "url": { + "raw": "http://localhost:8080/actuator/env", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "actuator", + "env" + ] + } + }, + "response": [] + }, + { + "name": "get_metrics", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "user", + "type": "string" + }, + { + "key": "password", + "value": "password", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": {}, + "url": { + "raw": "http://localhost:8080/actuator/metrics", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "actuator", + "metrics" + ] + } + }, + "response": [] } ], "event": [ @@ -348,7 +481,7 @@ ], "variable": [ { - "id": "44c22c6f-06d8-4a6d-a9b9-c853c1ac52ed", + "id": "7864d84b-867c-420a-bbfa-e9261bafdf8b", "key": "url", "value": "http://localhost:8081", "type": "string", diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9b63ded..254c2e0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -14,8 +14,9 @@ logging.file=users.log management.security.enabled=true security.basic.enabled=true -#management.endpoints.web.expose=* -#management.endpoints.web.exposure.include=* +management.endpoints.web.expose=* +management.endpoints.web.exposure.include=* +management.endpoint.metrics.enabled=true spring.security.user.name=user spring.security.user.password=password spring.security.user.roles=USER From 73e77c65d40d779f309a348a625513ceefb5f7c9 Mon Sep 17 00:00:00 2001 From: Carlos Patino Date: Wed, 18 Apr 2018 12:18:37 +0100 Subject: [PATCH 21/21] Adding ELK to the project --- .env | 3 +++ build.gradle | 13 ++++++++++++ config/elasticsearch.yml | 3 +++ config/httpbeat.yml | 27 ++++++++++++++++++++++++ config/kibana.yml | 2 ++ docker-compose-full.yml | 45 ++++++++++++++++++++++++++++++++++++++-- 6 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 config/elasticsearch.yml create mode 100644 config/kibana.yml diff --git a/.env b/.env index e69de29..73b12cd 100644 --- a/.env +++ b/.env @@ -0,0 +1,3 @@ +TAG=6.2.3 +ELASTIC_VERSION=6.2.3 +ELASTIC_PASSWORD=changeme \ No newline at end of file diff --git a/build.gradle b/build.gradle index 49dcf2f..0fa0fcb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,12 @@ +import com.bmuschko.gradle.docker.tasks.image.DockerBuildImage + buildscript { repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.0.RELEASE") + classpath('com.bmuschko:gradle-docker-plugin:3.0.8') } } @@ -18,6 +21,7 @@ apply plugin: 'eclipse' apply plugin: 'idea' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' +apply plugin: 'com.bmuschko.docker-remote-api' bootJar { baseName = 'java-restful-test' @@ -31,6 +35,11 @@ jacocoTestReport { } } +task createDockerImage(type: DockerBuildImage) { + inputDir = file('.') + tags = ['carlospatinos/java_restful_test'] +} + repositories { mavenCentral() } @@ -55,3 +64,7 @@ dependencies { testCompile('com.jayway.jsonpath:json-path') } +test.doLast { + println 'Testing the build from gradle' + createDockerImage +} \ No newline at end of file diff --git a/config/elasticsearch.yml b/config/elasticsearch.yml new file mode 100644 index 0000000..7e270fe --- /dev/null +++ b/config/elasticsearch.yml @@ -0,0 +1,3 @@ +#network.host: localhost +transport.host: 127.0.0.1 +http.host: 0.0.0.0 \ No newline at end of file diff --git a/config/httpbeat.yml b/config/httpbeat.yml index e69de29..b78fd4f 100644 --- a/config/httpbeat.yml +++ b/config/httpbeat.yml @@ -0,0 +1,27 @@ +############################## Httpbeat ######################################## +httpbeat: + hosts: + # Each - Host endpoints to call. Below are the host endpoint specific configurations + - + schedule: "@every 30s" + url: http://sprinbboot_ws:8080/actuator/health + method: get + headers: + Accept: application/json + output_format: json + json_dot_mode: replace + - + schedule: "@every 30s" + url: http://sprinbboot_ws:8080/actuator/metrics + method: get + headers: + Accept: application/json + output_format: json + json_dot_mode: replace +#================================ General ===================================== +fields: + app_id: java-test +#----------------------------- Logstash output -------------------------------- +output.elasticsearch: + hosts: ["elasticsearch:9200"] + index: "httpbeat-%{+yyyy.MM.dd}" \ No newline at end of file diff --git a/config/kibana.yml b/config/kibana.yml new file mode 100644 index 0000000..f753379 --- /dev/null +++ b/config/kibana.yml @@ -0,0 +1,2 @@ +server.host: "0.0.0.0" +elasticsearch.url: "http://elasticsearch:9200" \ No newline at end of file diff --git a/docker-compose-full.yml b/docker-compose-full.yml index 509c901..72b1655 100644 --- a/docker-compose-full.yml +++ b/docker-compose-full.yml @@ -1,9 +1,12 @@ version: '3.1' services: sprinbboot_ws: - build: ./ + image: . "carlospatinos/java_restful_test" + #build: . + container_name: sprinbboot_ws ports: - "8080:8080" + - "9010:9010" depends_on: - mongo_db links: @@ -15,7 +18,45 @@ services: image: "mongo" container_name: mongo_db restart: always + ports: + - "27017:27017" # volumes: # - ./data:/data/db + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:${TAG} + container_name: elasticsearch + ports: + - "9200:9200" + environment: ['http.host=0.0.0.0', 'transport.host=127.0.0.1'] +# volumes: +# - ./config/elasticsearch.yml:/etc/elasticsearch/elasticsearch.yml + #'ELASTIC_PASSWORD=${ELASTIC_PASSWORD}' + #discovery.type: single-node + kibana: + image: docker.elastic.co/kibana/kibana:${TAG} + container_name: kibana + links: + - elasticsearch + environment: + - ELASTICSEARCH_USERNAME=kibana + - ELASTICSEARCH_PASSWORD=${ELASTIC_PASSWORD} ports: - - "27017:27017" \ No newline at end of file + - "5601:5601" + depends_on: ['elasticsearch'] + volumes: + - ./config/kibana.yml:/usr/share/kibana/config/kibana.yml + httpbeat: + links: + - elasticsearch + image: evanhoucke/httpbeat + container_name: httpbeat + environment: + ES_HOST: elasticsearch + ES_PORT: 9200 + depends_on: ['elasticsearch'] + volumes: + - ./config/httpbeat.yml:/opt/beats/http.yml + #- ./config/httpbeat.yml:/etc/httpbeat/httpbeat.yml + #network_mode: bridge + restart: always +# networks: {stack: {}} \ No newline at end of file