diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..62464086 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target/ +.idea/ +bin +.classpath +.project +.settings/* diff --git a/InterviewSantanderJava-soapui-project.xml b/InterviewSantanderJava-soapui-project.xml new file mode 100644 index 00000000..f92393bd --- /dev/null +++ b/InterviewSantanderJava-soapui-project.xml @@ -0,0 +1,5 @@ + +http://localhost:8080application/json;charset=UTF-8200exp:Responseapplication/json<xml-fragment/>http://localhost:8080{ "description": "asd1231", "value": 12783.23, "userCode": 12283, "date": "2019-09-02T20:21:42.026Z" }http://localhost/expense-management/expensesadminmyadminBasicBasicGlobal HTTP SettingsuserCodeuserCodeTEMPLATEuserCodeapplication/json;charset=UTF-8401ns:Faultapplication/json;charset=UTF-8200ns:Response<xml-fragment/>http://localhost:8080http://localhost/expense-management/expense/userCode/12283clienttheclientBasicBasicGlobal HTTP SettingsuserCodeuserCodeuserCodeTEMPLATEuserCodedatedateTEMPLATEdateapplication/json;charset=UTF-8400ns:Faultapplication/json;charset=UTF-8200ns:Response<xml-fragment/>http://localhost:8080http://localhost/expense-management/expense/userCode/12283/date/02092019clienttheclientBasicBasicGlobal HTTP Settings + + +userCodedateididTEMPLATEidapplication/jsonapplication/json;charset=UTF-8200afa7:Response0data0data<xml-fragment/>http://localhost:8080{ "description": "2134asd", "value": 12351.23, "userCode": 1227883, "date": "2019-09-01T17:21:42.026Z" }http://localhost/expense-management/expense/6655afa7-7679-4098-9927-6c975f67fbd5clienttheclientBasicBasicGlobal HTTP Settingsidapplication/jsonapplication/json;charset=UTF-8200cat:Response<xml-fragment/>http://localhost:8080{ "detail": "teste" }http://localhost/expense-management/categoriesclienttheclientBasicBasicGlobal HTTP SettingsdetailPrefixdetailPrefixTEMPLATEdetailPrefixapplication/json;charset=UTF-8200tes:Response<xml-fragment/>http://localhost:8080http://localhost/expense-management/category/detail/testeclienttheclientBasicBasicGlobal HTTP SettingsdetailPrefixPARALLELLfalse1000250truetrue-1100000COUNTSimple00.5100true500interviewadminmyadmin \ No newline at end of file diff --git a/README.md b/README.md index 15d8f685..184c2169 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,29 @@ -# Show me the code ### # DESAFIO: API REST para Gestão de Gastos! +*** + +#### Configurando o Mongo ``` -Funcionalidade: Integração de gastos por cartão - Apenas sistemas credenciados poderão incluir novos gastos - É esperado um volume de 100.000 inclusões por segundo - Os gastos, serão informados atraves do protoloco JSON, seguindo padrão: - { "descricao": "alfanumerico", "valor": double americano, "codigousuario": numerico, "data": Data dem formato UTC } -``` -``` -Funcionalidade: Listagem de gastos* - Dado que acesso como um cliente autenticado que pode visualizar os gastos do cartão - Quando acesso a interface de listagem de gastos - Então gostaria de ver meus gastos mais atuais. - -*Para esta funcionalidade é esperado 2.000 acessos por segundo. -*O cliente espera ver gastos realizados a 5 segundos atrás. -``` -``` -Funcionalidade: Filtro de gastos - Dado que acesso como um cliente autenticado - E acessei a interface de listagem de gastos - E configure o filtro de data igual a 27/03/1992 - Então gostaria de ver meus gastos apenas deste dia. -``` -``` -Funcionalidade: Categorização de gastos - Dado que acesso como um cliente autenticado - Quando acesso o detalhe de um gasto - E este não possui uma categoria - Então devo conseguir incluir uma categoria para este +docker pull mongo +docker run -d -p 27018:27017 mongo ``` + +#### Executando o projeto ``` -Funcionalidade: Sugestão de categoria - Dado que acesso como um cliente autenticado - Quando acesso o detalhe do gasto que não possui categoria - E começo a digitar a categoria que desejo - Então uma lista de sugestões de categoria deve ser exibida, estas baseadas em categorias já informadas por outro usuários. +mvn install +mvn clean spring-boot:run ``` + +As variáveis de ambiente estão configuradas no arquivo *application.properties*. ``` -Funcionalidade: Categorização automatica de gasto - No processo de integração de gastos, a categoria deve ser incluida automaticamente - caso a descrição de um gasto seja igual a descrição de qualquer outro gasto já categorizado pelo cliente - o mesmo deve receber esta categoria no momento da inclusão do mesmo +mongo.host=localhost +mongo.port=27018 +mongo.database=app1 +thread.async_core_pool_size=5 +thread.async_max_pool_size=50 ``` -### # Avaliação - -Você será avaliado pela usabilidade, por respeitar o design e pela arquitetura da API. -É esperado que você consiga explicar as decisões que tomou durante o desenvolvimento através de commits. - -* Springboot - Java - Maven (preferêncialmente) ([https://projects.spring.io/spring-boot/](https://projects.spring.io/spring-boot/)) -* RESTFul ([https://blog.mwaysolutions.com/2014/06/05/10-best-practices-for-better-restful-api/](https://blog.mwaysolutions.com/2014/06/05/10-best-practices-for-better-restful-api/)) -* DDD ([https://airbrake.io/blog/software-design/domain-driven-design](https://airbrake.io/blog/software-design/domain-driven-design)) -* Microservices ([https://martinfowler.com/microservices/](https://martinfowler.com/microservices/)) -* Testes unitários, teste o que achar importante (De preferência JUnit + Mockito). Mas pode usar o que você tem mais experiência, só nos explique o que ele tem de bom. -* SOAPUI para testes de carga ([https://www.soapui.org/load-testing/concept.html](https://www.soapui.org/load-testing/concept.html)) -* Uso de diferentes formas de armazenamento de dados (REDIS, Cassandra, Solr/Lucene) -* Uso do git -* Diferencial: Criptografia de comunicação, com troca de chaves. ([http://noiseprotocol.org/](http://noiseprotocol.org/)) -* Diferencial: CQRS ([https://martinfowler.com/bliki/CQRS.html](https://martinfowler.com/bliki/CQRS.html)) -* Diferencial: Docker File + Docker Compose (com dbs) para rodar seus jars. - -### # Observações gerais - -Adicione um arquivo [README.md](http://README.md) com os procedimentos para executar o projeto. -Pedimos que trabalhe sozinho e não divulgue o resultado na internet. - -Faça um fork desse desse repositório em seu Github e nos envie um Pull Request com o resultado, por favor informe por qual empresa você esta se candidatando. - -### # Importante: não há prazo de entrega, faça com qualidade! -# BOA SORTE! +O arquivo *InterviewSantanderJava-soapui-project.xml* contém exemplos de chamadas para este projeto. \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..1012a353 --- /dev/null +++ b/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + com.santander.interview + TestBackJava + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.1.6.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.data + spring-data-mongodb + 2.1.9.RELEASE + + + org.springframework.boot + spring-boot-starter-security + + + + io.springfox + springfox-swagger2 + 2.9.2 + + + io.swagger + swagger-models + + + + + io.springfox + springfox-swagger-ui + 2.9.2 + + + io.swagger + swagger-models + 1.5.21 + + + + org.springframework.boot + spring-boot-starter-test + test + + + junit + junit + test + + + + + 1.8 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + 1.8 + 1.8 + UTF-8 + + + + org.springframework.boot + spring-boot-maven-plugin + + com.santander.interview.ExpenseManagement + JAR + + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/santander/interview/ExpenseManagement.java b/src/main/java/com/santander/interview/ExpenseManagement.java new file mode 100644 index 00000000..d28ea1ec --- /dev/null +++ b/src/main/java/com/santander/interview/ExpenseManagement.java @@ -0,0 +1,17 @@ +package com.santander.interview; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.scheduling.annotation.EnableAsync; + +@EnableAsync +@SpringBootApplication +@ComponentScan(basePackages = {"com.santander.interview"}) +public class ExpenseManagement { + + public static void main(String[] args) { + SpringApplication.run(ExpenseManagement.class, args); + } + +} diff --git a/src/main/java/com/santander/interview/SecurityInit.java b/src/main/java/com/santander/interview/SecurityInit.java new file mode 100644 index 00000000..9f467bc3 --- /dev/null +++ b/src/main/java/com/santander/interview/SecurityInit.java @@ -0,0 +1,21 @@ +package com.santander.interview; + +import com.santander.interview.config.SecurityConfig; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; + +@Configuration +public class SecurityInit extends AbstractSecurityWebApplicationInitializer { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + public SecurityInit() { + super(SecurityConfig.class); + } +} diff --git a/src/main/java/com/santander/interview/config/MongoConfig.java b/src/main/java/com/santander/interview/config/MongoConfig.java new file mode 100644 index 00000000..78a905f7 --- /dev/null +++ b/src/main/java/com/santander/interview/config/MongoConfig.java @@ -0,0 +1,33 @@ +package com.santander.interview.config; + +import com.mongodb.MongoClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.config.AbstractMongoConfiguration; + +@Configuration +public class MongoConfig extends AbstractMongoConfiguration { +// @Value("#{environment['MONGO_HOST']}") + @Value("${mongo.host}") + private String MONGO_HOST; + +// @Value("#{environment['MONGO_PORT']}") + @Value("${mongo.port}") + private int MONGO_PORT; + +// @Value("#{environment['MONGO_DATABASE']}") + @Value("${mongo.database}") + private String MONGO_DATABASE; + + @Bean + @Override + public MongoClient mongoClient() { + return new MongoClient(MONGO_HOST, MONGO_PORT); + } + + @Override + protected String getDatabaseName() { + return MONGO_DATABASE; + } +} diff --git a/src/main/java/com/santander/interview/config/SecurityConfig.java b/src/main/java/com/santander/interview/config/SecurityConfig.java new file mode 100644 index 00000000..7ee4f1e1 --- /dev/null +++ b/src/main/java/com/santander/interview/config/SecurityConfig.java @@ -0,0 +1,40 @@ +package com.santander.interview.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + PasswordEncoder passwordEncoder; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable().authorizeRequests() + .antMatchers(HttpMethod.POST, "/expense-management/expenses").hasRole("ADMIN") + .antMatchers("/expense-management/expense/**").hasRole("CLIENT") + .antMatchers("/expense-management/categories").hasRole("CLIENT") + .antMatchers("/expense-management/category/**").hasRole("CLIENT") + .anyRequest().authenticated() + .and().httpBasic(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication() +// .passwordEncoder(passwordEncoder) + .withUser("admin").password(passwordEncoder.encode("myadmin")).roles("ADMIN") +// .withUser("admin").password("{noop}myadmin").roles("ADMIN") + .and() + .withUser("client").password(passwordEncoder.encode("theclient")).roles("CLIENT"); + + } +} diff --git a/src/main/java/com/santander/interview/config/SwaggerConfig.java b/src/main/java/com/santander/interview/config/SwaggerConfig.java new file mode 100644 index 00000000..0588a003 --- /dev/null +++ b/src/main/java/com/santander/interview/config/SwaggerConfig.java @@ -0,0 +1,22 @@ +package com.santander.interview.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + @Bean + public Docket api () { + return new Docket(DocumentationType.SWAGGER_2) + .select() + .apis(RequestHandlerSelectors.any()) + .paths(PathSelectors.ant("/expense-management/**")) + .build(); + } +} diff --git a/src/main/java/com/santander/interview/config/ThreadConfig.java b/src/main/java/com/santander/interview/config/ThreadConfig.java new file mode 100644 index 00000000..8b7959ca --- /dev/null +++ b/src/main/java/com/santander/interview/config/ThreadConfig.java @@ -0,0 +1,38 @@ +package com.santander.interview.config; + +import com.santander.interview.utils.ExpenseManagementUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@Configuration +public class ThreadConfig { + public static final String THREAD_NAME_PREFIX = "interview"; + +// @Value("#{environment['ASYNC_CORE_POOL_SIZE']}") + @Value("${thread.async_core_pool_size}") + private String ASYNC_CORE_POOL_SIZE; + +// @Value("#{environment['ASYNC_MAX_POOL_SIZE']}") + @Value("${thread.async_max_pool_size}") + private String ASYNC_MAX_POOL_SIZE; + + @Bean + public Executor asyncExecutor() { + ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); + threadPoolTaskExecutor.setCorePoolSize( + ExpenseManagementUtils.convertStringtoInt(10, ASYNC_CORE_POOL_SIZE) + ); + threadPoolTaskExecutor.setMaxPoolSize( + ExpenseManagementUtils.convertStringtoInt(100, ASYNC_MAX_POOL_SIZE) + ); + threadPoolTaskExecutor.setQueueCapacity(Integer.MAX_VALUE); + threadPoolTaskExecutor.setThreadNamePrefix(THREAD_NAME_PREFIX); + threadPoolTaskExecutor.initialize(); + + return threadPoolTaskExecutor; + } +} diff --git a/src/main/java/com/santander/interview/controller/CategoryController.java b/src/main/java/com/santander/interview/controller/CategoryController.java new file mode 100644 index 00000000..24baf437 --- /dev/null +++ b/src/main/java/com/santander/interview/controller/CategoryController.java @@ -0,0 +1,49 @@ +package com.santander.interview.controller; + +import static com.santander.interview.enums.ResponseMessageEnum.*; + +import com.santander.interview.domain.Category; +import com.santander.interview.domain.Response; +import com.santander.interview.domain.ResponseObject; +import com.santander.interview.enums.ResponseMessageEnum; +import com.santander.interview.service.CategoryService; +import com.santander.interview.utils.ExpenseManagementUtils; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/expense-management") +@Api(value = "Categoria") +public class CategoryController { + + @Autowired + CategoryService categoryService; + + @ApiOperation("Adicionar uma nova categoria") + @PostMapping("/categories") + public ResponseEntity addCategory( + @ApiParam(value = "Nova categoria", required = true) @RequestBody Category category + ) { + categoryService.saveCategory(category); + + return ExpenseManagementUtils.responseWithoutData(ADD_CATEGORY_SUCCESS, HttpStatus.OK); + } + + @ApiOperation("Sugestão de categoria") + @GetMapping("/category/detail/{detailSubstring}") + public ResponseEntity suggestionCategory( + @ApiParam(value = "Substring da categoria", required = true) @PathVariable String detailSubstring + ) { + List categories = this.categoryService.searchCategoryByDetailSubstring(detailSubstring); + + return ExpenseManagementUtils.responseWithData(SUGGESTION_CATEGORY_SUCCESS, HttpStatus.OK, categories); + } + +} diff --git a/src/main/java/com/santander/interview/controller/ExpenseController.java b/src/main/java/com/santander/interview/controller/ExpenseController.java new file mode 100644 index 00000000..3cb431f2 --- /dev/null +++ b/src/main/java/com/santander/interview/controller/ExpenseController.java @@ -0,0 +1,77 @@ +package com.santander.interview.controller; + +import static com.santander.interview.enums.ResponseMessageEnum.*; + +import com.santander.interview.domain.Expense; +import com.santander.interview.domain.Response; +import com.santander.interview.domain.ResponseObject; +import com.santander.interview.exception.ExpenseException; +import com.santander.interview.service.ExpenseService; +import com.santander.interview.utils.ExpenseManagementUtils; +import io.swagger.annotations.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/expense-management") +@Api(value = "Gasto") +public class ExpenseController { + + @Autowired + ExpenseService expenseService; + + @ApiOperation(value = "Adicionar gasto") + @PostMapping("/expenses") + public ResponseEntity addExpense( + @ApiParam(value = "Novo gasto", required = true) @RequestBody Expense expense + ) { + this.expenseService.addNewExpense(expense); + + return ExpenseManagementUtils.responseWithoutData(ADD_EXPENSE_SUCCESS, HttpStatus.OK); + } + + @ApiOperation("Buscar gastos por usuário") + @GetMapping("/expense/userCode/{userCode}") + public ResponseEntity getExpenseByUserCode( + @ApiParam(value = "Código do cliente", required = true) @PathVariable long userCode + ) { + List expensesByUserCode = this.expenseService.searchExpensesByUserCode(userCode); + + return ExpenseManagementUtils.responseWithData(SEARCH_EXPENSE_BY_USER_CODE_SUCCESS, + HttpStatus.OK, expensesByUserCode); + } + + @ApiOperation("Buscar gastos por usuário e data") + @GetMapping("/expense/userCode/{userCode}/date/{date}") + public ResponseEntity getExpenseByUserCodeAndDate( + @ApiParam(value = "Código do cliente", required = true) @PathVariable long userCode, + @ApiParam(value = "Data a ser pesquisada", required = true) @PathVariable String date + ) { + List expensesByUserCodeAndDate; + try { + expensesByUserCodeAndDate = this.expenseService.searchExpensesByUserCodeAndDate(userCode, date); + return ExpenseManagementUtils.responseWithData(SEARCH_EXPENSE_BY_USER_CODE_AND_DATE_SUCCESS, + HttpStatus.OK, expensesByUserCodeAndDate); + } catch (ExpenseException ee){ + return ExpenseManagementUtils.responseWithError(ee.getResponseMessageEnum(), ee.getStatusCode()); + } + } + + @ApiOperation("Atualizar gasto") + @PutMapping("/expense/{id}") + public ResponseEntity updateExpense( + @ApiParam(value = "ID do gasto a ser atualizado", required = true) @PathVariable String id, + @ApiParam(value = "Gasto atualizado", required = true) @RequestBody Expense expense + ) { + try { + this.expenseService.updateExpense(id, expense); + return ExpenseManagementUtils.responseWithoutData(UPDATE_EXPENSE_SUCCESS, HttpStatus.OK); + } catch (ExpenseException ee) { + return ExpenseManagementUtils.responseWithError(ee.getResponseMessageEnum(), ee.getStatusCode()); + } + } +} diff --git a/src/main/java/com/santander/interview/domain/Category.java b/src/main/java/com/santander/interview/domain/Category.java new file mode 100644 index 00000000..933231ac --- /dev/null +++ b/src/main/java/com/santander/interview/domain/Category.java @@ -0,0 +1,46 @@ +package com.santander.interview.domain; + +import com.santander.interview.repository.CategoryRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.annotation.Id; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.UUID; + +@Component +public class Category { + @Id + private String id; + private String detail; + + @Autowired + CategoryRepository categoryRepository; + + private String generateID() { return UUID.randomUUID().toString(); } + + public Category() { } + + public Category(String detail) { + this.id = this.generateID(); + this.detail = detail; + } + + public String getId() { return id; } + + public void setId(String id) { this.id = id; } + + public String getDetail() { return detail; } + + public void setDetail(String detail) { this.detail = detail; } + + public void save(Category category) { + Category newCategory = new Category(category.getDetail()); + this.categoryRepository.save(newCategory); + } + + public List searchByDetailSubstring(String detailSubstring) { + return this.categoryRepository.findByDetailLike(detailSubstring); + } + +} diff --git a/src/main/java/com/santander/interview/domain/Expense.java b/src/main/java/com/santander/interview/domain/Expense.java new file mode 100644 index 00000000..a17c288b --- /dev/null +++ b/src/main/java/com/santander/interview/domain/Expense.java @@ -0,0 +1,135 @@ +package com.santander.interview.domain; + +import com.santander.interview.repository.CategoryRepository; +import com.santander.interview.repository.ExpenseRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.annotation.Id; +import org.springframework.stereotype.Component; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + + +@Component +public class Expense { + @Id + private String id; + private String description; + private double value; + private long userCode; + private Date date; + private Category category; + + @Autowired + ExpenseRepository expenseRepository; + + @Autowired + CategoryRepository categoryRepository; + + private String generateID() { return UUID.randomUUID().toString(); } + + public Expense() { this.id = this.generateID(); } + + public Expense(String description, double value, long userCode, Date date) { + this.id = UUID.randomUUID().toString(); + this.description = description; + this.value = value; + this.userCode = userCode; + this.date = date; + } + + public Expense(String description, double value, long userCode, Date date, Category category) { + this.id = UUID.randomUUID().toString(); + this.description = description; + this.value = value; + this.userCode = userCode; + this.date = date; + this.category = category; + } + + public String getId() { return id; } + + public void setId(String id) { this.id = id; } + + public String getDescription() { return description; } + + public void setDescription(String description) { this.description = description; } + + public double getValue() { return value; } + + public void setValue(double value) { this.value = value; } + + public long getUserCode() { return userCode; } + + public void setUserCode(long userCode) { this.userCode = userCode; } + + public Date getDate() { return date; } + + public void setDate(Date date) { this.date = date; } + + public Category getCategory() { return category; } + + public void setCategory(Category category) { this.category = category; } + + private Category automaticCategorization(Expense expense) { + Category category = expense.getCategory(); + if(category != null && category.getDetail() != null) { + List list = categoryRepository.findByDetail(category.getDetail()); + if (list.size() > 0) + return list.get(0); + } + return null; + } + + public void add(Expense expense) { + Expense newExpense = new Expense( + expense.getDescription(), + expense.getValue(), + expense.getUserCode(), + expense.getDate() + ); + newExpense.setCategory(this.automaticCategorization(expense)); + this.expenseRepository.save(newExpense); + } + + public List searchByUserCode(long userCode) { + List expenses = expenseRepository.findByUserCode(userCode); + Collections.sort(expenses, new Comparator() { + @Override + public int compare(Expense expense1, Expense expense2) { + return expense2.getDate().compareTo(expense1.getDate()); + } + }); + return expenses; + } + + public List searchByUserCodeAndDate(long userCode, String date) throws ParseException { + long oneDayInMilliseconds = 1000 * 60 * 60 * 24; + + Date startDate = new SimpleDateFormat("ddMMyyyy").parse(date); + Date endDate = new Date(startDate.getTime() + oneDayInMilliseconds); + return this.expenseRepository.findByUserCodeAndDateBetween(userCode, startDate, endDate); + + } + + public boolean update(String id, Expense expense) { + Optional existExpense = this.expenseRepository.findById(id); + if(existExpense.isPresent()) { + Expense expenseFound = existExpense.get(); + expense.setId(expenseFound.getId()); + expense.setCategory(this.automaticCategorization(expense)); + this.expenseRepository.save(expense); + return true; + } + return false; + } + + @Override + public String toString() { + return String.format( + "Expense[id=%s, descricao=%s, valor=%f, codigoUsuario=%d, data=%s]", + this.id, this.description, this.value, this.userCode, this.date.toString() + ); + } +} diff --git a/src/main/java/com/santander/interview/domain/Response.java b/src/main/java/com/santander/interview/domain/Response.java new file mode 100644 index 00000000..87291829 --- /dev/null +++ b/src/main/java/com/santander/interview/domain/Response.java @@ -0,0 +1,28 @@ +package com.santander.interview.domain; + +public class Response { + private long statusCode; + private String userMessage; + private String internalMessage; + + public Response() { } + + public Response(long statusCode, String userMessage, String internalMessage) { + this.statusCode = statusCode; + this.userMessage = userMessage; + this.internalMessage = internalMessage; + } + + public long getStatusCode() { return statusCode; } + + public void setStatusCode(long statusCode) { this.statusCode = statusCode; } + + public String getUserMessage() { return userMessage; } + + public void setUserMessage(String userMessage) { this.userMessage = userMessage; } + + public String getInternalMessage() { return internalMessage; } + + public void setInternalMessage(String internalMessage) { this.internalMessage = internalMessage; } + +} diff --git a/src/main/java/com/santander/interview/domain/ResponseError.java b/src/main/java/com/santander/interview/domain/ResponseError.java new file mode 100644 index 00000000..9f7d70b2 --- /dev/null +++ b/src/main/java/com/santander/interview/domain/ResponseError.java @@ -0,0 +1,14 @@ +package com.santander.interview.domain; + +public class ResponseError extends Response { + int internalCode; + + public ResponseError(long statusCode, String userMessage, String internalMessage, int internalCode) { + super(statusCode, userMessage, internalMessage); + this.internalCode = internalCode; + } + + public int getInternalCode() { return internalCode; } + + public void setInternalCode(int internalCode) { this.internalCode = internalCode; } +} diff --git a/src/main/java/com/santander/interview/domain/ResponseObject.java b/src/main/java/com/santander/interview/domain/ResponseObject.java new file mode 100644 index 00000000..59377419 --- /dev/null +++ b/src/main/java/com/santander/interview/domain/ResponseObject.java @@ -0,0 +1,16 @@ +package com.santander.interview.domain; + +public class ResponseObject extends Response { + private Object data; + + public ResponseObject() { } + + public ResponseObject(long statusCode, String userMessage, String internalMessage, Object data) { + super(statusCode, userMessage, internalMessage); + this.data = data; + } + + public Object getData() { return data; } + + public void setData(Object data) { this.data = data; } +} diff --git a/src/main/java/com/santander/interview/enums/ResponseMessageEnum.java b/src/main/java/com/santander/interview/enums/ResponseMessageEnum.java new file mode 100644 index 00000000..8571da37 --- /dev/null +++ b/src/main/java/com/santander/interview/enums/ResponseMessageEnum.java @@ -0,0 +1,39 @@ +package com.santander.interview.enums; + +public enum ResponseMessageEnum { + UNKNOWN_ERROR(0, "", ""), + ADD_EXPENSE_SUCCESS(1,"Gasto criado com sucesso", "Gasto criado"), +// EXPENSE_NOT_FOUND_TO_USER_CODE(2, "Esse cliente não possui gastos", +// "Não foram encontrados gastos para esse código de usuário"), + SEARCH_EXPENSE_BY_USER_CODE_SUCCESS(3, "Busca pelo cliente realizada com sucesso", + "Gastos do usuário encontrados"), + EXPENSE_BADLY_FORMATTED_DATE(4, "Data mal formatada", + "A data deve estar no formato ddMMyyyy"), + EXPENSE_NOT_FOUND(5, "Gasto não encontrado", + "Não foi encontrado gasto para o ID informado"), + SEARCH_EXPENSE_BY_USER_CODE_AND_DATE_SUCCESS(6, + "Busca realizada com sucesso", + "Busca por código do usuário e pela data realizada com sucesso"), + UPDATE_EXPENSE_SUCCESS(7, "Gasto atualizado com sucesso", + "Gasto encontrado e atualizado"), + ADD_CATEGORY_SUCCESS(8, "Categoria adicionada com sucesso", + "Categoria criada"), + SUGGESTION_CATEGORY_SUCCESS(9, "Busca da categoria realizada com sucesso", + "Lista com categorias"); + + private int code; + private String userMessage; + private String internalMessage; + + private ResponseMessageEnum(int code, String userMessage, String internalMessage) { + this.code = code; + this.userMessage = userMessage; + this.internalMessage = internalMessage; + } + + public int getCode() { return this.code; } + + public String getUserMessage() { return this.userMessage; } + + public String getInternalMessage() { return this.internalMessage; } +} diff --git a/src/main/java/com/santander/interview/exception/ExpenseException.java b/src/main/java/com/santander/interview/exception/ExpenseException.java new file mode 100644 index 00000000..b6a54fec --- /dev/null +++ b/src/main/java/com/santander/interview/exception/ExpenseException.java @@ -0,0 +1,48 @@ +package com.santander.interview.exception; + +import static com.santander.interview.enums.ResponseMessageEnum.*; + +import com.santander.interview.enums.ResponseMessageEnum; +import org.springframework.http.HttpStatus; + +public class ExpenseException extends Exception{ + private HttpStatus statusCode; + private ResponseMessageEnum responseMessageEnum; + private String message; + + public ExpenseException() { + super(); + this.statusCode = HttpStatus.INTERNAL_SERVER_ERROR; + this.message = super.getMessage(); + this.responseMessageEnum = UNKNOWN_ERROR; + } + + public ExpenseException(HttpStatus statusCode, String message, ResponseMessageEnum responseMessageEnum) { + super(message); + this.statusCode = statusCode; + this.message = message; + this.responseMessageEnum = responseMessageEnum; + } + + public ExpenseException(HttpStatus statusCode, ResponseMessageEnum responseMessageEnum) { + super(); + this.statusCode = statusCode; + this.message = super.getMessage(); + this.responseMessageEnum = responseMessageEnum; + } + + public HttpStatus getStatusCode() { return statusCode; } + + public void setStatusCode(HttpStatus statusCode) { this.statusCode = statusCode; } + + @Override + public String getMessage() { return message; } + + public void setMessage(String message) { this.message = message; } + + public ResponseMessageEnum getResponseMessageEnum() { return responseMessageEnum; } + + public void setResponseMessageEnum(ResponseMessageEnum responseMessageEnum) { + this.responseMessageEnum = responseMessageEnum; + } +} diff --git a/src/main/java/com/santander/interview/repository/CategoryRepository.java b/src/main/java/com/santander/interview/repository/CategoryRepository.java new file mode 100644 index 00000000..be18bdc6 --- /dev/null +++ b/src/main/java/com/santander/interview/repository/CategoryRepository.java @@ -0,0 +1,13 @@ +package com.santander.interview.repository; + +import com.santander.interview.domain.Category; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface CategoryRepository extends MongoRepository { + public List findByDetail(String detail); + public List findByDetailLike(String detail); +} diff --git a/src/main/java/com/santander/interview/repository/ExpenseRepository.java b/src/main/java/com/santander/interview/repository/ExpenseRepository.java new file mode 100644 index 00000000..a494c648 --- /dev/null +++ b/src/main/java/com/santander/interview/repository/ExpenseRepository.java @@ -0,0 +1,14 @@ +package com.santander.interview.repository; + +import com.santander.interview.domain.Expense; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.Date; +import java.util.List; + +@Repository +public interface ExpenseRepository extends MongoRepository { + public List findByUserCode(long userCode); + public List findByUserCodeAndDateBetween(long userCode, Date startData, Date endData); +} diff --git a/src/main/java/com/santander/interview/service/CategoryService.java b/src/main/java/com/santander/interview/service/CategoryService.java new file mode 100644 index 00000000..1969ba2c --- /dev/null +++ b/src/main/java/com/santander/interview/service/CategoryService.java @@ -0,0 +1,10 @@ +package com.santander.interview.service; + +import com.santander.interview.domain.Category; + +import java.util.List; + +public interface CategoryService { + public void saveCategory(Category newCategory); + public List searchCategoryByDetailSubstring(String detailSubstring); +} diff --git a/src/main/java/com/santander/interview/service/ExpenseService.java b/src/main/java/com/santander/interview/service/ExpenseService.java new file mode 100644 index 00000000..58143f51 --- /dev/null +++ b/src/main/java/com/santander/interview/service/ExpenseService.java @@ -0,0 +1,13 @@ +package com.santander.interview.service; + +import com.santander.interview.domain.Expense; +import com.santander.interview.exception.ExpenseException; + +import java.util.List; + +public interface ExpenseService { + public void addNewExpense(Expense newExpense); + public List searchExpensesByUserCode(long userCode); + public List searchExpensesByUserCodeAndDate(long userCode, String date) throws ExpenseException; + public void updateExpense(String id, Expense newExpense) throws ExpenseException; +} diff --git a/src/main/java/com/santander/interview/service/impl/CategoryServiceImpl.java b/src/main/java/com/santander/interview/service/impl/CategoryServiceImpl.java new file mode 100644 index 00000000..7d27ef7e --- /dev/null +++ b/src/main/java/com/santander/interview/service/impl/CategoryServiceImpl.java @@ -0,0 +1,24 @@ +package com.santander.interview.service.impl; + +import com.santander.interview.domain.Category; +import com.santander.interview.service.CategoryService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class CategoryServiceImpl implements CategoryService { + @Autowired + Category category; + + @Override + public void saveCategory(Category newCategory) { + this.category.save(newCategory); + } + + @Override + public List searchCategoryByDetailSubstring(String detailSubstring) { + return this.category.searchByDetailSubstring(detailSubstring); + } +} diff --git a/src/main/java/com/santander/interview/service/impl/ExpenseServiceImpl.java b/src/main/java/com/santander/interview/service/impl/ExpenseServiceImpl.java new file mode 100644 index 00000000..d0cc29b1 --- /dev/null +++ b/src/main/java/com/santander/interview/service/impl/ExpenseServiceImpl.java @@ -0,0 +1,47 @@ +package com.santander.interview.service.impl; + +import static com.santander.interview.enums.ResponseMessageEnum.*; + +import com.santander.interview.domain.Expense; +import com.santander.interview.exception.ExpenseException; +import com.santander.interview.service.ExpenseService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.text.ParseException; +import java.util.*; + +@Service +public class ExpenseServiceImpl implements ExpenseService { + @Autowired + Expense expense; + + @Async + @Override + public void addNewExpense(Expense newExpense) { + this.expense.add(newExpense); + } + + @Override + public List searchExpensesByUserCode(long userCode) { + return this.expense.searchByUserCode(userCode); + } + + @Override + public List searchExpensesByUserCodeAndDate(long userCode, String date) throws ExpenseException { + try { + return this.expense.searchByUserCodeAndDate(userCode, date); + } catch(ParseException pe) { + throw new ExpenseException(HttpStatus.BAD_REQUEST, EXPENSE_BADLY_FORMATTED_DATE); + } + } + + @Override + public void updateExpense(String id, Expense newExpense) throws ExpenseException { + if (!this.expense.update(id, newExpense)) { + throw new ExpenseException(HttpStatus.NOT_FOUND, EXPENSE_NOT_FOUND); + } + } +} diff --git a/src/main/java/com/santander/interview/utils/ExpenseManagementUtils.java b/src/main/java/com/santander/interview/utils/ExpenseManagementUtils.java new file mode 100644 index 00000000..2b03205c --- /dev/null +++ b/src/main/java/com/santander/interview/utils/ExpenseManagementUtils.java @@ -0,0 +1,56 @@ +package com.santander.interview.utils; + +import com.santander.interview.domain.Response; +import com.santander.interview.domain.ResponseError; +import com.santander.interview.domain.ResponseObject; +import com.santander.interview.enums.ResponseMessageEnum; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class ExpenseManagementUtils { + + public static int convertStringtoInt(int defaultValue, String value) { + try { + return Integer.valueOf(value); + } catch (Exception e) { + return defaultValue; + } + } + + public static ResponseEntity responseWithData (ResponseMessageEnum responseMessageEnum, + HttpStatus httpStatus, + Object data) { + return new ResponseEntity<>( + new ResponseObject( + httpStatus.value(), + responseMessageEnum.getUserMessage(), + responseMessageEnum.getInternalMessage(), + data + ), + httpStatus); + } + + public static ResponseEntity responseWithoutData (ResponseMessageEnum responseMessageEnum, + HttpStatus httpStatus) { + return new ResponseEntity<>( + new Response( + httpStatus.value(), + responseMessageEnum.getUserMessage(), + responseMessageEnum.getInternalMessage() + ), + httpStatus); + } + + public static ResponseEntity responseWithError (ResponseMessageEnum responseMessageEnum, + HttpStatus httpStatus) { + return new ResponseEntity<>( + new ResponseError( + httpStatus.value(), + responseMessageEnum.getUserMessage(), + responseMessageEnum.getInternalMessage(), + responseMessageEnum.getCode() + ), + httpStatus); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 00000000..893fd39e --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,6 @@ +mongo.host=localhost +mongo.port=27018 +mongo.database=app1 +thread.async_core_pool_size=5 +thread.async_max_pool_size=50 +info.app.version=@project.version@ \ No newline at end of file diff --git a/src/test/java/com/santander/interview/config/MongoConfigTest.java b/src/test/java/com/santander/interview/config/MongoConfigTest.java new file mode 100644 index 00000000..fe3dff85 --- /dev/null +++ b/src/test/java/com/santander/interview/config/MongoConfigTest.java @@ -0,0 +1,21 @@ +package com.santander.interview.config; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.InjectMocks; + +public class MongoConfigTest { + @InjectMocks + MongoConfig mongoConfig = new MongoConfig(); + + @Test + public void mongoClientTest() { + Assert.assertNotNull(mongoConfig.mongoClient()); + } + + @Test + public void mongoDatabaseTest() { + Assert.assertNull(mongoConfig.getDatabaseName()); + } + +} diff --git a/src/test/java/com/santander/interview/controller/CategoryControllerTest.java b/src/test/java/com/santander/interview/controller/CategoryControllerTest.java new file mode 100644 index 00000000..a5283bed --- /dev/null +++ b/src/test/java/com/santander/interview/controller/CategoryControllerTest.java @@ -0,0 +1,58 @@ +package com.santander.interview.controller; + +import static com.santander.interview.enums.ResponseMessageEnum.*; + +import com.santander.interview.domain.Category; +import com.santander.interview.domain.Response; +import com.santander.interview.domain.ResponseObject; +import com.santander.interview.service.CategoryService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.ArrayList; +import java.util.List; + + +public class CategoryControllerTest { + private static final String CATEGORY_DETAIL = "Detail"; + private Category category; + + @InjectMocks + CategoryController categoryController = new CategoryController(); + + @Mock + CategoryService categoryService; + + @Before + public void init() { + category = new Category(CATEGORY_DETAIL); + MockitoAnnotations.initMocks(this); + } + + @Test + public void suggestionCategoryTest() { + String detailSubstring = "teste"; + List categories = new ArrayList<>(); + Mockito.when(categoryService.searchCategoryByDetailSubstring(detailSubstring)).thenReturn(categories); + ResponseEntity response = categoryController.suggestionCategory(detailSubstring); + Assert.assertEquals(response.getStatusCode(), HttpStatus.OK); + Assert.assertNotNull(response.getBody().getData()); + } + + @Test + public void addCategoryTest() { + ResponseEntity response = categoryController.addCategory(category); + Assert.assertEquals(response.getStatusCode(), HttpStatus.OK); + Assert.assertEquals(response.getBody().getStatusCode(), HttpStatus.OK.value()); + Assert.assertEquals(response.getBody().getUserMessage(), ADD_CATEGORY_SUCCESS.getUserMessage()); + Assert.assertEquals(response.getBody().getInternalMessage(), ADD_CATEGORY_SUCCESS.getInternalMessage()); + } + +} diff --git a/src/test/java/com/santander/interview/controller/ExpenseControllerTest.java b/src/test/java/com/santander/interview/controller/ExpenseControllerTest.java new file mode 100644 index 00000000..f5d6b6c1 --- /dev/null +++ b/src/test/java/com/santander/interview/controller/ExpenseControllerTest.java @@ -0,0 +1,112 @@ +package com.santander.interview.controller; + +import static com.santander.interview.enums.ResponseMessageEnum.*; + +import com.santander.interview.domain.Expense; +import com.santander.interview.domain.Response; +import com.santander.interview.domain.ResponseObject; +import com.santander.interview.exception.ExpenseException; +import com.santander.interview.service.ExpenseService; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +public class ExpenseControllerTest { + private static final String DESCRIPTION = "descricao"; + private static final Double VALUE = 198.23; + private static final long USER_CODE = 129; + private static final Date DATE = new Date(); + private static final String INCORRECT_DATE = "121251"; + + @InjectMocks + ExpenseController expenseController = new ExpenseController(); + + @Mock + ExpenseService expenseService; + + Expense expense; + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + this.expense = new Expense(DESCRIPTION, VALUE, USER_CODE, DATE); + } + + @Test + public void addExpenseTest() { + ResponseEntity response = this.expenseController.addExpense(this.expense); + Assert.assertEquals(response.getStatusCode(), HttpStatus.OK); + Assert.assertEquals(response.getBody().getStatusCode(), HttpStatus.OK.value()); + Assert.assertEquals(response.getBody().getInternalMessage(), ADD_EXPENSE_SUCCESS.getInternalMessage()); + Assert.assertEquals(response.getBody().getUserMessage(), ADD_EXPENSE_SUCCESS.getUserMessage()); + } + + @Test + public void getExpenseByUserCodeTest() { + long userCode = USER_CODE; + List list = new ArrayList<>(); + list.add(this.expense); + + Mockito.when(expenseService.searchExpensesByUserCode(userCode)).thenReturn(list); + + ResponseEntity response = this.expenseController.getExpenseByUserCode(userCode); + Assert.assertEquals(response.getStatusCode(), HttpStatus.OK); + Assert.assertEquals(response.getBody().getStatusCode(), HttpStatus.OK.value()); + Assert.assertEquals(response.getBody().getData(), list); + Assert.assertEquals(response.getBody().getInternalMessage(), SEARCH_EXPENSE_BY_USER_CODE_SUCCESS.getInternalMessage()); + Assert.assertEquals(response.getBody().getUserMessage(), SEARCH_EXPENSE_BY_USER_CODE_SUCCESS.getUserMessage()); + } + + @Test + public void getExpenseByUserCodeAndDateTest() throws ExpenseException { + long userCode = USER_CODE; + String date = DATE.toString(); + List list = new ArrayList<>(); + list.add(this.expense); + + Mockito.when(this.expenseService.searchExpensesByUserCodeAndDate(userCode, date)).thenReturn(list); + + ResponseEntity response = this.expenseController.getExpenseByUserCodeAndDate(userCode, date); + Assert.assertEquals(response.getStatusCode(), HttpStatus.OK); + Assert.assertNotNull(response.getBody()); + } + + @Test + public void getExpenseByUserCodeAndDate_WithDataIncorrectTest() throws ExpenseException { + long userCode = USER_CODE; + String date = INCORRECT_DATE; + Mockito.when(this.expenseService.searchExpensesByUserCodeAndDate(userCode, date)) + .thenThrow(new ExpenseException(HttpStatus.BAD_REQUEST, EXPENSE_BADLY_FORMATTED_DATE)); + ResponseEntity response = this.expenseController.getExpenseByUserCodeAndDate(userCode, date); + Assert.assertEquals(response.getStatusCode(), HttpStatus.BAD_REQUEST); + Assert.assertNotNull(response.getBody()); + } + + @Test + public void updateExpenseTest() { + String id = expense.getId(); + ResponseEntity response = this.expenseController.updateExpense(id, expense); + Assert.assertEquals(response.getStatusCode(), HttpStatus.OK); + Assert.assertNotNull(response.getBody()); + } + + @Test + public void updateExpense_ExpenseWithoutIdTest() throws ExpenseException { + String id = expense.getId(); + Mockito.doThrow(new ExpenseException(HttpStatus.NOT_FOUND, EXPENSE_NOT_FOUND)) + .when(this.expenseService).updateExpense(id, expense); + ResponseEntity response = this.expenseController.updateExpense(id, expense); + Assert.assertEquals(response.getStatusCode(), HttpStatus.NOT_FOUND); + Assert.assertNotNull(response.getBody()); + } +} diff --git a/src/test/java/com/santander/interview/domain/CategoryTest.java b/src/test/java/com/santander/interview/domain/CategoryTest.java new file mode 100644 index 00000000..9f444a5c --- /dev/null +++ b/src/test/java/com/santander/interview/domain/CategoryTest.java @@ -0,0 +1,74 @@ +package com.santander.interview.domain; + +import com.santander.interview.repository.CategoryRepository; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(MockitoJUnitRunner.class) +public class CategoryTest { + private static final String ID = "123"; + private static final String DETAIL = "teste"; + List categoriesResult; + Category categoryObject; + + @InjectMocks + Category categoryDomain = new Category(); + + @Mock + CategoryRepository categoryRepository; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + this.categoryObject = new Category(DETAIL); + this.categoriesResult = new ArrayList<>(); + this.categoriesResult.add(this.categoryObject); + } + + @Test + public void category_EmptyConstructorTest() { + Category category = new Category(); + category.setId(ID); + category.setDetail(DETAIL); + + Assert.assertEquals(category.getId(), ID); + Assert.assertEquals(category.getDetail(), DETAIL); + } + + @Test + public void category_ConstructorWithParamsTest() { + Category category = new Category(DETAIL); + Assert.assertEquals(category.getDetail(), DETAIL); + } + + @Test + public void saveTest() { + boolean isOk = true; + try { + this.categoryDomain.save(this.categoryObject); + } catch (Exception e) { + isOk = false; + } + Assert.assertTrue(isOk); + } + + @Test + public void searchByDetailSubstringTest() { + Mockito.when(categoryRepository.findByDetailLike(this.categoryObject.getDetail())) + .thenReturn(this.categoriesResult); + + String substring = this.categoryObject.getDetail(); + List result = this.categoryDomain.searchByDetailSubstring(substring); + Assert.assertEquals(result, categoriesResult); + } +} diff --git a/src/test/java/com/santander/interview/domain/ExpenseTest.java b/src/test/java/com/santander/interview/domain/ExpenseTest.java new file mode 100644 index 00000000..0df77d2f --- /dev/null +++ b/src/test/java/com/santander/interview/domain/ExpenseTest.java @@ -0,0 +1,174 @@ +package com.santander.interview.domain; + +import com.santander.interview.enums.ResponseMessageEnum; +import com.santander.interview.exception.ExpenseException; +import com.santander.interview.repository.CategoryRepository; +import com.santander.interview.repository.ExpenseRepository; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; + +import java.text.ParseException; +import java.util.*; + +@RunWith(MockitoJUnitRunner.class) +public class ExpenseTest { + private static final String ID = "123"; + private static final String DESCRIPTION = "descricao"; + private static final double VALUE = 124.2; + private static final long USER_CODE = 142; + private static final Date DATE = new Date(); + private static final String CATEGORY_DETAIL = "teste"; + private static final Category CATEGORY = new Category(CATEGORY_DETAIL); + private static final String DATE_STRING = "01102019"; + private static final String DATE_STRING_INVALID = "011"; + private static final String DETAIL = "Detalhes"; + + @InjectMocks + Expense expenseDomain = new Expense(); + + @Mock + ExpenseRepository expenseRepository; + + @Mock + CategoryRepository categoryRepository; + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void expenseEmptyConstructorTest() { + String expectedToString = String.format( + "Expense[id=%s, descricao=%s, valor=%f, codigoUsuario=%d, data=%s]", + ID, DESCRIPTION, VALUE, USER_CODE, DATE + ); + Expense expense = new Expense(); + expense.setId(ID); + expense.setCategory(CATEGORY); + expense.setDescription(DESCRIPTION); + expense.setValue(VALUE); + expense.setUserCode(USER_CODE); + expense.setDate(DATE); + + Assert.assertEquals(expense.getId(), ID); + Assert.assertEquals(expense.getCategory(), CATEGORY); + Assert.assertEquals(expense.getUserCode(), USER_CODE); + Assert.assertEquals(expense.getDate(), DATE); + Assert.assertEquals(expense.getDescription(), DESCRIPTION); + Assert.assertTrue(expense.getValue() == VALUE); + Assert.assertEquals(expense.toString(), expectedToString); + } + + @Test + public void expenseConstrutorWithAllAttrTest() { + Expense expense = new Expense(DESCRIPTION, VALUE, USER_CODE, DATE, CATEGORY); + + Assert.assertEquals(expense.getCategory(), CATEGORY); + Assert.assertEquals(expense.getUserCode(), USER_CODE); + Assert.assertEquals(expense.getDate(), DATE); + Assert.assertEquals(expense.getDescription(), DESCRIPTION); + Assert.assertTrue(expense.getValue() == VALUE); + } + + @Test + public void expenseConstrutorWithoutCategoryAttrTest() { + Expense expense = new Expense(DESCRIPTION, VALUE, USER_CODE, DATE); + + Assert.assertEquals(expense.getUserCode(), USER_CODE); + Assert.assertEquals(expense.getDate(), DATE); + Assert.assertEquals(expense.getDescription(), DESCRIPTION); + Assert.assertTrue(expense.getValue() == VALUE); + } + + + @Test + public void add_WithCategoryNullTest() { + List expenses = new ArrayList<>(); + Expense expense = new Expense(DESCRIPTION, VALUE, USER_CODE, DATE); + expenses.add(expense); + + Mockito.when(this.expenseRepository.findByUserCode(USER_CODE)).thenReturn(expenses); + + this.expenseDomain.add(expense); + List result = this.expenseDomain.searchByUserCode(USER_CODE); + Assert.assertEquals(result, expenses); + } + + @Test + public void add_WithExpenseWithCategoryTest() { + boolean isOk = true; + Expense expense = new Expense(DESCRIPTION, VALUE, USER_CODE, DATE); + Category category = new Category(); + List categories = new ArrayList<>(); + category.setDetail(DETAIL); + expense.setCategory(category); + categories.add(new Category(DETAIL)); + + Mockito.when(categoryRepository.findByDetail(category.getDetail())).thenReturn(categories); + try { + this.expenseDomain.add(expense); + } catch (Exception e) { + isOk = false; + } + + Assert.assertTrue(isOk); + } + + @Test + public void searchByUserCodeAndDateTest() throws ParseException { + List expenses = new ArrayList<>(); + Expense expense = new Expense(DESCRIPTION, VALUE, USER_CODE, DATE); + expenses.add(expense); + + Assert.assertNotNull(this.expenseDomain.searchByUserCodeAndDate(USER_CODE, DATE_STRING)); + } + + @Test + public void searchByUserCodeAndDate_InvalidDataTest() { + List expenses; + boolean parseException = false; + + try { + expenses = this.expenseDomain.searchByUserCodeAndDate(USER_CODE, DATE_STRING_INVALID); + } catch (ParseException pe) { + parseException = true; + } + + Assert.assertTrue(parseException); + } + + @Test + public void update_FoundExpenseTest() throws ExpenseException { + boolean isOk = true; + String uuid = UUID.randomUUID().toString(); + Expense expense = new Expense(); + Optional optionalExpense = Optional.of(expense); + Mockito.when(this.expenseRepository.findById(uuid)).thenReturn(optionalExpense); + + try { + this.expenseDomain.update(uuid, expense); + } catch (Exception e) { + isOk = false; + } + + Assert.assertTrue(isOk); + } + + @Test + public void update_NotFoundExpenseTest() { + String uuid = UUID.randomUUID().toString(); + Expense expense = new Expense(); + Optional optionalExpense = Optional.empty(); + Mockito.when(this.expenseRepository.findById(uuid)).thenReturn(optionalExpense); + + Assert.assertFalse(this.expenseDomain.update(uuid, expense)); + } +} diff --git a/src/test/java/com/santander/interview/domain/ResponseErrorTest.java b/src/test/java/com/santander/interview/domain/ResponseErrorTest.java new file mode 100644 index 00000000..61113753 --- /dev/null +++ b/src/test/java/com/santander/interview/domain/ResponseErrorTest.java @@ -0,0 +1,24 @@ +package com.santander.interview.domain; + +import org.junit.Assert; +import org.junit.Test; + +public class ResponseErrorTest { + private static final long STATUS_CODE = 12; + private static final String USER_MESSAGE = "userMessage"; + private static final String INTERNAL_MESSAGE = "internalMessage"; + private static final int INTERNAL_CODE = 1; + private static final int INTERNAL_CODE_2 = 3; + + @Test + public void responseErrorTest(){ + ResponseError responseError = new ResponseError(STATUS_CODE, USER_MESSAGE, INTERNAL_MESSAGE, INTERNAL_CODE); + Assert.assertEquals(responseError.getStatusCode(), STATUS_CODE); + Assert.assertEquals(responseError.getUserMessage(), USER_MESSAGE); + Assert.assertEquals(responseError.getInternalMessage(), INTERNAL_MESSAGE); + Assert.assertEquals(responseError.getInternalCode(), INTERNAL_CODE); + + responseError.setInternalCode(INTERNAL_CODE_2); + Assert.assertEquals(responseError.getInternalCode(), INTERNAL_CODE_2); + } +} diff --git a/src/test/java/com/santander/interview/domain/ResponseObjectTest.java b/src/test/java/com/santander/interview/domain/ResponseObjectTest.java new file mode 100644 index 00000000..91a86897 --- /dev/null +++ b/src/test/java/com/santander/interview/domain/ResponseObjectTest.java @@ -0,0 +1,37 @@ +package com.santander.interview.domain; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ResponseObjectTest { + private static final long STATUS_CODE = 123; + private static final String USER_MESSAGE = "userMessage"; + private static final String INTERNAL_MESSAGE = "internalMessage"; + private static final Object DATA = new Object(); + + @Test + public void responseEmptyConstTest() { + ResponseObject responseObject = new ResponseObject(); + responseObject.setData(DATA); + responseObject.setUserMessage(USER_MESSAGE); + responseObject.setInternalMessage(INTERNAL_MESSAGE); + responseObject.setStatusCode(STATUS_CODE); + + Assert.assertEquals(responseObject.getData(), DATA); + Assert.assertEquals(responseObject.getUserMessage(), USER_MESSAGE); + Assert.assertEquals(responseObject.getInternalMessage(), INTERNAL_MESSAGE); + Assert.assertEquals(responseObject.getStatusCode(), STATUS_CODE); + } + + @Test + public void responseConstructorWithParamsTest() { + ResponseObject responseObject = new ResponseObject(STATUS_CODE, USER_MESSAGE, INTERNAL_MESSAGE, DATA); + Assert.assertEquals(responseObject.getStatusCode(), STATUS_CODE); + Assert.assertEquals(responseObject.getUserMessage(), USER_MESSAGE); + Assert.assertEquals(responseObject.getInternalMessage(), INTERNAL_MESSAGE); + Assert.assertEquals(responseObject.getData(), DATA); + } +} diff --git a/src/test/java/com/santander/interview/domain/ResponseTest.java b/src/test/java/com/santander/interview/domain/ResponseTest.java new file mode 100644 index 00000000..79e8945a --- /dev/null +++ b/src/test/java/com/santander/interview/domain/ResponseTest.java @@ -0,0 +1,31 @@ +package com.santander.interview.domain; + +import org.junit.Assert; +import org.junit.Test; + +public class ResponseTest { + private static final long STATUS_CODE = 12; + private static final String USER_MESSAGE = "userMessage"; + private static final String INTERNAL_MESSAGE = "internalMessage"; + + @Test + public void responseEmptyConstructorTest() { + Response response = new Response(); + response.setStatusCode(STATUS_CODE); + response.setInternalMessage(INTERNAL_MESSAGE); + response.setUserMessage(USER_MESSAGE); + + Assert.assertEquals(response.getStatusCode(), STATUS_CODE); + Assert.assertEquals(response.getUserMessage(), USER_MESSAGE); + Assert.assertEquals(response.getInternalMessage(), INTERNAL_MESSAGE); + } + + @Test + public void responseWithAllConstructorAttrTest() { + Response response = new Response(STATUS_CODE, USER_MESSAGE, INTERNAL_MESSAGE); + + Assert.assertEquals(response.getStatusCode(), STATUS_CODE); + Assert.assertEquals(response.getUserMessage(), USER_MESSAGE); + Assert.assertEquals(response.getInternalMessage(), INTERNAL_MESSAGE); + } +} diff --git a/src/test/java/com/santander/interview/enums/ResponseMessageEnumTest.java b/src/test/java/com/santander/interview/enums/ResponseMessageEnumTest.java new file mode 100644 index 00000000..1f01e500 --- /dev/null +++ b/src/test/java/com/santander/interview/enums/ResponseMessageEnumTest.java @@ -0,0 +1,15 @@ +package com.santander.interview.enums; + +import org.junit.Assert; +import org.junit.Test; + +import static com.santander.interview.enums.ResponseMessageEnum.*; + +public class ResponseMessageEnumTest { + @Test + public void responseMessageEnumTest() { + Assert.assertNotNull(UNKNOWN_ERROR.getUserMessage()); + Assert.assertNotNull(UNKNOWN_ERROR.getInternalMessage()); + Assert.assertNotNull(UNKNOWN_ERROR.getCode()); + } +} diff --git a/src/test/java/com/santander/interview/exception/ExpenseExceptionTest.java b/src/test/java/com/santander/interview/exception/ExpenseExceptionTest.java new file mode 100644 index 00000000..a68e496d --- /dev/null +++ b/src/test/java/com/santander/interview/exception/ExpenseExceptionTest.java @@ -0,0 +1,41 @@ +package com.santander.interview.exception; + +import static com.santander.interview.enums.ResponseMessageEnum.*; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.http.HttpStatus; + +public class ExpenseExceptionTest { + private static final String MESSAGE = "teste"; + + @Test + public void expenseExceptionConstructor1Test() { + ExpenseException ee = new ExpenseException(); + Assert.assertEquals(ee.getStatusCode(), HttpStatus.INTERNAL_SERVER_ERROR); + Assert.assertEquals(ee.getResponseMessageEnum(), UNKNOWN_ERROR); + } + + @Test + public void expenseExceptionConstructor2Test() { + ExpenseException ee = new ExpenseException(HttpStatus.OK, MESSAGE, EXPENSE_BADLY_FORMATTED_DATE); + Assert.assertEquals(ee.getStatusCode(), HttpStatus.OK); + Assert.assertEquals(ee.getResponseMessageEnum(), EXPENSE_BADLY_FORMATTED_DATE); + Assert.assertEquals(ee.getMessage(), MESSAGE); + } + + @Test + public void expenseExceptionConstructor3Test() { + ExpenseException ee = new ExpenseException(HttpStatus.OK, EXPENSE_BADLY_FORMATTED_DATE); + Assert.assertEquals(ee.getStatusCode(), HttpStatus.OK); + Assert.assertEquals(ee.getResponseMessageEnum(), EXPENSE_BADLY_FORMATTED_DATE); + + ee.setMessage(MESSAGE); + ee.setResponseMessageEnum(UNKNOWN_ERROR); + ee.setStatusCode(HttpStatus.BAD_GATEWAY); + + Assert.assertEquals(ee.getMessage(), MESSAGE); + Assert.assertEquals(ee.getResponseMessageEnum(), UNKNOWN_ERROR); + Assert.assertEquals(ee.getStatusCode(), HttpStatus.BAD_GATEWAY); + } +} diff --git a/src/test/java/com/santander/interview/service/CategoryServiceTest.java b/src/test/java/com/santander/interview/service/CategoryServiceTest.java new file mode 100644 index 00000000..e34e0d9f --- /dev/null +++ b/src/test/java/com/santander/interview/service/CategoryServiceTest.java @@ -0,0 +1,59 @@ +package com.santander.interview.service; + +import com.santander.interview.domain.Category; +import com.santander.interview.service.impl.CategoryServiceImpl; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(MockitoJUnitRunner.class) +public class CategoryServiceTest { + private static final String DETAIL = "teste"; + + @InjectMocks + CategoryServiceImpl categoryService = new CategoryServiceImpl(); + + @Mock + Category categoryDomain; + + Category categoryObject; + List categoriesResult; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + this.categoryObject = new Category(DETAIL); + this.categoriesResult = new ArrayList<>(); + this.categoriesResult.add(this.categoryObject); + } + + @Test + public void saveCategoryTest() { + boolean isOk = true; + try { + this.categoryService.saveCategory(categoryObject); + } catch (Exception e) { + isOk = false; + } + Assert.assertTrue(isOk); + } + + @Test + public void searchCategoryByDetailSubstringTest() { + Mockito.when(categoryDomain.searchByDetailSubstring(DETAIL)) + .thenReturn(this.categoriesResult); + + List categories = this.categoryService.searchCategoryByDetailSubstring(DETAIL); + Assert.assertEquals(categories, this.categoriesResult); + } + +} diff --git a/src/test/java/com/santander/interview/service/ExpenseServiceTest.java b/src/test/java/com/santander/interview/service/ExpenseServiceTest.java new file mode 100644 index 00000000..dafac6ac --- /dev/null +++ b/src/test/java/com/santander/interview/service/ExpenseServiceTest.java @@ -0,0 +1,108 @@ +package com.santander.interview.service; + +import com.santander.interview.domain.Expense; +import com.santander.interview.enums.ResponseMessageEnum; +import com.santander.interview.exception.ExpenseException; +import com.santander.interview.service.impl.ExpenseServiceImpl; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; + +import java.text.ParseException; +import java.util.*; + +import static com.santander.interview.enums.ResponseMessageEnum.EXPENSE_BADLY_FORMATTED_DATE; +import static com.santander.interview.enums.ResponseMessageEnum.EXPENSE_NOT_FOUND; + +public class ExpenseServiceTest { + private static final long USER_CODE = 1232; + private static final Date DATE = new Date(); + private static final String DATE_STRING = "01102019"; + private static final String DATE_STRING_INVALID = "011"; + private static final double VALUE = 124.2; + private static final String DESCRIPTION = "Teste"; + private static final String DETAIL = "Detalhes"; + List expensesResult; + + @InjectMocks + ExpenseServiceImpl expenseService = new ExpenseServiceImpl(); + + @Mock + Expense expenseDomain; + + @Before + public void init() { + MockitoAnnotations.initMocks(this); + this.expensesResult = new ArrayList<>(); + this.expensesResult.add(new Expense()); + } + + @Test + public void searchExpensesByUserCodeAndDate_InvalidDataTest() throws ParseException { + List expenses = new ArrayList<>(); + ResponseMessageEnum responseMessageEnumExpected = EXPENSE_BADLY_FORMATTED_DATE; + HttpStatus httpStatusExpected = HttpStatus.BAD_REQUEST; + Expense expense = new Expense(DESCRIPTION, VALUE, USER_CODE, DATE); + expenses.add(expense); + Mockito.when(this.expenseDomain.searchByUserCodeAndDate(USER_CODE, DATE_STRING_INVALID)) + .thenThrow(ParseException.class); + + try { + this.expenseService.searchExpensesByUserCodeAndDate(USER_CODE, DATE_STRING_INVALID); + } catch (ExpenseException ee) { + ResponseMessageEnum responseMessageEnum = ee.getResponseMessageEnum(); + Assert.assertEquals(responseMessageEnum.getInternalMessage(), responseMessageEnumExpected.getInternalMessage()); + Assert.assertEquals(responseMessageEnum.getUserMessage(), responseMessageEnumExpected.getUserMessage()); + Assert.assertEquals(ee.getStatusCode(), httpStatusExpected); + } + } + + @Test + public void searchExpensesByUserCodeTest() { + Mockito.when(this.expenseDomain.searchByUserCode(USER_CODE)).thenReturn(this.expensesResult); + Assert.assertEquals( + this.expenseService.searchExpensesByUserCode(USER_CODE), + this.expensesResult + ); + } + + + @Test + public void updateExpenseTest() { + boolean isOk = true; + String uuid = UUID.randomUUID().toString(); + Expense expense = new Expense(); + Mockito.when(this.expenseDomain.update(uuid, expense)).thenReturn(true); + + try { + this.expenseService.updateExpense(uuid, expense); + } catch (ExpenseException e) { + isOk = false; + } + + Assert.assertTrue(isOk); + } + + @Test + public void updateExpense_NotFoundExpenseTest() { + HttpStatus httpStatusExpected = HttpStatus.NOT_FOUND; + ResponseMessageEnum responseMessageEnum = EXPENSE_NOT_FOUND; + String uuid = UUID.randomUUID().toString(); + Expense expense = new Expense(); + Mockito.when(this.expenseDomain.update(uuid, expense)).thenReturn(false); + + try { + this.expenseService.updateExpense(uuid, expense); + } catch (ExpenseException ee) { + Assert.assertEquals(ee.getStatusCode(), httpStatusExpected); + Assert.assertEquals(ee.getResponseMessageEnum().getUserMessage(), responseMessageEnum.getUserMessage()); + } + + } + +} diff --git a/src/test/java/com/santander/interview/utils/ExpenseManagementUtilsTest.java b/src/test/java/com/santander/interview/utils/ExpenseManagementUtilsTest.java new file mode 100644 index 00000000..7b6780e0 --- /dev/null +++ b/src/test/java/com/santander/interview/utils/ExpenseManagementUtilsTest.java @@ -0,0 +1,58 @@ +package com.santander.interview.utils; + +import static com.santander.interview.enums.ResponseMessageEnum.*; + +import com.santander.interview.domain.Response; +import com.santander.interview.domain.ResponseError; +import com.santander.interview.domain.ResponseObject; +import com.santander.interview.enums.ResponseMessageEnum; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class ExpenseManagementUtilsTest { + private static final ResponseMessageEnum RESPONSE_MESSAGE_ENUM = ADD_EXPENSE_SUCCESS; + private static final HttpStatus HTTP_STATUS = HttpStatus.OK; + private static final Object DATA = new Object(); + + @Test + public void convertStringToInt_InvalidStringTest() { + int intValue = ExpenseManagementUtils.convertStringtoInt(10, "asd"); + Assert.assertEquals(intValue, 10); + } + + @Test + public void convertStringToInt_ValidStringTest() { + int intValue = ExpenseManagementUtils.convertStringtoInt(10, "123"); + Assert.assertEquals(intValue, 123); + } + + @Test + public void responseWithDataTest() { + ResponseEntity response = ExpenseManagementUtils + .responseWithData(RESPONSE_MESSAGE_ENUM, HTTP_STATUS, DATA); + Assert.assertEquals(response.getStatusCode(), HTTP_STATUS); + Assert.assertEquals(response.getBody().getInternalMessage(), RESPONSE_MESSAGE_ENUM.getInternalMessage()); + Assert.assertEquals(response.getBody().getUserMessage(), RESPONSE_MESSAGE_ENUM.getUserMessage()); + Assert.assertEquals(response.getBody().getData(), DATA); + } + + @Test + public void responseWithoutData() { + ResponseEntity response = ExpenseManagementUtils + .responseWithoutData(RESPONSE_MESSAGE_ENUM, HTTP_STATUS); + Assert.assertEquals(response.getStatusCode(), HTTP_STATUS); + Assert.assertEquals(response.getBody().getInternalMessage(), RESPONSE_MESSAGE_ENUM.getInternalMessage()); + Assert.assertEquals(response.getBody().getUserMessage(), RESPONSE_MESSAGE_ENUM.getUserMessage()); + } + + @Test + public void responseWithError() { + ResponseEntity response = ExpenseManagementUtils + .responseWithError(RESPONSE_MESSAGE_ENUM, HTTP_STATUS); + Assert.assertEquals(response.getBody().getInternalMessage(), RESPONSE_MESSAGE_ENUM.getInternalMessage()); + Assert.assertEquals(response.getBody().getUserMessage(), RESPONSE_MESSAGE_ENUM.getUserMessage()); + } + +}