diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4267c4b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.settings +.vscode +.DS_Store +.project +.classpath +target +bin +.idea +*.iml \ No newline at end of file diff --git a/README.md b/README.md index 15d8f685..64f1fc00 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,46 @@ +# Desafio de desenvolvimento back end - Santander + +Passos para executar a solução: + +1) garanta que o computador possui o JDK versão 1.8, Maven versão 3.6.0, MongoDB versão 4.0.4 e git (qualquer versão recente) instalados. Para isso, abra uma janela de seu terminal e execute os comandos + +``` +java -version +mvn -version +mongod -version +git --version +``` + +As versões de cada um devem ser impressas na saída padrão. + +2) clone este repositório para o seu computador +3) inicie o MongoDB executando o comando `mongod`. Isso impedirá você de utilizar esta janela do terminal, portanto abra uma nova e passe a usá-la, mas deixe a anterior aberta +3) utilizando a nova janela do terminal, navegue até o diretório local onde você clonou este repositório +4) entre no o diretório do microserviço de autenticação, ou `./auth_microservice` +5) execute o comando `mvn spring-boot:run` +6) abra uma terceira janela de seu terminal e, novamente, navegue até o diretório local onde se encontra este repositório +7) entre no diretório do microserviço de gastos, ou `./spend_microservice`, e finalmente, execute o comando `mvn spring-boot:run` +8) desfrute da aplicação! +  +  +#### Observações +- Os microserviços estão configurados para ocuparem as portas 8080 e 8081, respectivamente. +- Para executar os testes unitários do microserviço de gastos, é preciso que o microserviço de autenticação esteja online +- Este repositório conta com uma collection e um environment do Postman, uma ferramenta muito útil para testar as funcionalidades de APIs. Para utilizá-los, faça o download e instale o Postman em seu computador (caso ainda não o possua intalado) e importe tanto collection quanto environment para o seu ambiente. +  +  +#### Possíveis otimizações (próximos passos) +1) Para melhorar a velocidade de resposta da rota de inserção de gastos, seria interessante refatorá-la para que ela inserisse os documentos a serem persistidos primeiramente em um sistema de mensageria, ou no próprio Redis (devido a sua velocidade de inserção e consulta). Após isso, workers assíncronos subscritos ao canal de mensagens seriam responsáveis por persistir os dados no banco de dados de fato +2) Outra possível melhoria seria tornar assíncronas as execuções dos métodos dos controllers (no momento, apenas os métodos da camada de serviço estão sendo processados de forma assíncrona). Isto não foi feito pois o uso da classe `HandlerInterceptorAdapter` faz com que duas chamadas de seu método `preHandle()` ocorram a cada requisição assíncrona recebida, e como a lógica de validação de tokens se encontra dentro deste método, o resultado seria que o número de requisições que o microserviço de autentição precisaria responder para o sistema funcionasse normalmente dobraria +3) Containerizar cada um dos microserviços tornaria muito mais simples a tarefa de movê-los de um infraestrutura para outra, escalá-los e administrá-los no geral +  + +  +Por fim, estou me candidando pela **IBM**. +  + +  +#### O texto original do desafio se encontra abaixo # Show me the code ### # DESAFIO: diff --git a/auth_microservice/pom.xml b/auth_microservice/pom.xml new file mode 100644 index 00000000..020e76b9 --- /dev/null +++ b/auth_microservice/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + br.com.testesantander.api + authentication_microservice + 0.1.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.0.5.RELEASE + + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + org.springframework.boot + spring-boot-starter-web + + + + org.mindrot + jbcrypt + 0.4 + + + + com.auth0 + java-jwt + 3.4.1 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.jayway.jsonpath + json-path + test + + + + + 1.8 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + spring-releases + https://repo.spring.io/libs-release + + + + \ No newline at end of file diff --git a/auth_microservice/src/main/java/microservice/AuthMicroservice.java b/auth_microservice/src/main/java/microservice/AuthMicroservice.java new file mode 100644 index 00000000..a00b5aad --- /dev/null +++ b/auth_microservice/src/main/java/microservice/AuthMicroservice.java @@ -0,0 +1,39 @@ +package microservice; + + +import org.springframework.boot.SpringApplication; +import org.springframework.core.task.TaskExecutor; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +@SpringBootApplication +public class AuthMicroservice { + + @Value("${thread.pool.core.size}") + private int corePoolSize; + + @Value("${thread.pool.max.size}") + private int maxPoolSize; + + @Value("${thread.queue.capacity}") + private int queueCapacity; + + @Bean(name = "ThreadPoolExecutor") + public TaskExecutor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(corePoolSize); + executor.setMaxPoolSize(maxPoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setThreadNamePrefix("ThreadPoolExecutor-"); + executor.initialize(); + return executor; + } + + public static void main(String[] args) { + SpringApplication.run(AuthMicroservice.class, args); + } + +} diff --git a/auth_microservice/src/main/java/microservice/configurations/WebMvcConfig.java b/auth_microservice/src/main/java/microservice/configurations/WebMvcConfig.java new file mode 100644 index 00000000..0efd4b02 --- /dev/null +++ b/auth_microservice/src/main/java/microservice/configurations/WebMvcConfig.java @@ -0,0 +1,22 @@ +package microservice.configurations; + + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import microservice.interceptors.JWTInterceptor; + + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired + private JWTInterceptor interceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry){ + registry.addInterceptor(interceptor).addPathPatterns("/**"); + } + +} diff --git a/auth_microservice/src/main/java/microservice/controllers/SystemController.java b/auth_microservice/src/main/java/microservice/controllers/SystemController.java new file mode 100644 index 00000000..747560c2 --- /dev/null +++ b/auth_microservice/src/main/java/microservice/controllers/SystemController.java @@ -0,0 +1,73 @@ +package microservice.controllers; + + +import org.springframework.web.bind.annotation.RestController; +import org.springframework.beans.factory.annotation.Autowired; +import microservice.services.SystemService; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestBody; +import javax.validation.Valid; +import org.springframework.http.ResponseEntity; +import java.net.URI; +import java.net.URISyntaxException; +import org.springframework.web.util.UriComponentsBuilder; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.springframework.http.MediaType; +import microservice.models.Message; +import microservice.models.System; +import microservice.models.Authorization; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RequestHeader; + + +@RestController +public class SystemController { + + @Autowired + private SystemService systemService; + + @RequestMapping(value = "/systems", + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity inserNewSystem(UriComponentsBuilder builder, + @Valid @RequestBody System system) + throws URISyntaxException, InterruptedException, ExecutionException { + + CompletableFuture systemFuture = systemService.register(system); + System storedSystem = systemFuture.get(); + return ResponseEntity + .created(new URI(builder.toUriString() + "/systems/" + storedSystem.get_id())) + .body(new Message("system " + system.getUsername() + " successfully registered", storedSystem.get_id(), true)); + } + + @RequestMapping(value = "/systems/authentication", + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity authenticateSystem(@Valid @RequestBody System system) + throws InterruptedException, ExecutionException { + + CompletableFuture systemFuture = systemService.authenticate(system); + Object result = systemFuture.get(); + if (result.getClass() == Authorization.class) + return ResponseEntity.ok(result); + else + return new ResponseEntity<>(result, HttpStatus.UNAUTHORIZED); + } + + @RequestMapping(value = "/systems/authorization", + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity authorizeSystem(@RequestHeader("Authorization") String accessToken) + throws InterruptedException, ExecutionException { + + CompletableFuture systemFuture = systemService.authorize(accessToken); + Message msg = systemFuture.get(); + if (msg.getStatus().equals("success")) + return ResponseEntity.ok(msg); + else + return new ResponseEntity<>(msg, HttpStatus.UNAUTHORIZED); + } + +} diff --git a/auth_microservice/src/main/java/microservice/controllers/UserController.java b/auth_microservice/src/main/java/microservice/controllers/UserController.java new file mode 100644 index 00000000..4bbf7f4b --- /dev/null +++ b/auth_microservice/src/main/java/microservice/controllers/UserController.java @@ -0,0 +1,73 @@ +package microservice.controllers; + + +import org.springframework.web.bind.annotation.RestController; +import org.springframework.beans.factory.annotation.Autowired; +import microservice.services.UserService; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import javax.validation.Valid; +import org.springframework.http.ResponseEntity; +import java.net.URI; +import java.net.URISyntaxException; +import org.springframework.web.util.UriComponentsBuilder; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.springframework.http.MediaType; +import org.springframework.http.HttpStatus; +import microservice.models.Authorization; +import microservice.models.Message; +import microservice.models.User; + + +@RestController +public class UserController { + + @Autowired + private UserService userService; + + @RequestMapping(value = "/users", + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity register(UriComponentsBuilder builder, + @Valid @RequestBody User user) + throws URISyntaxException, InterruptedException, ExecutionException { + + CompletableFuture userFuture = userService.register(user); + User storedUser = userFuture.get(); + return ResponseEntity + .created(new URI(builder.toUriString() + "/users/" + storedUser.get_id())) + .body(new Message("user " + user.getUsername() + " successfully registered", storedUser.get_id(), true)); + } + + @RequestMapping(value = "/users/authentication", + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity authenticateUser(@Valid @RequestBody User user) + throws InterruptedException, ExecutionException { + + CompletableFuture userFuture = userService.authenticate(user); + Object result = userFuture.get(); + if (result.getClass() == Authorization.class) + return ResponseEntity.ok(result); + else + return new ResponseEntity<>(result, HttpStatus.UNAUTHORIZED); + } + + @RequestMapping(value = "/users/authorization", + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity authorizeUser(@RequestHeader("Authorization") String accessToken) + throws InterruptedException, ExecutionException { + + CompletableFuture userFuture = userService.authorize(accessToken); + Message msg = userFuture.get(); + if (msg.getStatus().equals("success")) + return ResponseEntity.ok(msg); + else + return new ResponseEntity<>(msg, HttpStatus.UNAUTHORIZED); + } + +} diff --git a/auth_microservice/src/main/java/microservice/interceptors/JWTInterceptor.java b/auth_microservice/src/main/java/microservice/interceptors/JWTInterceptor.java new file mode 100644 index 00000000..1e1d8cb0 --- /dev/null +++ b/auth_microservice/src/main/java/microservice/interceptors/JWTInterceptor.java @@ -0,0 +1,34 @@ +package microservice.interceptors; + + +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +@Component +public class JWTInterceptor extends HandlerInterceptorAdapter { + + private static final Logger LOGGER = LoggerFactory.getLogger(JWTInterceptor.class); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, PATCH, DELETE, OPTIONS"); + response.setHeader("Access-Control-Max-Age", "6000"); + response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With"); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + String requestMethod = request.getMethod(); + String requestURI = request.getRequestURI(); + + LOGGER.info("[" + requestMethod + "] " + requestURI); + + return super.preHandle(request, response, handler); + } + +} diff --git a/auth_microservice/src/main/java/microservice/models/Authorization.java b/auth_microservice/src/main/java/microservice/models/Authorization.java new file mode 100644 index 00000000..61c53c90 --- /dev/null +++ b/auth_microservice/src/main/java/microservice/models/Authorization.java @@ -0,0 +1,45 @@ +package microservice.models; + + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; + + +public class Authorization { + + private String accessToken; + private boolean status; + private String clientId; + + @JsonFormat(shape=JsonFormat.Shape.STRING, + pattern="yyyy-MM-dd'T'HH:mm:ss.SSSZ", + timezone="UTC") + private Date expiryDate; + + + public Authorization() { } + + public Authorization(String accessToken, Date expiryDate, String clientId, boolean status) { + this.accessToken = accessToken; + this.status = status; + this.expiryDate = expiryDate; + this.clientId = clientId; + } + + public String getAccessToken() { return accessToken; } + + public void setAccessToken(String accessToken) { this.accessToken = accessToken; } + + public Date getExpiryDate() { return expiryDate; } + + public void setExpiryDate(Date expiryDate) { this.expiryDate = expiryDate; } + + public String getStatus() { return status ? "success" : "failed"; } + + public void setStatus(boolean status) { this.status = status; } + + public String getClientId() { return clientId; } + + public void setClientId(String clientId) { this.clientId = clientId; } + +} diff --git a/auth_microservice/src/main/java/microservice/models/Message.java b/auth_microservice/src/main/java/microservice/models/Message.java new file mode 100644 index 00000000..364b2790 --- /dev/null +++ b/auth_microservice/src/main/java/microservice/models/Message.java @@ -0,0 +1,30 @@ +package microservice.models; + + +public class Message { + + private String content; + private boolean status; + private String clientId; + + public Message() { } + + public Message(String content, String clientId, boolean status) { + this.content = content; + this.clientId = clientId; + this.status = status; + } + + public String getContent() { return content; } + + public void setContent(String content) { this.content = content; } + + public String getStatus() { return status ? "success" : "failed"; } + + public void setStatus(boolean status) { this.status = status; } + + public String getClientId() { return clientId; } + + public void setClientId(String clientId) { this.clientId = clientId; } + +} diff --git a/auth_microservice/src/main/java/microservice/models/System.java b/auth_microservice/src/main/java/microservice/models/System.java new file mode 100644 index 00000000..35b545bd --- /dev/null +++ b/auth_microservice/src/main/java/microservice/models/System.java @@ -0,0 +1,43 @@ +package microservice.models; + + +import org.springframework.data.annotation.Id; +import javax.validation.constraints.NotNull; +import org.bson.types.ObjectId; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + + +@Document(collection = "systems") +public class System { + + @Id + private ObjectId _id; + + @NotNull + @Indexed(unique = true) + private String username; + + @NotNull + private String password; + + public System() { } + + public System(String username, String password) { + this.username = username; + this.password = password; + } + + public String get_id() { return _id.toHexString(); } + + public String getUsername() { return username; } + + public String getPassword() { return password; } + + public void set_id(ObjectId _id) { this._id = _id; } + + public void setUsername(String username) { this.username = username; } + + public void setPassword(String password) { this.password = password; } + +} diff --git a/auth_microservice/src/main/java/microservice/models/User.java b/auth_microservice/src/main/java/microservice/models/User.java new file mode 100644 index 00000000..abdd6490 --- /dev/null +++ b/auth_microservice/src/main/java/microservice/models/User.java @@ -0,0 +1,43 @@ +package microservice.models; + + +import org.springframework.data.annotation.Id; +import javax.validation.constraints.NotNull; +import org.bson.types.ObjectId; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + + +@Document(collection = "users") +public class User { + + @Id + private ObjectId _id; + + @NotNull + @Indexed(unique = true) + private String username; + + @NotNull + private String password; + + public User() { } + + public User(String username, String password) { + this.username = username; + this.password = password; + } + + public String get_id() { return _id.toHexString(); } + + public String getUsername() { return username; } + + public String getPassword() { return password; } + + public void set_id(ObjectId _id) { this._id = _id; } + + public void setUsername(String username) { this.username = username; } + + public void setPassword(String password) { this.password = password; } + +} diff --git a/auth_microservice/src/main/java/microservice/repositories/SystemRepository.java b/auth_microservice/src/main/java/microservice/repositories/SystemRepository.java new file mode 100644 index 00000000..4a46980b --- /dev/null +++ b/auth_microservice/src/main/java/microservice/repositories/SystemRepository.java @@ -0,0 +1,12 @@ +package microservice.repositories; + + +import microservice.models.System; +import org.springframework.data.mongodb.repository.MongoRepository; + + +public interface SystemRepository extends MongoRepository { + + public System findByUsername(String username); + +} diff --git a/auth_microservice/src/main/java/microservice/repositories/UserRepository.java b/auth_microservice/src/main/java/microservice/repositories/UserRepository.java new file mode 100644 index 00000000..fb0766a4 --- /dev/null +++ b/auth_microservice/src/main/java/microservice/repositories/UserRepository.java @@ -0,0 +1,12 @@ +package microservice.repositories; + + +import microservice.models.User; +import org.springframework.data.mongodb.repository.MongoRepository; + + +public interface UserRepository extends MongoRepository { + + public User findByUsername(String username); + +} diff --git a/auth_microservice/src/main/java/microservice/services/SystemService.java b/auth_microservice/src/main/java/microservice/services/SystemService.java new file mode 100644 index 00000000..05ba2834 --- /dev/null +++ b/auth_microservice/src/main/java/microservice/services/SystemService.java @@ -0,0 +1,110 @@ +package microservice.services; + + +import microservice.repositories.SystemRepository; +import microservice.util.PasswordHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import microservice.models.Authorization; +import microservice.models.Message; +import microservice.models.System; +import java.util.concurrent.CompletableFuture; +import org.springframework.scheduling.annotation.Async; +import org.bson.types.ObjectId; +import java.util.Calendar; +import com.auth0.jwt.algorithms.Algorithm; +import org.springframework.beans.factory.annotation.Value; +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.JWTCreationException; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.exceptions.JWTVerificationException; + + +@Service +public class SystemService { + + @Value("${jwt.issuer}") + private String ISSUER; + + @Value("${jwt.sys.secret}") + private String SECRET; + + @Value("${jwt.duration.in.days}") + private int ACCESS_TOKEN_DURATION; + + @Autowired + private SystemRepository systemRepo; + + + @Async("ThreadPoolExecutor") + public CompletableFuture register(System system) { + system.set_id(ObjectId.get()); + system.setPassword(PasswordHandler.encryptPassword(system.getPassword())); + return CompletableFuture.completedFuture(systemRepo.save(system)); + } + + @Async("ThreadPoolExecutor") + public CompletableFuture authenticate(System system) { + System storedSystem = systemRepo.findByUsername(system.getUsername()); + String msg = "invalid username or password"; + + if (storedSystem != null) { + boolean isValidPassword = PasswordHandler.checkPassword(system.getPassword(), + storedSystem.getPassword()); + + if (isValidPassword) { + Calendar issuedAt = Calendar.getInstance(); + Calendar expiresAt = Calendar.getInstance(); + expiresAt.add(Calendar.DATE, ACCESS_TOKEN_DURATION); + + try { + Algorithm algorithm = Algorithm.HMAC256(SECRET); + String token = JWT.create() + .withIssuer(ISSUER) + .withIssuedAt(issuedAt.getTime()) + .withExpiresAt(expiresAt.getTime()) + .withClaim("systemId", storedSystem.get_id()) + .sign(algorithm); + + Authorization auth = new Authorization(token, expiresAt.getTime(), storedSystem.get_id(), true); + return CompletableFuture.completedFuture(auth); + } + catch (JWTCreationException e) { + // invalid signing configuration / couldn't convert claims + msg = e.getMessage(); + } + } + } + + return CompletableFuture.completedFuture(new Message(msg, null, false)); + } + + @Async("ThreadPoolExecutor") + public CompletableFuture authorize(String token) { + String msg = "and error has occurred"; + boolean status = false; + String systemId = null; + try { + Algorithm algorithm = Algorithm.HMAC256(SECRET); + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer(ISSUER) + .build(); + + DecodedJWT jwt = verifier.verify(token); + systemId = jwt.getClaim("systemId").asString(); + msg = "valid token"; + status = true; + } + catch (JWTVerificationException e) { + msg = "invalid token"; + } + catch (NullPointerException e) { + msg = "no token provided"; + } + + return CompletableFuture.completedFuture(new Message(msg, systemId, status)); + } + + +} diff --git a/auth_microservice/src/main/java/microservice/services/UserService.java b/auth_microservice/src/main/java/microservice/services/UserService.java new file mode 100644 index 00000000..5166b24c --- /dev/null +++ b/auth_microservice/src/main/java/microservice/services/UserService.java @@ -0,0 +1,109 @@ +package microservice.services; + + +import microservice.repositories.UserRepository; +import microservice.util.PasswordHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import microservice.models.Authorization; +import microservice.models.Message; +import microservice.models.User; +import java.util.concurrent.CompletableFuture; +import org.springframework.scheduling.annotation.Async; +import org.bson.types.ObjectId; +import java.util.Calendar; +import com.auth0.jwt.algorithms.Algorithm; +import org.springframework.beans.factory.annotation.Value; +import com.auth0.jwt.JWT; +import com.auth0.jwt.exceptions.JWTCreationException; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.exceptions.JWTVerificationException; + + +@Service +public class UserService { + + @Value("${jwt.issuer}") + private String ISSUER; + + @Value("${jwt.usr.secret}") + private String SECRET; + + @Value("${jwt.duration.in.days}") + private int ACCESS_TOKEN_DURATION; + + @Autowired + private UserRepository userRepo; + + + @Async("ThreadPoolExecutor") + public CompletableFuture register(User user) { + user.set_id(ObjectId.get()); + user.setPassword(PasswordHandler.encryptPassword(user.getPassword())); + return CompletableFuture.completedFuture(userRepo.save(user)); + } + + @Async("ThreadPoolExecutor") + public CompletableFuture authenticate(User user) { + User storedUser = userRepo.findByUsername(user.getUsername()); + String msg = "invalid username or password"; + + if (storedUser != null) { + boolean isValidPassword = PasswordHandler.checkPassword(user.getPassword(), + storedUser.getPassword()); + + if (isValidPassword) { + Calendar issuedAt = Calendar.getInstance(); + Calendar expiresAt = Calendar.getInstance(); + expiresAt.add(Calendar.DATE, ACCESS_TOKEN_DURATION); + + try { + Algorithm algorithm = Algorithm.HMAC256(SECRET); + String token = JWT.create() + .withIssuer(ISSUER) + .withIssuedAt(issuedAt.getTime()) + .withExpiresAt(expiresAt.getTime()) + .withClaim("userId", storedUser.get_id()) + .sign(algorithm); + + Authorization auth = new Authorization(token, expiresAt.getTime(), storedUser.get_id(), true); + return CompletableFuture.completedFuture(auth); + } + catch (JWTCreationException e) { + // invalid signing configuration / couldn't convert claims + msg = e.getMessage(); + } + } + } + + return CompletableFuture.completedFuture(new Message(msg, null, false)); + } + + @Async("ThreadPoolExecutor") + public CompletableFuture authorize(String token) { + String msg = "an error has occurred"; + boolean status = false; + String userId = null; + try { + Algorithm algorithm = Algorithm.HMAC256(SECRET); + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer(ISSUER) + .build(); + + DecodedJWT jwt = verifier.verify(token); + userId = jwt.getClaim("userId").asString(); + msg = "valid token"; + status = true; + } + catch (JWTVerificationException e) { + msg = "invalid token"; + } + catch (NullPointerException e) { + msg = "no token provided"; + } + + return CompletableFuture.completedFuture(new Message(msg, userId, status)); + } + +} diff --git a/auth_microservice/src/main/java/microservice/util/PasswordHandler.java b/auth_microservice/src/main/java/microservice/util/PasswordHandler.java new file mode 100644 index 00000000..77bd1289 --- /dev/null +++ b/auth_microservice/src/main/java/microservice/util/PasswordHandler.java @@ -0,0 +1,29 @@ +package microservice.util; + + +import org.mindrot.jbcrypt.BCrypt; +import org.springframework.stereotype.Component; + + +@Component +public class PasswordHandler { + + private static int workload = 12; + + public static String encryptPassword(String plaintextPassword) { + String salt = BCrypt.gensalt(workload); + String hashedPassword = BCrypt.hashpw(plaintextPassword, salt); + return hashedPassword; + } + + public static boolean checkPassword(String plaintextPassword, String storedHash) { + boolean verifiedPassword = false; + + if (null == storedHash || !storedHash.startsWith("$2a$")) + throw new java.lang.IllegalArgumentException("invalid hash provided for comparison"); + + verifiedPassword = BCrypt.checkpw(plaintextPassword, storedHash); + + return verifiedPassword; + } +} \ No newline at end of file diff --git a/auth_microservice/src/main/resources/application.properties b/auth_microservice/src/main/resources/application.properties new file mode 100644 index 00000000..9628d8ac --- /dev/null +++ b/auth_microservice/src/main/resources/application.properties @@ -0,0 +1,8 @@ +jwt.usr.secret=dev@ibm#santanderTEST$11043512/Uz3r}Auth +jwt.sys.secret=dev@ibm#santanderTEST$11043512/SYZZ{Auth +jwt.issuer=http://api.santandertest.com.br +thread.pool.core.size=7 +thread.pool.max.size=42 +thread.queue.capacity=11 +jwt.duration.in.days=14 +server.port=8080 \ No newline at end of file diff --git a/postman_testbench/IBM-Santander.postman_collection.json b/postman_testbench/IBM-Santander.postman_collection.json new file mode 100644 index 00000000..14171723 --- /dev/null +++ b/postman_testbench/IBM-Santander.postman_collection.json @@ -0,0 +1,543 @@ +{ + "info": { + "_postman_id": "a0d948d5-d8b6-4712-9527-17a8717d6bfc", + "name": "IBM-Santander", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "auth microservice", + "item": [ + { + "name": "register new user", + "event": [ + { + "listen": "test", + "script": { + "id": "10762c41-aadb-42f9-abe5-2fd62e7727a9", + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if (pm.response.to.have.header(\"Location\")) {", + " const id = postman.getResponseHeader('Location').replace(pm.variables.get(\"auth_url\"), '');", + " pm.environment.set(\"usr_id\", id);", + " }", + "});", + "", + "", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"username\": \"{{usr}}\",\n\t\"password\": \"{{usr_pwd}}\"\n}" + }, + "url": { + "raw": "{{auth_url}}users", + "host": [ + "{{auth_url}}users" + ] + } + }, + "response": [] + }, + { + "name": "register new system", + "event": [ + { + "listen": "test", + "script": { + "id": "10762c41-aadb-42f9-abe5-2fd62e7727a9", + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " if (pm.response.to.have.header(\"Location\")) {", + " const id = postman.getResponseHeader('Location').replace(pm.variables.get(\"auth_url\"), '');", + " pm.environment.set(\"sys_id\", id);", + " }", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"username\": \"{{sys}}\",\n\t\"password\": \"{{sys_pwd}}\"\n}" + }, + "url": { + "raw": "{{auth_url}}systems", + "host": [ + "{{auth_url}}systems" + ] + } + }, + "response": [] + }, + { + "name": "user login", + "event": [ + { + "listen": "test", + "script": { + "id": "1117d661-8bb3-44af-9ca3-2cec7cd1347c", + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.environment.set(\"token\", pm.response.json().accessToken);", + " pm.environment.set(\"usr_id\", pm.response.json().clientId);", + "});", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "id": "cab6fe3e-4c98-43bc-96a1-4640849ac2b9", + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"username\": \"{{usr}}\",\n\t\"password\": \"{{usr_pwd}}\"\n}" + }, + "url": { + "raw": "{{auth_url}}users/authentication", + "host": [ + "{{auth_url}}users" + ], + "path": [ + "authentication" + ] + } + }, + "response": [] + }, + { + "name": "system login", + "event": [ + { + "listen": "test", + "script": { + "id": "1117d661-8bb3-44af-9ca3-2cec7cd1347c", + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.environment.set(\"token\", pm.response.json().accessToken);", + " pm.environment.set(\"sys_id\", pm.response.json().clientId);", + "});", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"username\": \"{{sys}}\",\n\t\"password\": \"{{sys_pwd}}\"\n}" + }, + "url": { + "raw": "{{auth_url}}systems/authentication", + "host": [ + "{{auth_url}}systems" + ], + "path": [ + "authentication" + ] + } + }, + "response": [] + }, + { + "name": "validate user token", + "event": [ + { + "listen": "test", + "script": { + "id": "1117d661-8bb3-44af-9ca3-2cec7cd1347c", + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "{{token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{auth_url}}users/authorization", + "host": [ + "{{auth_url}}users" + ], + "path": [ + "authorization" + ] + } + }, + "response": [] + }, + { + "name": "validate system token", + "event": [ + { + "listen": "test", + "script": { + "id": "1117d661-8bb3-44af-9ca3-2cec7cd1347c", + "exec": [ + "" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "{{token}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{auth_url}}systems/authorization", + "host": [ + "{{auth_url}}systems" + ], + "path": [ + "authorization" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "spends microservice", + "item": [ + { + "name": "register new spend", + "event": [ + { + "listen": "test", + "script": { + "id": "7075ca05-67af-45ac-943f-e4ad20c8d6b7", + "exec": [ + "pm.test(\"Status code is 201\", function () {", + " pm.response.to.have.status(201);", + " pm.environment.set(\"spend_id\", pm.response.json()._id);", + "});", + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "id": "2164e194-f49f-4265-8f7d-57724ef44607", + "exec": [ + "pm.environment.set(\"spend_date\", (new Date()).toISOString().replace('Z', '+0000'));" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "disabled": false, + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "type": "text", + "value": "{{token}}" + } + ], + "body": { + "mode": "raw", + "raw": "{ \n\t\"description\": \"testSpend1\", \n\t\"value\": 99.98123456, \n\t\"userCode\": \"{{usr_id}}\",\n\t\"date\": \"{{spend_date}}\"\n}" + }, + "url": { + "raw": "{{spend_url}}spends", + "host": [ + "{{spend_url}}spends" + ] + } + }, + "response": [] + }, + { + "name": "retrieve user spends", + "event": [ + { + "listen": "test", + "script": { + "id": "7075ca05-67af-45ac-943f-e4ad20c8d6b7", + "exec": [ + "" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "id": "2164e194-f49f-4265-8f7d-57724ef44607", + "exec": [ + "const now = new Date();", + "pm.environment.set(\"endDate\", now.toISOString().replace('Z', '+0000'));", + "", + "let past = new Date();", + "past.setDate(now.getDate() - 21);", + "pm.environment.set(\"startDate\", past.toISOString().replace('Z', '+0000'));" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "disabled": false, + "key": "Content-Type", + "name": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "type": "text", + "value": "{{token}}" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{spend_url}}users/spends?start_date={{startDate}}&end_date={{endDate}}", + "host": [ + "{{spend_url}}users" + ], + "path": [ + "spends" + ], + "query": [ + { + "key": "start_date", + "value": "{{startDate}}" + }, + { + "key": "end_date", + "value": "{{endDate}}" + } + ] + } + }, + "response": [] + }, + { + "name": "update category", + "request": { + "method": "PATCH", + "header": [ + { + "key": "Content-Type", + "name": "Content-Type", + "type": "text", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "{{token}}", + "equals": true + } + ], + "body": { + "mode": "raw", + "raw": "{\n\t\"category\": \"newCategory\"\n}" + }, + "url": { + "raw": "{{spend_url}}spends/{{spend_id}}/categories", + "host": [ + "{{spend_url}}spends" + ], + "path": [ + "{{spend_id}}", + "categories" + ] + } + }, + "response": [] + }, + { + "name": "category suggestions", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "{{token}}", + "type": "text" + } + ], + "body": {}, + "url": { + "raw": "{{spend_url}}categories/suggestions?partial_name=cat", + "host": [ + "{{spend_url}}categories" + ], + "path": [ + "suggestions" + ], + "query": [ + { + "key": "partial_name", + "value": "cat" + } + ] + } + }, + "response": [] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "id": "55867996-6115-4d11-84bf-f549f04450af", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "1f5beed3-6e7b-44c1-b9a7-2dfa0104c40a", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "id": "3f578014-977b-4640-a289-ffd79438b179", + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "id": "751bf649-adb9-4061-b73d-511aae36d620", + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] +} \ No newline at end of file diff --git a/postman_testbench/IBM-Santander.postman_environment.json b/postman_testbench/IBM-Santander.postman_environment.json new file mode 100644 index 00000000..4f59167a --- /dev/null +++ b/postman_testbench/IBM-Santander.postman_environment.json @@ -0,0 +1,119 @@ +{ + "id": "e4e4105e-af64-4847-a690-7dbaa64883eb", + "name": "IBM-Santander", + "values": [ + { + "key": "token", + "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzeXN0ZW1JZCI6IjVjMWIyOGVlZGQzYjdlM2VlZGZkNjQ4NCIsImlzcyI6Imh0dHA6Ly9hcGkuc2FudGFuZGVydGVzdC5jb20uYnIiLCJleHAiOjE1NDY0OTM0MzUsImlhdCI6MTU0NTI4MzgzNX0.aqKw1Aodhjf2rYLJ6XeS7Y6bDzMnAwi80ekj85UPIyg", + "description": { + "content": "", + "type": "text/plain" + }, + "enabled": true + }, + { + "key": "usr_id", + "value": "5c1b1cc9dd3b7e3eedfd6482", + "description": { + "content": "", + "type": "text/plain" + }, + "enabled": true + }, + { + "key": "usr", + "value": "testUser", + "description": { + "content": "", + "type": "text/plain" + }, + "enabled": true + }, + { + "key": "usr_pwd", + "value": "testUser12345", + "description": { + "content": "", + "type": "text/plain" + }, + "enabled": true + }, + { + "key": "sys_id", + "value": "5c1b28eedd3b7e3eedfd6484", + "description": { + "content": "", + "type": "text/plain" + }, + "enabled": true + }, + { + "key": "sys", + "value": "testSystem", + "description": { + "content": "", + "type": "text/plain" + }, + "enabled": true + }, + { + "key": "sys_pwd", + "value": "testSystem12345", + "description": { + "content": "", + "type": "text/plain" + }, + "enabled": true + }, + { + "key": "auth_url", + "value": "http://localhost:8080/", + "description": { + "content": "", + "type": "text/plain" + }, + "enabled": true + }, + { + "key": "spend_url", + "value": "http://localhost:8081/", + "description": { + "content": "", + "type": "text/plain" + }, + "enabled": true + }, + { + "key": "spend_date", + "value": "2018-12-20T05:31:04.054+0000", + "enabled": true + }, + { + "key": "endDate", + "value": "2018-12-20T05:25:06.567+0000", + "enabled": true + }, + { + "key": "startDate", + "value": "2018-11-29T05:25:06.567+0000", + "enabled": true + }, + { + "key": "now", + "value": "2018-12-16T07:54:32.744+0000", + "enabled": true + }, + { + "key": "spend_id", + "value": "5c1b2918dd3b7e47dfcc6ee3", + "description": { + "content": "", + "type": "text/plain" + }, + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2018-12-20T05:32:42.129Z", + "_postman_exported_using": "Postman/6.6.1" +} \ No newline at end of file diff --git a/spend_microservice/pom.xml b/spend_microservice/pom.xml new file mode 100644 index 00000000..bfa9d905 --- /dev/null +++ b/spend_microservice/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + br.com.testesantander.api + spend_microservice + 0.1.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.0.5.RELEASE + + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.jayway.jsonpath + json-path + test + + + + + 1.8 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + spring-releases + https://repo.spring.io/libs-release + + + + \ No newline at end of file diff --git a/spend_microservice/src/main/java/microservice/SpendsMicroservice.java b/spend_microservice/src/main/java/microservice/SpendsMicroservice.java new file mode 100644 index 00000000..e2f1774b --- /dev/null +++ b/spend_microservice/src/main/java/microservice/SpendsMicroservice.java @@ -0,0 +1,41 @@ +package microservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.core.task.TaskExecutor; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.beans.factory.annotation.Value; + +@EnableAsync +@SpringBootApplication +public class SpendsMicroservice { + + @Value("${thread.pool.core.size}") + private int corePoolSize; + + @Value("${thread.pool.max.size}") + private int maxPoolSize; + + @Value("${thread.queue.capacity}") + private int queueCapacity; + + + @Bean(name = "ThreadPoolExecutor") + public TaskExecutor getAsyncExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(corePoolSize); + executor.setMaxPoolSize(maxPoolSize); + executor.setQueueCapacity(queueCapacity); + executor.setThreadNamePrefix("ThreadPoolExecutor-"); + executor.initialize(); + return executor; + } + + + public static void main(String[] args) { + SpringApplication.run(SpendsMicroservice.class, args); + } + +} diff --git a/spend_microservice/src/main/java/microservice/configurations/WebMvcConfig.java b/spend_microservice/src/main/java/microservice/configurations/WebMvcConfig.java new file mode 100644 index 00000000..d76fab25 --- /dev/null +++ b/spend_microservice/src/main/java/microservice/configurations/WebMvcConfig.java @@ -0,0 +1,24 @@ +package microservice.configurations; + + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import microservice.interceptors.JWTInterceptor; +import org.springframework.context.annotation.Bean; + + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Bean + public JWTInterceptor jwtInterceptor() { + return new JWTInterceptor(); + } + + @Override + public void addInterceptors(InterceptorRegistry registry){ + registry.addInterceptor(jwtInterceptor()).addPathPatterns("/**"); + } + +} diff --git a/spend_microservice/src/main/java/microservice/controllers/CategoryController.java b/spend_microservice/src/main/java/microservice/controllers/CategoryController.java new file mode 100644 index 00000000..a3c93c7b --- /dev/null +++ b/spend_microservice/src/main/java/microservice/controllers/CategoryController.java @@ -0,0 +1,41 @@ +package microservice.controllers; + + +import java.util.List; +import microservice.models.Category; +import javax.validation.constraints.Size; +import org.springframework.http.ResponseEntity; +import microservice.services.CategoryService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.springframework.http.MediaType; + + +@RestController +@Validated +public class CategoryController { + + @Autowired + private CategoryService categoryService; + + + @RequestMapping(value = "/categories/suggestions", + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity> getSuggestedCategories( + @Size(min=3, message="partial_name should contain at least 3 characters") + @RequestParam(value="partial_name") String partialCategoryName) throws InterruptedException, ExecutionException { + + CompletableFuture> categoryFuture = categoryService.listSimilarCategories(partialCategoryName); + List categoryList = categoryFuture.get(); + return ResponseEntity.ok(categoryList); + + } + +} diff --git a/spend_microservice/src/main/java/microservice/controllers/SpendController.java b/spend_microservice/src/main/java/microservice/controllers/SpendController.java new file mode 100644 index 00000000..3e5e4c74 --- /dev/null +++ b/spend_microservice/src/main/java/microservice/controllers/SpendController.java @@ -0,0 +1,91 @@ +package microservice.controllers; + + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import org.bson.types.ObjectId; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import javax.validation.Valid; +import javax.xml.bind.ValidationException; +import microservice.models.Message; +import microservice.models.Spend; +import org.springframework.http.ResponseEntity; +import java.net.URI; +import java.net.URISyntaxException; +import java.text.ParseException; +import org.springframework.web.util.UriComponentsBuilder; +import microservice.services.SpendService; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestParam; +import javax.servlet.http.HttpServletRequest; +import microservice.models.Category; + + +@RestController +public class SpendController { + + + @Autowired + private SpendService spendService; + + + @RequestMapping(value = "/spends", + method = RequestMethod.POST, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity register(UriComponentsBuilder builder, + @Valid @RequestBody Spend spend) throws URISyntaxException, InterruptedException, ExecutionException { + + CompletableFuture spendFuture = spendService.insert(spend); + Spend storedSpend = spendFuture.get(); + return ResponseEntity + .created(new URI(builder.toUriString() + "/spends/" + spend.get_id())) + .body(storedSpend); + + } + + + @RequestMapping(value = "/users/spends", + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity getUserSpends(HttpServletRequest request, + @RequestParam(value="start_date", defaultValue="") String startDateStr, + @RequestParam(value="end_date", defaultValue="") String endDateStr) + throws ValidationException, InterruptedException, ExecutionException, ParseException { + + CompletableFuture spendFuture = spendService.filterBetweenDates(startDateStr, endDateStr, request.getAttribute("userId").toString()); + Object result = spendFuture.get(); + if (result.getClass() == Message.class) + return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST); + else + return ResponseEntity.ok(result); + } + + + @RequestMapping(value = "/spends/{spendId}/categories", + method = RequestMethod.PATCH, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ResponseEntity updateCategory(UriComponentsBuilder builder, + HttpServletRequest request, + @Valid @RequestBody Category category, + @PathVariable ObjectId spendId) throws URISyntaxException, InterruptedException, ExecutionException { + + CompletableFuture spendFuture = spendService.updateCategory(spendId, request.getAttribute("userId").toString(), category); + Object result = spendFuture.get(); + if (result.getClass() == Message.class) { + Message msg = (Message) result; + return new ResponseEntity<>(msg, msg.getStatus().equals("forbidden") ? HttpStatus.FORBIDDEN : HttpStatus.NOT_FOUND); + } + else { + Spend spend = (Spend) result; + String location = (new URI(builder.toUriString() + "/spends/" + spend.get_id())).toString(); + return ResponseEntity.ok().header("Location", location).body(spend); + } + } + +} diff --git a/spend_microservice/src/main/java/microservice/interceptors/JWTInterceptor.java b/spend_microservice/src/main/java/microservice/interceptors/JWTInterceptor.java new file mode 100644 index 00000000..018e6b1a --- /dev/null +++ b/spend_microservice/src/main/java/microservice/interceptors/JWTInterceptor.java @@ -0,0 +1,100 @@ +package microservice.interceptors; + + +import org.springframework.http.HttpStatus; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; +import java.io.IOException; +import org.springframework.web.client.ResourceAccessException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import microservice.models.Message; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +@Component +public class JWTInterceptor extends HandlerInterceptorAdapter { + + @Value("${usr.auth.endpoint}") + private String USR_AUTH_URL; + + @Value("${sys.auth.endpoint}") + private String SYS_AUTH_URL; + + private static final Logger LOGGER = LoggerFactory.getLogger(JWTInterceptor.class); + + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, PATCH, DELETE, OPTIONS"); + response.setHeader("Access-Control-Max-Age", "6000"); + response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With"); + response.setContentType("application/json"); + response.setCharacterEncoding("UTF-8"); + + String requestMethod = request.getMethod(); + String requestURI = request.getRequestURI(); + + if (!requestMethod.equals("OPTIONS")) { + String token = request.getHeader("Authorization"); + HttpHeaders headers = new HttpHeaders(); + headers.set("Authorization", token); + HttpEntity entity = new HttpEntity<>(headers); + RestTemplate restTemplate = new RestTemplate(); + String url = requestURI.contains("/spends") && requestMethod.equals("POST") ? SYS_AUTH_URL : USR_AUTH_URL; + try { + ResponseEntity authResponse = restTemplate.exchange(url, HttpMethod.GET, entity, Message.class); + Message msg = authResponse.getBody(); + + if (!msg.getStatus().equals("success")) { + formatErrorResponse(response, msg, HttpStatus.UNAUTHORIZED.value()); + LOGGER.error("[" + requestMethod + "] " + requestURI + " - " + msg.getContent()); + return false; + } + + LOGGER.info("[" + requestMethod + "] " + requestURI); + request.setAttribute("userId", msg.getClientId()); + } + catch (HttpClientErrorException e) { + Message errorMsg; + int status; + if (requestURI.contains("/error")) { + errorMsg = new Message("invalid request body. " + e.getMessage(), null, "failed"); + status = HttpStatus.BAD_REQUEST.value(); + } + else { + errorMsg = new Message("invalid access token. " + e.getMessage(), null, "failed"); + status = HttpStatus.UNAUTHORIZED.value(); + } + formatErrorResponse(response, errorMsg, status); + LOGGER.error("[" + requestMethod + "] " + requestURI + " - " + errorMsg.getContent()); + return false; + } + catch (ResourceAccessException e) { + Message errorMsg = new Message("authorization microservice is not available", null, "failed"); + formatErrorResponse(response, errorMsg, HttpStatus.UNAUTHORIZED.value()); + LOGGER.error("[" + requestMethod + "] " + requestURI + " - " + errorMsg.getContent()); + return false; + } + } + return super.preHandle(request, response, handler); + } + + + private void formatErrorResponse(HttpServletResponse response, Message errorMsg, int status) throws IOException { + response.setStatus(status); + response.getWriter().write(errorMsg.toJSONString()); + response.getWriter().flush(); + response.getWriter().close(); + } + +} diff --git a/spend_microservice/src/main/java/microservice/models/Category.java b/spend_microservice/src/main/java/microservice/models/Category.java new file mode 100644 index 00000000..6eebe096 --- /dev/null +++ b/spend_microservice/src/main/java/microservice/models/Category.java @@ -0,0 +1,39 @@ +package microservice.models; + + +import org.springframework.data.annotation.Id; +import javax.validation.constraints.NotNull; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + + +@Document(collection = "categories") +public class Category { + + @Id + private String _id; + + @NotNull + @Indexed(unique = true) + private String category; + + public Category() { } + + public Category(String category) { this.category = category; } + + public Category(String _id, String category) { + this._id = _id; + this.category = category; + } + + public String getCategory() { return category; } + + public void setCategory(String category) { this.category = category; } + + public String get_id() { return _id; } + + public void set_id(String _id) { this._id = _id; } + + + +} diff --git a/spend_microservice/src/main/java/microservice/models/Message.java b/spend_microservice/src/main/java/microservice/models/Message.java new file mode 100644 index 00000000..08a1142a --- /dev/null +++ b/spend_microservice/src/main/java/microservice/models/Message.java @@ -0,0 +1,43 @@ +package microservice.models; + + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + + +public class Message { + + private String content; + private String status; + private String clientId; + + private ObjectMapper mapper = new ObjectMapper(); + + public Message() { + this.status = ""; + this.content = ""; + } + + public Message(String content, String clientId, String status) { + this.content = content; + this.clientId = clientId; + this.status = status; + } + + public String getContent() { return content; } + + public void setContent(String content) { this.content = content; } + + public String getStatus() { return status; } + + public void setStatus(String status) { this.status = status; } + + public String getClientId() { return clientId; } + + public void setClientId(String clientId) { this.clientId = clientId; } + + public String toJSONString() throws JsonProcessingException { + return mapper.writeValueAsString(this); + } + +} diff --git a/spend_microservice/src/main/java/microservice/models/Spend.java b/spend_microservice/src/main/java/microservice/models/Spend.java new file mode 100644 index 00000000..fb6eb929 --- /dev/null +++ b/spend_microservice/src/main/java/microservice/models/Spend.java @@ -0,0 +1,81 @@ +package microservice.models; + +import java.util.Date; +import javax.validation.constraints.NotNull; +import org.springframework.data.annotation.Id; +import com.fasterxml.jackson.annotation.JsonFormat; +import java.math.BigDecimal; +import org.springframework.data.mongodb.core.mapping.Document; + + +@Document(collection = "spends") +public class Spend { + @Id + private String _id; + + @NotNull + private String description; + + @NotNull + private BigDecimal value; + + @NotNull + private String userCode; + + private String category; + + @NotNull + @JsonFormat(shape=JsonFormat.Shape.STRING, + pattern="yyyy-MM-dd'T'HH:mm:ss.SSSZ", + timezone="UTC") + private Date date; + + public Spend() { } + + public Spend(Spend otherSpend) { + this.description = otherSpend.getDescription(); + this.value = otherSpend.getValue(); + this.userCode = otherSpend.getUserCode(); + this.category = otherSpend.getCategory(); + this.date = otherSpend.getDate(); + } + + public Spend(String _id, + String description, + BigDecimal value, + String userCode, + String category, + Date date) { + this._id = _id; + this.description = description; + this.value = value; + this.userCode = userCode; + this.category = category; + this.date = date; + } + + public String get_id() { return _id; } + + public String getDescription() { return description; } + + public BigDecimal getValue() { return value; } + + public String getUserCode() { return userCode; } + + public String getCategory() { return category; } + + public Date getDate() { return date; } + + public void set_id(String _id) { this._id = _id; } + + public void setDescription(String description) { this.description = description; } + + public void setValue(BigDecimal value) { this.value = value.setScale(2, BigDecimal.ROUND_HALF_UP); } + + public void setUserCode(String userCode) { this.userCode = userCode; } + + public void setCategory(String category) { this.category = category; } + + public void setDate(Date date) { this.date = date; } + +} \ No newline at end of file diff --git a/spend_microservice/src/main/java/microservice/repositories/CategoryRepository.java b/spend_microservice/src/main/java/microservice/repositories/CategoryRepository.java new file mode 100644 index 00000000..c47d926b --- /dev/null +++ b/spend_microservice/src/main/java/microservice/repositories/CategoryRepository.java @@ -0,0 +1,14 @@ +package microservice.repositories; + +import java.util.List; +import microservice.models.Category; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; + + +public interface CategoryRepository extends MongoRepository { + + @Query("{ 'category': { $regex: '?0', $options: 'i' } }") + public List findBySimilarName(String category); + +} diff --git a/spend_microservice/src/main/java/microservice/repositories/SpendRepository.java b/spend_microservice/src/main/java/microservice/repositories/SpendRepository.java new file mode 100644 index 00000000..d66c5792 --- /dev/null +++ b/spend_microservice/src/main/java/microservice/repositories/SpendRepository.java @@ -0,0 +1,23 @@ +package microservice.repositories; + + +import java.util.Date; +import java.util.List; +import microservice.models.Spend; +import org.springframework.data.domain.Pageable; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Query; +import org.bson.types.ObjectId; +import org.springframework.data.domain.Page; + + +public interface SpendRepository extends MongoRepository { + + @Query("{ 'date': { '$gte': ?0, '$lte': ?1 }, 'userCode': ?2 }") + public List findByStartAndEndDate(Date startDate, Date endDate, String userCode); + + public Spend findBy_id(ObjectId _id); + + public Page findByDescriptionAndUserCodeAndCategoryNotNull(String description, String userCode, Pageable pageable); + +} diff --git a/spend_microservice/src/main/java/microservice/services/CategoryService.java b/spend_microservice/src/main/java/microservice/services/CategoryService.java new file mode 100644 index 00000000..f414ec48 --- /dev/null +++ b/spend_microservice/src/main/java/microservice/services/CategoryService.java @@ -0,0 +1,25 @@ +package microservice.services; + + +import microservice.repositories.CategoryRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import microservice.models.Category; +import java.util.List; +import java.util.concurrent.CompletableFuture; + + +@Service +public class CategoryService { + + @Autowired + private CategoryRepository categoryRepo; + + @Async("ThreadPoolExecutor") + public CompletableFuture> listSimilarCategories(String partialCategoryName) { + List categories = categoryRepo.findBySimilarName(partialCategoryName); + return CompletableFuture.completedFuture(categories); + } + +} diff --git a/spend_microservice/src/main/java/microservice/services/SpendService.java b/spend_microservice/src/main/java/microservice/services/SpendService.java new file mode 100644 index 00000000..8a80754b --- /dev/null +++ b/spend_microservice/src/main/java/microservice/services/SpendService.java @@ -0,0 +1,105 @@ +package microservice.services; + + +import microservice.repositories.SpendRepository; +import microservice.repositories.CategoryRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import microservice.models.Spend; +import microservice.models.Category; +import org.bson.types.ObjectId; +import microservice.models.Message; +import java.util.concurrent.CompletableFuture; +import javax.xml.bind.ValidationException; +import java.util.Date; +import java.util.Calendar; +import java.text.SimpleDateFormat; +import java.util.TimeZone; +import java.text.ParseException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + + +@Service +public class SpendService { + + @Autowired + private SpendRepository spendRepo; + + @Autowired + private CategoryRepository categoryRepo; + + @Async("ThreadPoolExecutor") + public CompletableFuture insert(Spend spend) { + if (spend.getCategory() != null) { + Category c = new Category(spend.getCategory()); + try { categoryRepo.save(c); } + // do nothing if the category already exists + catch (DuplicateKeyException ignore) { } + + } + + Page storedSpends = spendRepo.findByDescriptionAndUserCodeAndCategoryNotNull(spend.getDescription(), + spend.getUserCode(), + PageRequest.of(0, 1)); + if (storedSpends.getNumberOfElements() > 0) + spend.setCategory(storedSpends.getContent().get(0).getCategory()); + return CompletableFuture.completedFuture(spendRepo.save(spend)); + } + + + @Async("ThreadPoolExecutor") + public CompletableFuture updateCategory(ObjectId spendId, String userId, Category newCategory) { + Spend spend = spendRepo.findBy_id(spendId); + if (spend != null) { + if (spend.getUserCode().equals(userId)) { + try { categoryRepo.save(newCategory); } + // do nothing if the category already exists + catch (DuplicateKeyException ignore) { } + + spend.setCategory(newCategory.getCategory()); + spend = spendRepo.save(spend); + return CompletableFuture.completedFuture(spend); + } + else { + return CompletableFuture.completedFuture(new Message("this spend does not belong to this user", userId, "forbidden")); + } + } + else { + return CompletableFuture.completedFuture(new Message("no spend with the provided spendId", userId, "failed")); + } + } + + @Async("ThreadPoolExecutor") + public CompletableFuture filterBetweenDates(String startDateStr, String endDateStr, String userId) + throws ParseException, ValidationException { + + startDateStr = startDateStr.replace("Z", "+0000").replace(" ", "+"); + endDateStr = endDateStr.replace("Z", "+0000").replace(" ", "+"); + + // getting the current Date as a UTC aware object + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + Date now = calendar.getTime(); + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + Date startDate = startDateStr.trim().isEmpty() ? addDays(now, -7) : dateFormat.parse(startDateStr); + Date endDate = startDateStr.trim().isEmpty() ? now : dateFormat.parse(endDateStr); + + if (startDate.after(endDate)) + return CompletableFuture.completedFuture(new Message("startDate cannot be after endDate", userId, "failed")); + else if (addDays(startDate, 21).before(endDate)) + return CompletableFuture.completedFuture(new Message("the time range between startDate and endDate have to be smaller than 21 days", userId, "failed")); + + return CompletableFuture.completedFuture(spendRepo.findByStartAndEndDate(startDate, endDate, userId)); + } + + + public static Date addDays(Date date, int days){ + Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + cal.setTime(date); + cal.add(Calendar.DATE, days); //minus number would decrement the days + return cal.getTime(); + } +} \ No newline at end of file diff --git a/spend_microservice/src/main/resources/application.properties b/spend_microservice/src/main/resources/application.properties new file mode 100644 index 00000000..fa58a40a --- /dev/null +++ b/spend_microservice/src/main/resources/application.properties @@ -0,0 +1,6 @@ +thread.pool.core.size=7 +thread.pool.max.size=42 +thread.queue.capacity=11 +server.port=8081 +sys.auth.endpoint=http://localhost:8080/systems/authorization +usr.auth.endpoint=http://localhost:8080/users/authorization \ No newline at end of file diff --git a/spend_microservice/src/test/java/microservice/controller_tests/CategoryControllerTest.java b/spend_microservice/src/test/java/microservice/controller_tests/CategoryControllerTest.java new file mode 100644 index 00000000..63527399 --- /dev/null +++ b/spend_microservice/src/test/java/microservice/controller_tests/CategoryControllerTest.java @@ -0,0 +1,67 @@ +package microservice.controller_tests; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +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.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.Arrays; +import java.util.List; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import microservice.controllers.CategoryController; +import microservice.models.Authorization; +import microservice.models.Category; +import microservice.util.AuthRequester; + + +@RunWith(SpringRunner.class) +@WebMvcTest(value = CategoryController.class, secure = false) +public class CategoryControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private CategoryController controller; + + private final ObjectMapper mapper = new ObjectMapper(); + + @Test + public void testJSONResponse() throws Exception { + List mockCategories = Arrays.asList(new Category("5c1afa52dd3b7e2268264e9d", "dummyCategory1"), + new Category("5c1afa82dd3b7e2268264e9f", "dummyCategory2"), + new Category("5c1aff16dd3b7e2698e06a1e", "dummyCategory3")); + + when(controller.getSuggestedCategories(anyString())).thenReturn( + ResponseEntity.ok(mockCategories) + ); + + Authorization authorization = AuthRequester.authenticate( + "http://localhost:8080/users/authentication", + "zanferrari", + "zan12345"); + + String expected = mapper.writeValueAsString(mockCategories); + + this.mockMvc.perform( + get("/categories/suggestions") + .param("partial_name", "cat") + .header("Authorization", authorization.getAccessToken())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json(expected)) + .andExpect(content().string(expected)); + } + + + +} diff --git a/spend_microservice/src/test/java/microservice/controller_tests/SpendControllerTest.java b/spend_microservice/src/test/java/microservice/controller_tests/SpendControllerTest.java new file mode 100644 index 00000000..365c7d23 --- /dev/null +++ b/spend_microservice/src/test/java/microservice/controller_tests/SpendControllerTest.java @@ -0,0 +1,121 @@ +package microservice.controller_tests; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +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.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import javax.servlet.http.HttpServletRequest; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.net.URI; +import java.text.SimpleDateFormat; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import microservice.controllers.SpendController; +import microservice.models.Authorization; +import microservice.models.Spend; +import microservice.util.AuthRequester; +import org.springframework.web.util.UriComponentsBuilder; +import java.math.BigDecimal; + + +@RunWith(SpringRunner.class) +@WebMvcTest(value = SpendController.class, secure = false) +public class SpendControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private SpendController controller; + + private final ObjectMapper mapper = new ObjectMapper(); + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + + @Test + public void testInsertSpendJSONResponse() throws Exception { + String mockDateStr = "2018-12-20T02:32:13.743+0000"; + Date mockDate = dateFormat.parse(mockDateStr); + + Spend mockSpend = new Spend(); + mockSpend.set_id("5c1af62cdd3b7e2014b8b5a4"); + mockSpend.setDescription("dummyDescription"); + mockSpend.setUserCode("5c171d4ba917193e00cf68c4"); + mockSpend.setValue(new BigDecimal(123.456)); + mockSpend.setCategory("dummyCategory"); + mockSpend.setDate(mockDate); + + ResponseEntity mockCreatedResponse = ResponseEntity.created(new URI("http://localhost:8081/spend/5c17215ea917193e0c3c84ab")).body(mockSpend); + + when(controller.register(any(UriComponentsBuilder.class), any(Spend.class))).thenReturn(mockCreatedResponse); + + Authorization authorization = AuthRequester.authenticate( + "http://localhost:8080/systems/authentication", + "mySystem", + "54321naz"); + + String expected = mapper.writeValueAsString(mockSpend); + + String content = "{\"description\":\"dummyDescription\",\"value\":123.456,\"userCode\":\"5c171d4ba917193e00cf68c4\",\"category\":\"dummyCategory\",\"date\":\"2018-12-20T02:32:13.743+0000\"}"; + + this.mockMvc.perform( + post("/spends") + .header("Authorization", authorization.getAccessToken()) + .content(content) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isCreated()) + .andExpect(content().json(expected)) + .andExpect(content().string(expected)); + + } + + @Test + public void testListUserSpendsJSONResponse() throws Exception { + String mockDateStr = "2018-12-18T02:48:26.163+0000"; + String startDateStr = "2018-12-15T02:48:26.163+0000"; + String endDateStr = "2018-12-20T21:51:33.775+0000"; + + Date mockDate = dateFormat.parse(mockDateStr); + + Spend firstSpend = new Spend("5c1afa52dd3b7e2268264e9d", "dummyDescription1", new BigDecimal(123.465), "5c17214ea917193e0c3c84aa", "dummyCategory1", mockDate); + Spend secondSpend = new Spend("5c1afbb5dd3b7e2268264ead", "dummyDescription2", new BigDecimal(879.1011), "5c1aff16dd3b7e2698e06a1e", "dummyCategory2", mockDate); + Spend thirdSpend = new Spend("5c1aff16dd3b7e2698e06a1e", "dummyDescription3", new BigDecimal(1213.1415), "5c1aff42dd3b7e2698e06a22", "dummyCategory3", mockDate); + + List mockSpends = Arrays.asList(firstSpend, secondSpend, thirdSpend); + + when(controller.getUserSpends(any(HttpServletRequest.class), anyString(), anyString())) + .thenReturn(ResponseEntity.ok(mockSpends)); + + Authorization authorization = AuthRequester.authenticate( + "http://localhost:8080/users/authentication", + "zanferrari", + "zan12345"); + + String expected = mapper.writeValueAsString(mockSpends); + + this.mockMvc.perform( + get("/users/spends") + .param("start_date", startDateStr) + .param("end_date", endDateStr) + .header("Authorization", authorization.getAccessToken())) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(content().json(expected)) + .andExpect(content().string(expected)); + + } +} diff --git a/spend_microservice/src/test/java/microservice/models/Authorization.java b/spend_microservice/src/test/java/microservice/models/Authorization.java new file mode 100644 index 00000000..ebf0e567 --- /dev/null +++ b/spend_microservice/src/test/java/microservice/models/Authorization.java @@ -0,0 +1,44 @@ +package microservice.models; + + +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; + + +public class Authorization { + + private String accessToken; + private String status; + private String clientId; + + @JsonFormat(shape=JsonFormat.Shape.STRING, + pattern="yyyy-MM-dd'T'HH:mm:ss.SSSZ", + timezone="UTC") + private Date expiryDate; + + public Authorization() { } + + public Authorization(String accessToken, Date expiryDate, String clientId, String status) { + this.accessToken = accessToken; + this.status = status; + this.expiryDate = expiryDate; + this.clientId = clientId; + } + + public String getAccessToken() { return accessToken; } + + public void setAccessToken(String accessToken) { this.accessToken = accessToken; } + + public Date getExpiryDate() { return expiryDate; } + + public void setExpiryDate(Date expiryDate) { this.expiryDate = expiryDate; } + + public String getStatus() { return status; } + + public void setStatus(String status) { this.status = status; } + + public String getClientId() { return clientId; } + + public void setClientId(String clientId) { this.clientId = clientId; } + +} diff --git a/spend_microservice/src/test/java/microservice/models/Client.java b/spend_microservice/src/test/java/microservice/models/Client.java new file mode 100644 index 00000000..43ab9ade --- /dev/null +++ b/spend_microservice/src/test/java/microservice/models/Client.java @@ -0,0 +1,25 @@ +package microservice.models; + + +public class Client { + + private String username; + + private String password; + + public Client() { } + + public Client(String username, String password) { + this.username = username; + this.password = password; + } + + public String getUsername() { return username; } + + public String getPassword() { return password; } + + public void setUsername(String username) { this.username = username; } + + public void setPassword(String password) { this.password = password; } + +} diff --git a/spend_microservice/src/test/java/microservice/util/AuthRequester.java b/spend_microservice/src/test/java/microservice/util/AuthRequester.java new file mode 100644 index 00000000..e012f672 --- /dev/null +++ b/spend_microservice/src/test/java/microservice/util/AuthRequester.java @@ -0,0 +1,20 @@ +package microservice.util; + +import microservice.models.Authorization; +import microservice.models.Client; +import org.springframework.http.HttpEntity; +import org.springframework.web.client.RestTemplate; +import org.springframework.http.ResponseEntity; + + +public final class AuthRequester { + + public static Authorization authenticate(String authenticationURL, String username, String password) { + HttpEntity request = new HttpEntity<>(new Client(username, password)); + RestTemplate restTemplate = new RestTemplate(); + + ResponseEntity authResponse = restTemplate.postForEntity(authenticationURL, request, Authorization.class); + return authResponse.getBody(); + } + +} \ No newline at end of file