diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1ee92668 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +*.sw? +.#* +*# +*~ +.classpath +.project +.settings +bin +build +target +dependency-reduced-pom.xml +*.sublime-* +/scratch +.gradle +README.html +.idea +*.iml diff --git a/README.md b/README.md index 15d8f685..0da12fa4 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,19 @@ -# Show me the code - -### # DESAFIO: - API REST para Gestão de Gastos! -``` -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 -``` -``` -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. -``` -``` -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 -``` -### # 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. +**Procedimentos de execução** -* 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. +Dentro do diretório gastos-api digitar +- mvn install -Dmaven.test.skip=true -### # Observações gerais +Observação: é necessário o parâmetro -Dmaven.test.skip=true porque alguns testes exigem a conexão com o Solr (que só estará disponível após executar os procedimentos do Docker) -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. +No diretório raiz (TestBackJava) digitar os comandos +- docker-compose build +- docker-compose up -d -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. +Neste ponto um container com projeto e outro com o Solr estará disponível. -### # Importante: não há prazo de entrega, faça com qualidade! +Caso queira executar os testes (unitário e integração), basta editar o arquivo application.properties do projeto Spring e modificar o atributo _spring.data.solr.host=http://solrnode:8983/solr_ para _spring.data.solr.host=http://localhost:8983/solr_ e adicionando um novo atributo para porta _port:8081_. -# BOA SORTE! +Assim, dentro do diretório gastos-api (fora do container) basta digitar o seguinte comando para executar os testes: +- mvn test diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..204e79a0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3' +services: + solrnode: + image: solr + ports: + - "8983:8983" + volumes: + - data:/opt/solr/server/solr/gasto + entrypoint: + - docker-entrypoint.sh + - solr-precreate + - gasto + gastos-api: + build: gastos-api + ports: + - "8080:8080" + volumes: + - ~/.m2:/root/.m2 + links: + - solrnode +volumes: + data: diff --git a/gastos-api/.gitignore b/gastos-api/.gitignore new file mode 100644 index 00000000..1ee92668 --- /dev/null +++ b/gastos-api/.gitignore @@ -0,0 +1,17 @@ +*.sw? +.#* +*# +*~ +.classpath +.project +.settings +bin +build +target +dependency-reduced-pom.xml +*.sublime-* +/scratch +.gradle +README.html +.idea +*.iml diff --git a/gastos-api/.mvn/wrapper/MavenWrapperDownloader.java b/gastos-api/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..72308aa4 --- /dev/null +++ b/gastos-api/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,114 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one +or more contributor license agreements. See the NOTICE file +distributed with this work for additional information +regarding copyright ownership. The ASF licenses this file +to you under the Apache License, Version 2.0 (the +"License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.util.Properties; + +public class MavenWrapperDownloader { + + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = + "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: : " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/gastos-api/.mvn/wrapper/maven-wrapper.jar b/gastos-api/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 00000000..01e67997 Binary files /dev/null and b/gastos-api/.mvn/wrapper/maven-wrapper.jar differ diff --git a/gastos-api/.mvn/wrapper/maven-wrapper.properties b/gastos-api/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..cd0d451c --- /dev/null +++ b/gastos-api/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip diff --git a/gastos-api/Dockerfile b/gastos-api/Dockerfile new file mode 100644 index 00000000..49495122 --- /dev/null +++ b/gastos-api/Dockerfile @@ -0,0 +1,9 @@ +FROM maven:3-jdk-8 + +RUN mkdir data && cd /data && mkdir gastos-api && cd gastos-api + +WORKDIR /data/gastos-api + +ADD . /data/gastos-api + +CMD ["mvn","spring-boot:run"] diff --git a/gastos-api/pom.xml b/gastos-api/pom.xml new file mode 100644 index 00000000..0d339195 --- /dev/null +++ b/gastos-api/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 1.5.21.RELEASE + + + com.santander + gastos-api + 0.0.1-SNAPSHOT + Gastos-API + Demo project for Spring Boot + + + 1.8 + + + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + org.springframework.boot + spring-boot-starter-data-solr + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/gastos-api/src/main/java/com/santander/gastosapi/GastosApiApplication.java b/gastos-api/src/main/java/com/santander/gastosapi/GastosApiApplication.java new file mode 100644 index 00000000..f90eef35 --- /dev/null +++ b/gastos-api/src/main/java/com/santander/gastosapi/GastosApiApplication.java @@ -0,0 +1,22 @@ +package com.santander.gastosapi; + +import java.util.TimeZone; + +import javax.annotation.PostConstruct; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class GastosApiApplication { + + @PostConstruct + void started() { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); + } + + public static void main(String[] args) { + SpringApplication.run(GastosApiApplication.class, args); + } + +} diff --git a/gastos-api/src/main/java/com/santander/gastosapi/model/Gasto.java b/gastos-api/src/main/java/com/santander/gastosapi/model/Gasto.java new file mode 100644 index 00000000..60187369 --- /dev/null +++ b/gastos-api/src/main/java/com/santander/gastosapi/model/Gasto.java @@ -0,0 +1,109 @@ +package com.santander.gastosapi.model; + +import java.io.Serializable; +import java.util.Date; + +import org.apache.solr.client.solrj.beans.Field; +import org.springframework.data.annotation.Id; +import org.springframework.data.solr.core.mapping.SolrDocument; + +import com.fasterxml.jackson.annotation.JsonFormat; + +@SolrDocument(solrCoreName = "gasto") +public class Gasto implements Serializable { + + @Id + @Field + private String id; + + @Field + private String descricao; + + @Field + private Double valor; + + @Field + private int codigousuario; + + @Field + @JsonFormat(pattern="dd/MM/yyyy HH:mm:ss") + private Date data; + + @Field + private String categoria; + + public Gasto() {} + + public Gasto(String id, String descricao, Double valor, int codigousuario, Date data, String categoria) { + super(); + this.id = id; + this.descricao = descricao; + this.valor = valor; + this.codigousuario = codigousuario; + this.data = data; + this.categoria = categoria; + } + + public Gasto(String id, String descricao, Double valor, int codigousuario, Date data) { + this.id = id; + this.descricao = descricao; + this.valor = valor; + this.codigousuario = codigousuario; + this.data = data; + } + + public Gasto(String descricao, Double valor, int codigousuario, Date data) { + this.descricao = descricao; + this.valor = valor; + this.codigousuario = codigousuario; + this.data = data; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getDescricao() { + return descricao; + } + + public void setDescricao(String descricao) { + this.descricao = descricao; + } + + public Double getValor() { + return valor; + } + + public void setValor(Double valor) { + this.valor = valor; + } + + public int getCodigousuario() { + return codigousuario; + } + + public void setCodigousuario(int codigousuario) { + this.codigousuario = codigousuario; + } + + public Date getData() { + return data; + } + + public void setData(Date data) { + this.data = data; + } + + public String getCategoria() { + return categoria; + } + + public void setCategoria(String categoria) { + this.categoria = categoria; + } +} \ No newline at end of file diff --git a/gastos-api/src/main/java/com/santander/gastosapi/repository/GastosRepository.java b/gastos-api/src/main/java/com/santander/gastosapi/repository/GastosRepository.java new file mode 100644 index 00000000..4fcca610 --- /dev/null +++ b/gastos-api/src/main/java/com/santander/gastosapi/repository/GastosRepository.java @@ -0,0 +1,8 @@ +package com.santander.gastosapi.repository; + +import org.springframework.data.solr.repository.SolrCrudRepository; + +import com.santander.gastosapi.model.Gasto; + +public interface GastosRepository extends SolrCrudRepository{ +} diff --git a/gastos-api/src/main/java/com/santander/gastosapi/resource/GastosResource.java b/gastos-api/src/main/java/com/santander/gastosapi/resource/GastosResource.java new file mode 100644 index 00000000..edb9eae6 --- /dev/null +++ b/gastos-api/src/main/java/com/santander/gastosapi/resource/GastosResource.java @@ -0,0 +1,48 @@ +package com.santander.gastosapi.resource; + +import java.util.LinkedHashSet; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +import com.santander.gastosapi.model.Gasto; +import com.santander.gastosapi.service.GastosService; + +@RestController +@RequestMapping("/v1") +public class GastosResource { + + @Autowired + private GastosService gastosService; + + @RequestMapping(path="/accredited/gastos/cartao", method=RequestMethod.POST) + public void gastosPorCartao(@RequestBody Gasto gasto){ + gastosService.gastosPorCartao(gasto); + } + + @RequestMapping(path="/protected/gastos/listagem", method=RequestMethod.GET) + public @ResponseBody List listagemGastos(){ + return gastosService.listagemGastos(); + } + + @RequestMapping(path="/protected/gastos/listagem/filtro", method=RequestMethod.GET) + public @ResponseBody List filtroGastos(@RequestHeader String filtro){ + return gastosService.filtroGastos(filtro); + } + + @RequestMapping(path="/protected/gastos/categoria", method=RequestMethod.PUT) + public void categorizacaoGastos(@RequestHeader String categoria, @RequestHeader String gastoID){ + gastosService.categorizacaoGastos(categoria, gastoID); + } + + @RequestMapping(path="/protected/gastos/categoria/sugestao", method=RequestMethod.GET) + public @ResponseBody LinkedHashSet sugestaoCategoria(){ + return gastosService.sugestaoCategoria(); + } +} \ No newline at end of file diff --git a/gastos-api/src/main/java/com/santander/gastosapi/security/SpringSecurityConfig.java b/gastos-api/src/main/java/com/santander/gastosapi/security/SpringSecurityConfig.java new file mode 100644 index 00000000..0687307a --- /dev/null +++ b/gastos-api/src/main/java/com/santander/gastosapi/security/SpringSecurityConfig.java @@ -0,0 +1,31 @@ +package com.santander.gastosapi.security; + +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +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.web.cors.CorsConfiguration; + +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled=true) +public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.cors().configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues()) + .and().csrf().disable() + .authorizeRequests() + .antMatchers("/*/accredited/**").hasRole("SYSTEM") + .antMatchers("/*/protected/**").hasRole("USER") + .and().httpBasic(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication(). + withUser("cliente01").password("asd123").roles("USER") + .and() + .withUser("sistema01").password("asd321").roles("SYSTEM"); + } +} \ No newline at end of file diff --git a/gastos-api/src/main/java/com/santander/gastosapi/service/GastosService.java b/gastos-api/src/main/java/com/santander/gastosapi/service/GastosService.java new file mode 100644 index 00000000..b45977e9 --- /dev/null +++ b/gastos-api/src/main/java/com/santander/gastosapi/service/GastosService.java @@ -0,0 +1,82 @@ +package com.santander.gastosapi.service; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.santander.gastosapi.model.Gasto; +import com.santander.gastosapi.repository.GastosRepository; + +@Service +public class GastosService { + + @Autowired + private GastosRepository gastosRepository; + + public void gastosPorCartao(Gasto gasto) { + if(gasto.getCategoria()==null) + categorizacaoAutomaticaGastos(gasto); + gastosRepository.save(gasto); + } + + public List listagemGastos(){ + List gastos = new ArrayList<>(); + gastosRepository.findAll().forEach(gastos::add); + return gastos; + } + + public Gasto getGasto(String gastoID){ + return gastosRepository.findOne(gastoID); + } + + public List filtroGastos(String filtro){ + List gastos = listagemGastos(); + List gastosDiaEspecifico = new ArrayList<>(); + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd/MM/yyyy"); + + gastos.forEach(g->{ + if(g.getData()!=null){ + String dataValue = simpleDateFormat.format(g.getData()); + + if(dataValue.equals(filtro)) + gastosDiaEspecifico.add(g); + } + }); + return gastosDiaEspecifico; + } + + public void categorizacaoGastos(String categoria, String gastoID) { + Gasto gasto = gastosRepository.findOne(gastoID); + gasto.setCategoria(categoria); + gastosRepository.save(gasto); + } + + public LinkedHashSet sugestaoCategoria(){ + LinkedHashSet categoria = new LinkedHashSet<>(); + + gastosRepository.findAll().forEach(g->{ + if(g.getCategoria()!=null) + categoria.add(g.getCategoria()); + }); + + return categoria; + } + + private void categorizacaoAutomaticaGastos(Gasto gasto){ + List gastos = listagemGastos(); + + // busca todos os gastos do cliente + List gastosUserFilter1 = gastos.stream().filter(g -> g.getCodigousuario()==g.getCodigousuario()).collect(Collectors.toList()); + + // verifica se a descrição do gasto atual é igual a descrição de algum outro gasto + List gastosUserFilter2 = gastosUserFilter1.stream().filter(g -> g.getDescricao().equals(gasto.getDescricao())).collect(Collectors.toList()); + + if(gastosUserFilter2.size()>0) + gasto.setCategoria(gastosUserFilter2.get(0).getCategoria()); + } +} \ No newline at end of file diff --git a/gastos-api/src/main/resources/application.properties b/gastos-api/src/main/resources/application.properties new file mode 100644 index 00000000..a2841038 --- /dev/null +++ b/gastos-api/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.data.solr.host=http://solrnode:8983/solr \ No newline at end of file diff --git a/gastos-api/src/test/java/com/santander/gastosapi/GastoEndpointTest.java b/gastos-api/src/test/java/com/santander/gastosapi/GastoEndpointTest.java new file mode 100644 index 00000000..39623861 --- /dev/null +++ b/gastos-api/src/test/java/com/santander/gastosapi/GastoEndpointTest.java @@ -0,0 +1,181 @@ +package com.santander.gastosapi; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.BDDMockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.junit4.SpringRunner; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.santander.gastosapi.model.Gasto; +import com.santander.gastosapi.repository.GastosRepository; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +public class GastoEndpointTest { + + @Autowired + private TestRestTemplate testRestTemplate; + + @MockBean + private GastosRepository gastoRepository; + + @TestConfiguration + static class Config { + + @Bean + public RestTemplateBuilder restTemplateBuilder(){ + return new RestTemplateBuilder().basicAuthorization("sistema01", "asd321"); + } + } + + @Test + public void listGastosIncorretUser(){ + testRestTemplate = testRestTemplate.withBasicAuth("cliente02", "asd123"); + ResponseEntity response = testRestTemplate.getForEntity("/v1/protected/gastos/listagem", String.class); + Assertions.assertThat(response.getStatusCodeValue()).isEqualTo(401); + } + + @Test + public void listGastosCorretUser() throws ParseException{ + List gastos = Arrays.asList( + new Gasto("id01", "descricao gasto 1", 1.31, 1, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("21/04/2019 12:11:45")), + new Gasto("id02", "descricao gasto 2", 2.31, 2, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("21/04/2019 12:11:45")) + ); + testRestTemplate = testRestTemplate.withBasicAuth("cliente01", "asd123"); + BDDMockito.when(gastoRepository.findAll()).thenReturn(gastos); + ResponseEntity response = testRestTemplate.getForEntity("/v1/protected/gastos/listagem", Gasto[].class); + Assertions.assertThat(response.getStatusCodeValue()).isEqualTo(200); + Assertions.assertThat(response.getBody().length).isEqualTo(2); + } + + + @Test + public void filtroGastosEndpoint() throws ParseException{ + List gastos = Arrays.asList( + new Gasto("id01", "descricao gasto 1", 1.31, 1, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("25/12/2015 10:11:32")), + new Gasto("id02", "descricao gasto 2", 2.31, 2, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("01/01/2020 12:11:45")), + new Gasto("id03", "descricao gasto 2", 2.31, 2, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("01/01/2020 14:15:00")), + new Gasto("id04", "descricao gasto 3", 3.31, 3, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("11/07/2017 12:11:45")) + ); + + testRestTemplate = testRestTemplate.withBasicAuth("cliente01", "asd123"); + BDDMockito.when(gastoRepository.findAll()).thenReturn(gastos); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("filtro", "01/01/2020"); + + ResponseEntity responseEntity = testRestTemplate.exchange("/v1/protected/gastos/listagem/filtro", HttpMethod.GET, new HttpEntity<>(headers), Gasto[].class); + Assertions.assertThat(responseEntity.getStatusCodeValue()).isEqualTo(200); + Assertions.assertThat(responseEntity.getBody().length).isEqualTo(2); + } + + @Test + public void categorizacaoEndpoint() throws ParseException, JsonProcessingException { + List gastos = Arrays.asList( + new Gasto("id01", "descricao gasto 1", 1.31, 1, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("25/12/2015 10:11:32")), + new Gasto("id02", "descricao gasto 2", 2.31, 2, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("01/01/2020 12:11:45")), + new Gasto("id03", "descricao gasto 2", 2.31, 2, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("01/01/2020 14:15:00")), + new Gasto("id04", "descricao gasto 3", 3.31, 3, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("11/07/2017 12:11:45")) + ); + + testRestTemplate = testRestTemplate.withBasicAuth("cliente01", "asd123"); + BDDMockito.when(gastoRepository.findOne("id03")).thenReturn(gastos.get(2)); + BDDMockito.when(gastoRepository.findAll()).thenReturn(gastos); + + Assertions.assertThat(gastos.get(0).getCategoria()).isNull(); + + ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("categoria", "Categoria 025"); + headers.set("gastoID", "id03"); + + // categoriza um gasto específico + String requestJson = ow.writeValueAsString("Categoria 025"); + HttpEntity entity = new HttpEntity(requestJson, headers); + + ResponseEntity responseEntity = testRestTemplate.exchange("/v1/protected/gastos/categoria", HttpMethod.PUT, entity, Void.class); + Assertions.assertThat(responseEntity.getStatusCodeValue()).isEqualTo(200); + + // Verifica se o gasto foi categorizado + BDDMockito.when(gastoRepository.findAll()).thenReturn(gastos); + ResponseEntity response = testRestTemplate.getForEntity("/v1/protected/gastos/listagem", Gasto[].class); + Assertions.assertThat(response.getBody()[2].getCategoria()).isNotNull(); + Assertions.assertThat(response.getBody()[2].getCategoria()).isEqualTo("Categoria 025"); + } + + @Test + public void gastosPorCartaoEndpoint() throws ParseException, JsonProcessingException { + List categorias = Arrays.asList( + "Categoria01", + "Categoria02", + "Categoria03", + "Categoria04" + ); + + List gastos = Arrays.asList( + new Gasto("id01", "descricao gasto 1", 1.31, 1, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("25/12/2015 10:11:32"), categorias.get(0)), + new Gasto("id02", "descricao gasto 2", 2.31, 2, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("01/01/2020 12:11:45"), categorias.get(1)), + new Gasto("id03", "descricao gasto 3", 2.31, 2, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("01/01/2020 14:15:00"), categorias.get(2)), + new Gasto("id04", "descricao gasto 4", 3.31, 3, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("11/07/2017 12:11:45"), categorias.get(3)) + ); + + Gasto novoGasto = new Gasto("descricao gasto 3", 4.2, 2, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("11/07/2017 12:11:45")); + + BDDMockito.when(gastoRepository.findAll()).thenReturn(gastos); + + ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + String requestJson = ow.writeValueAsString(novoGasto); + HttpEntity entity = new HttpEntity(requestJson, headers); + + ResponseEntity responseEntity = testRestTemplate.exchange("/v1/accredited/gastos/cartao", HttpMethod.POST, entity, Void.class); + Assertions.assertThat(responseEntity.getStatusCodeValue()).isEqualTo(200); + } + + @Test + public void listSugestaoCategoria() throws ParseException{ + List gastos = Arrays.asList( + new Gasto("id01", "descricao gasto 1", 1.31, 1, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("21/04/2019 12:11:45"), "Categoria X"), + new Gasto("id01", "descricao gasto 1", 1.31, 1, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("21/04/2019 12:11:45"), "Categoria X"), + new Gasto("id01", "descricao gasto 1", 1.31, 1, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("21/04/2019 12:11:45"), "Categoria X"), + new Gasto("id01", "descricao gasto 1", 1.31, 1, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("21/04/2019 12:11:45"), "Categoria Y"), + new Gasto("id01", "descricao gasto 1", 1.31, 1, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("21/04/2019 12:11:45"), "Categoria Y") + ); + testRestTemplate = testRestTemplate.withBasicAuth("cliente01", "asd123"); + BDDMockito.when(gastoRepository.findAll()).thenReturn(gastos); + ResponseEntity response = testRestTemplate.getForEntity("/v1/protected/gastos/categoria/sugestao", String[].class); + Assertions.assertThat(response.getStatusCodeValue()).isEqualTo(200); + Assertions.assertThat(response.getBody().length).isEqualTo(2); + } + + +} diff --git a/gastos-api/src/test/java/com/santander/gastosapi/GastoRepositoryTest.java b/gastos-api/src/test/java/com/santander/gastosapi/GastoRepositoryTest.java new file mode 100644 index 00000000..026c1517 --- /dev/null +++ b/gastos-api/src/test/java/com/santander/gastosapi/GastoRepositoryTest.java @@ -0,0 +1,54 @@ +package com.santander.gastosapi; + +import java.text.ParseException; +import java.text.SimpleDateFormat; + +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootContextLoader; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +import com.santander.gastosapi.model.Gasto; +import com.santander.gastosapi.repository.GastosRepository; + +@RunWith(SpringRunner.class) +@ContextConfiguration(classes = GastosApiApplication.class, loader=SpringBootContextLoader.class) +public class GastoRepositoryTest { + + @Autowired + private GastosRepository gastosRepository; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createGasto() throws ParseException{ + gastosRepository.save(new Gasto("id-teste-create-gasto-001", "Descrição do gasto 01", 2.64, 3, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("21/04/2019 12:11:45"))); + Gasto novoGasto = gastosRepository.findOne("id-teste-create-gasto-001"); + Assertions.assertThat(novoGasto).isNotNull(); + Assertions.assertThat(novoGasto.getId()).isEqualTo("id-teste-create-gasto-001"); + } + + @Test + public void getGasto() throws ParseException{ + gastosRepository.save(new Gasto("id-teste-get-gasto-001", "Descrição do gasto 99", 2.64, 3, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("21/04/2019 12:11:45"))); + Gasto novoGasto = gastosRepository.findOne("id-teste-get-gasto-001"); + Assertions.assertThat(novoGasto).isNotNull(); + Assertions.assertThat(novoGasto.getId()).isEqualTo("id-teste-get-gasto-001"); + } + + + @Test + public void updateGasto() throws ParseException{ + gastosRepository.save(new Gasto("id-teste-update-gasto-001", "Descrição do gasto 99", 2.64, 3, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("21/04/2019 12:11:45"))); + gastosRepository.save(new Gasto("id-teste-update-gasto-001", "Descrição do gasto 100", 2.64, 3, new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").parse("21/04/2019 12:11:45"))); + Gasto novoGasto = gastosRepository.findOne("id-teste-update-gasto-001"); + Assertions.assertThat(novoGasto.getId()).isEqualTo("id-teste-update-gasto-001"); + Assertions.assertThat(novoGasto.getDescricao()).isEqualTo("Descrição do gasto 100"); + } +} diff --git a/gastos-api/src/test/java/com/santander/gastosapi/GastosApiApplicationTests.java b/gastos-api/src/test/java/com/santander/gastosapi/GastosApiApplicationTests.java new file mode 100644 index 00000000..63e1b94a --- /dev/null +++ b/gastos-api/src/test/java/com/santander/gastosapi/GastosApiApplicationTests.java @@ -0,0 +1,16 @@ +package com.santander.gastosapi; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class GastosApiApplicationTests { + + @Test + public void contextLoads() { + } + +}