diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6cbefb2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM openjdk:19 +COPY build/libs/*.jar app.jar +EXPOSE 8081 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/README.md b/README.md index d171857..3632c33 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,26 @@ -# Desafio Back End Java na DoroTech +# Desafio Back End Java na DoroTech -Somos uma empresa com clientes que atuam em vários segmentos do mercado, com diferentes tecnologias, culturas e desafios. +### Projeto + Desenvolvimento de uma API REST para o gerenciamento de produtos eletrônicos com Spring Webflux e MongoDB. +### Tecnologias + - Java 19 + - Spring 3.0.0 + - Lombok + - Gradle para gerenciamento de dependências + - MapStruct para a conversão entre classes do domínio e DTOs + - Testes automatizados com Mockito e WebTestClient + - Banco de dados NoSQL MongoDB Atlas + - Swagger e OpenAPI para documentação da API. + - Docker e Amazon ECS para o deploy da aplicação. +### Como executar o projeto: -Gostamos de compor nossos times com profissionais multidisciplinares, que tenham alta capacidade de aprendizado, sejam detalhistas, resilientes, questionadores e curiosos. Você, como Java Developer, será o responsável por implementar, dar manutenção, aplicar correções e propor soluções em projetos de software. +Para acessar o projeto é possível seguir as seguintes alternativas: +1. Realizar o download da [imagem docker](https://hub.docker.com/repository/docker/ferreirabrunomarcelo/product-service) e executar os seguintes comandos: -## Requisitos do desafio: -``` -1. Criar um código que execute um CRUD(Create, Read, Update, Delete) em uma tabela para gerenciar produtos eletrônicos. -2. Use um banco NoSQL(DynamoDB é um diferencial). -3. Utilizar Spring como framework(Quarkus é um diferencial). -4. Dados da tabela a ser criada no banco: - - Products: - name, - description, - price, - amount. +a. `docker pull ferreirabrunomarcelo/product-service:latest` +b. `docker run --publish 8081:8081 ferreirabrunomarcelo/product-service` -Seja criativo! fazer o melhor não é ser complexo. -``` +c. acessar pelo navegador a URI `http://localhost:8081/swagger-ui.html` -## Dicas e Informações Valiosas -``` -O que gostaríamos de ver em seu teste: - - Convenção de nome em classes, objetos, variáveis, métodos e etc. - Faça commits regulares. Eles são melhores do que um commit gigantesco. Gostaríamos de ver commits organizados e padronizados, então capriche neles! - Bônus 1 Quarkus & AWS, implementação de uma lambda AWS utilizando framework Quarkus - Bônus 2 Testes automatizados - Observação: Nenhum dos itens acima é obrigatório. - -O que o seu Teste não deve ter: - Saber que não foi você quem implementou o projeto. - Várias bibliotecas instaladas sem uso. - Falta de organização de código. - Falta de documentação. - Nome de variáveis sem sentido ou sem padrão de nomes. - Histórico de commits desorganizado e despadronizado. - -Boa Sorte!! -``` - -## Itens obrigatórios -``` -1. Possibilitar a criação de um novo produto -2. Possibilitar consulta de todos os produtos no banco de dados. -3. Possibilitar consultar um produto específico pelo id. -4. Permitir a exclusão de um produto. -5. Persistir os dados na base. -``` - -## Itens desejáveis -``` -1. Criação de Testes unitários. -2. Utilização de alguma ferramenta AWS(API Gateway, Lambda, SQS, SNS, EC2,..). -3. Docker. -4. Utilização de algum padrão de projeto. -``` - -### Instruções para entrega -``` -1. Fazer um fork desse repositório - -2. Criar um branch com o seu primeiro e último nome -git checkout -b joao-silva - -3. Escreva a documentação da sua aplicação -Você deve, substituir o conteúdo do arquivo README.md e escrever a documentação da sua aplicação, com os seguintes tópicos: - - Projeto: Descreva o projeto e como você o executou. Seja objetivo. - - Tecnologias: Descreva quais tecnologias foram utilizadas, enumerando versões (se necessário) e os links para suas documentações, quais bibliotecas instalou e porque. -Como compilar e rodar: Descreva como compilar e rodar sua aplicação. - -4. Faça uma Pull Request -Após implementada a solução, crie uma pull request com o seu projeto para esse repositório, avise o recrutador. -``` +2. Acessar a API por meio do IP público do [Amazon ECS](http://177.71.160.52:8081/swagger-ui.html). \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..d9487e6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.0.0' + id 'io.spring.dependency-management' version '1.1.0' +} + +group = 'com.ferreirabrunomarcelo' +version = '0.0.1-SNAPSHOT' + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-webflux' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive' + implementation 'org.mapstruct:mapstruct:1.5.3.Final' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final' + implementation 'org.springdoc:springdoc-openapi:2.0.0' + implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.0.0' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.projectreactor:reactor-test' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/product-service b/product-service new file mode 160000 index 0000000..89f5334 --- /dev/null +++ b/product-service @@ -0,0 +1 @@ +Subproject commit 89f5334b232a63f05aea4109e3d54804aded71d0 diff --git a/src/main/java/com/ferreirabrunomarcelo/productapi/ProductApiApplication.java b/src/main/java/com/ferreirabrunomarcelo/productapi/ProductApiApplication.java new file mode 100644 index 0000000..a3694e2 --- /dev/null +++ b/src/main/java/com/ferreirabrunomarcelo/productapi/ProductApiApplication.java @@ -0,0 +1,20 @@ +package com.ferreirabrunomarcelo.productapi; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@OpenAPIDefinition(info = @Info( + title = "Products API", + version = "1.0", + description = "API for electronic product management" +)) +public class ProductApiApplication { + + public static void main(String[] args) { + SpringApplication.run(ProductApiApplication.class, args); + } + +} diff --git a/src/main/java/com/ferreirabrunomarcelo/productapi/configurations/CORSConfiguration.java b/src/main/java/com/ferreirabrunomarcelo/productapi/configurations/CORSConfiguration.java new file mode 100644 index 0000000..deefc4e --- /dev/null +++ b/src/main/java/com/ferreirabrunomarcelo/productapi/configurations/CORSConfiguration.java @@ -0,0 +1,19 @@ +package com.ferreirabrunomarcelo.productapi.configurations; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.config.CorsRegistry; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.reactive.config.WebFluxConfigurer; + +@Configuration +@EnableWebFlux +public class CORSConfiguration implements WebFluxConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("*") + .allowedMethods("GET", "POST", "PUT", "DELETE") + .allowedHeaders("*"); + } + +} diff --git a/src/main/java/com/ferreirabrunomarcelo/productapi/configurations/MongoDBConfiguration.java b/src/main/java/com/ferreirabrunomarcelo/productapi/configurations/MongoDBConfiguration.java new file mode 100644 index 0000000..815c3f5 --- /dev/null +++ b/src/main/java/com/ferreirabrunomarcelo/productapi/configurations/MongoDBConfiguration.java @@ -0,0 +1,36 @@ +package com.ferreirabrunomarcelo.productapi.configurations; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; +import org.springframework.data.mongodb.config.MongoConfigurationSupport; +import org.springframework.data.mongodb.core.convert.*; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; + + +@Configuration +@EnableMongoRepositories +public class MongoDBConfiguration extends MongoConfigurationSupport { + + @Value("${spring.data.mongodb.database}") + private String database; + + @Bean + public MappingMongoConverter mappingMongoConverter(ReactiveMongoDatabaseFactory databaseFactory, + MongoCustomConversions customConversions, MongoMappingContext mappingContext) { + + MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext); + converter.setCustomConversions(customConversions); + converter.setCodecRegistryProvider(databaseFactory); + converter.setTypeMapper(new DefaultMongoTypeMapper(null)); + + return converter; + } + + @Override + protected String getDatabaseName() { + return database; + } +} diff --git a/src/main/java/com/ferreirabrunomarcelo/productapi/configurations/RoutesConfiguration.java b/src/main/java/com/ferreirabrunomarcelo/productapi/configurations/RoutesConfiguration.java new file mode 100644 index 0000000..17cff0e --- /dev/null +++ b/src/main/java/com/ferreirabrunomarcelo/productapi/configurations/RoutesConfiguration.java @@ -0,0 +1,183 @@ +package com.ferreirabrunomarcelo.productapi.configurations; + +import com.ferreirabrunomarcelo.productapi.handlers.ProductHandler; +import com.ferreirabrunomarcelo.productapi.dtos.ProductDTO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import org.springdoc.core.annotations.RouterOperation; +import org.springdoc.core.annotations.RouterOperations; +import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +@Configuration +public class RoutesConfiguration { + @Bean + @RouterOperations( + { + @RouterOperation( + path = "/product/", + produces = { + MediaType.APPLICATION_JSON_VALUE + }, + method = RequestMethod.GET, + beanClass = ProductHandler.class, + beanMethod = "getAllProducts", + operation = @Operation( + operationId = "getAllProducts", + tags = "products", + responses = { + @ApiResponse( + responseCode = "200", + description = "A product list was retrieved successfully", + content = @Content(schema = @Schema( + implementation = ProductDTO.class + )) + ) + } + ) + ), + @RouterOperation( + path = "/product/{id}", + produces = { + MediaType.APPLICATION_JSON_VALUE + }, + method = RequestMethod.GET, + beanClass = ProductHandler.class, + beanMethod = "getProduct", + operation = @Operation( + operationId = "getProduct", + tags = "products", + responses = { + @ApiResponse( + responseCode = "200", + description = "The product was retrieved successfully", + content = @Content(schema = @Schema( + implementation = ProductDTO.class + )) + ), + @ApiResponse(responseCode = "404", description = "product not found") + }, + parameters = { + @Parameter(in = ParameterIn.PATH, name = "id") + } + ) + ), + + @RouterOperation( + path = "/product/", + produces = { + MediaType.APPLICATION_JSON_VALUE + }, + method = RequestMethod.POST, + beanClass = ProductHandler.class, + beanMethod = "createProduct", + operation = @Operation( + operationId = "createProduct", + tags = "products", + responses = { + @ApiResponse( + responseCode = "201", + description = "The product was created successfully", + content = @Content(schema = @Schema( + implementation = ProductDTO.class + )) + ), + @ApiResponse(responseCode = "400", description = "bad request") + }, + requestBody = @RequestBody( + content = @Content(schema = @Schema( + implementation = ProductDTO.class + )) + ) + ) + + ), + + @RouterOperation( + path = "/product/{id}", + produces = { + MediaType.APPLICATION_JSON_VALUE + }, + method = RequestMethod.PUT, + beanClass = ProductHandler.class, + beanMethod = "updateProduct", + operation = @Operation( + operationId = "updateProduct", + tags = "products", + responses = { + @ApiResponse( + responseCode = "200", + description = "The product was updated successfully", + content = @Content(schema = @Schema( + implementation = ProductDTO.class + )) + ), + @ApiResponse(responseCode = "404", description = "product not found"), + @ApiResponse(responseCode = "400", description = "bad request") + }, + parameters = { + @Parameter(in = ParameterIn.PATH, name = "id") + }, + requestBody = @RequestBody( + content = @Content(schema = @Schema( + implementation = ProductDTO.class + )) + ) + ) + + ), + @RouterOperation( + path = "/product/{id}", + produces = { + MediaType.APPLICATION_JSON_VALUE + }, + method = RequestMethod.DELETE, + beanClass = ProductHandler.class, + beanMethod = "deleteProduct", + operation = @Operation( + operationId = "deleteProduct", + tags = "products", + responses = { + @ApiResponse( + responseCode = "204", + description = "The product was deleted successfully", + content = @Content(schema = @Schema( + implementation = ProductDTO.class + )) + ), + @ApiResponse(responseCode = "404", description = "product not found") + }, + parameters = { + @Parameter(in = ParameterIn.PATH, name = "id") + } + ) + ) + } + ) + public RouterFunction routes(ProductHandler productHandler) { + return route() + .GET("/product/{id}", productHandler::getProduct) + .GET("/product/", productHandler::getAllProducts) + .POST("/product/", productHandler::createProduct) + .PUT("/product/{id}", productHandler::updateProduct) + .DELETE("/product/{id}", productHandler::deleteProduct) + .build(); + } + + @Bean + public WebProperties.Resources resources() { + return new WebProperties.Resources(); + } +} diff --git a/src/main/java/com/ferreirabrunomarcelo/productapi/dtos/ProductDTO.java b/src/main/java/com/ferreirabrunomarcelo/productapi/dtos/ProductDTO.java new file mode 100644 index 0000000..461817f --- /dev/null +++ b/src/main/java/com/ferreirabrunomarcelo/productapi/dtos/ProductDTO.java @@ -0,0 +1,17 @@ +package com.ferreirabrunomarcelo.productapi.dtos; + +import lombok.*; + +import java.math.BigDecimal; + +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +public class ProductDTO { + private String name; + private String description; + private BigDecimal price; + private Integer amount; +} diff --git a/src/main/java/com/ferreirabrunomarcelo/productapi/exceptions/CustomAttributes.java b/src/main/java/com/ferreirabrunomarcelo/productapi/exceptions/CustomAttributes.java new file mode 100644 index 0000000..fde041d --- /dev/null +++ b/src/main/java/com/ferreirabrunomarcelo/productapi/exceptions/CustomAttributes.java @@ -0,0 +1,26 @@ +package com.ferreirabrunomarcelo.productapi.exceptions; + +import org.springframework.boot.web.error.ErrorAttributeOptions; +import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Map; + +@Component +public class CustomAttributes extends DefaultErrorAttributes { + @Override + public Map getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) { + Map errorAttributes = super.getErrorAttributes(request, options); + Throwable throwable = getError(request); + if(throwable instanceof ResponseStatusException exception) { + if(exception.getStatusCode().value() == 404) { + errorAttributes.put("message", "resource not found."); + } + } + return errorAttributes; + } + + +} diff --git a/src/main/java/com/ferreirabrunomarcelo/productapi/exceptions/GlobalExceptionHandler.java b/src/main/java/com/ferreirabrunomarcelo/productapi/exceptions/GlobalExceptionHandler.java new file mode 100644 index 0000000..59bd10b --- /dev/null +++ b/src/main/java/com/ferreirabrunomarcelo/productapi/exceptions/GlobalExceptionHandler.java @@ -0,0 +1,43 @@ +package com.ferreirabrunomarcelo.productapi.exceptions; + +import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.autoconfigure.web.reactive.error.AbstractErrorWebExceptionHandler; +import org.springframework.boot.web.error.ErrorAttributeOptions; +import org.springframework.boot.web.reactive.error.ErrorAttributes; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.Order; +import org.springframework.http.MediaType; +import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.server.*; +import reactor.core.publisher.Mono; + +import java.util.Map; + +@Component +@Order(-2) +public class GlobalExceptionHandler extends AbstractErrorWebExceptionHandler { + + public GlobalExceptionHandler(ErrorAttributes errorAttributes, WebProperties.Resources resources, ApplicationContext applicationContext, + ServerCodecConfigurer configurer) { + super(errorAttributes, resources, applicationContext); + super.setMessageReaders(configurer.getReaders()); + super.setMessageWriters(configurer.getWriters()); + } + + @Override + protected RouterFunction getRoutingFunction(ErrorAttributes errorAttributes) { + return RouterFunctions + .route(RequestPredicates.all(), this::buildErrorResponse); + } + + private Mono buildErrorResponse(ServerRequest request) { + Map error = this.getErrorAttributes(request, ErrorAttributeOptions.defaults()); + int httpStatus = (int) error.get("status"); + return ServerResponse + .status(httpStatus) + .contentType(MediaType.APPLICATION_JSON) + .body(BodyInserters.fromValue(error)); + } +} diff --git a/src/main/java/com/ferreirabrunomarcelo/productapi/handlers/ProductHandler.java b/src/main/java/com/ferreirabrunomarcelo/productapi/handlers/ProductHandler.java new file mode 100644 index 0000000..f610b4e --- /dev/null +++ b/src/main/java/com/ferreirabrunomarcelo/productapi/handlers/ProductHandler.java @@ -0,0 +1,53 @@ +package com.ferreirabrunomarcelo.productapi.handlers; + +import com.ferreirabrunomarcelo.productapi.dtos.ProductDTO; +import com.ferreirabrunomarcelo.productapi.services.ProductService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +@Component +@RequiredArgsConstructor +public class ProductHandler { + private final ProductService productService; + + public Mono getProduct(ServerRequest request) { + String productId = request.pathVariable("id"); + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(productService.getProduct(productId), ProductDTO.class); + } + + public Mono getAllProducts(ServerRequest request) { + return ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .body(productService.getAllProducts(), ProductDTO.class); + } + + public Mono createProduct(ServerRequest request) { + return request + .bodyToMono(ProductDTO.class) + .flatMap(p -> ServerResponse + .status(HttpStatus.CREATED) + .body(productService.createProduct(p), ProductDTO.class)); + } + + public Mono updateProduct(ServerRequest request) { + String productId = request.pathVariable("id"); + return request + .bodyToMono(ProductDTO.class) + .flatMap(p -> ServerResponse + .ok() + .body(productService.updateProduct(p, productId), ProductDTO.class)); + + } + + public Mono deleteProduct(ServerRequest request) { + String productId = request.pathVariable("id"); + return ServerResponse.noContent().build(productService.deleteProduct(productId)); + } +} diff --git a/src/main/java/com/ferreirabrunomarcelo/productapi/mappers/ProductMapper.java b/src/main/java/com/ferreirabrunomarcelo/productapi/mappers/ProductMapper.java new file mode 100644 index 0000000..e1ad1c6 --- /dev/null +++ b/src/main/java/com/ferreirabrunomarcelo/productapi/mappers/ProductMapper.java @@ -0,0 +1,12 @@ +package com.ferreirabrunomarcelo.productapi.mappers; + + +import com.ferreirabrunomarcelo.productapi.resources.ProductResource; +import com.ferreirabrunomarcelo.productapi.dtos.ProductDTO; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface ProductMapper { + ProductDTO toProductDTO(ProductResource productResource); + ProductResource toProductResource(ProductDTO productDTO); +} diff --git a/src/main/java/com/ferreirabrunomarcelo/productapi/repositories/ProductRepository.java b/src/main/java/com/ferreirabrunomarcelo/productapi/repositories/ProductRepository.java new file mode 100644 index 0000000..fb53fdc --- /dev/null +++ b/src/main/java/com/ferreirabrunomarcelo/productapi/repositories/ProductRepository.java @@ -0,0 +1,8 @@ +package com.ferreirabrunomarcelo.productapi.repositories; + +import com.ferreirabrunomarcelo.productapi.resources.ProductResource; +import org.springframework.data.mongodb.repository.ReactiveMongoRepository; + + +public interface ProductRepository extends ReactiveMongoRepository { +} diff --git a/src/main/java/com/ferreirabrunomarcelo/productapi/resources/ProductResource.java b/src/main/java/com/ferreirabrunomarcelo/productapi/resources/ProductResource.java new file mode 100644 index 0000000..d2407bb --- /dev/null +++ b/src/main/java/com/ferreirabrunomarcelo/productapi/resources/ProductResource.java @@ -0,0 +1,30 @@ +package com.ferreirabrunomarcelo.productapi.resources; + +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.math.BigDecimal; + +@Document("products") +@AllArgsConstructor +@NoArgsConstructor +@Builder +@Getter +@Setter +public class ProductResource { + @Id + private String id; + private String name; + private String description; + private BigDecimal price; + private Integer amount; + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ProductResource productResource)) { + return false; + } + return this.getId().equals(productResource.getId()); + } +} diff --git a/src/main/java/com/ferreirabrunomarcelo/productapi/services/DefaultProductService.java b/src/main/java/com/ferreirabrunomarcelo/productapi/services/DefaultProductService.java new file mode 100644 index 0000000..daf81df --- /dev/null +++ b/src/main/java/com/ferreirabrunomarcelo/productapi/services/DefaultProductService.java @@ -0,0 +1,59 @@ +package com.ferreirabrunomarcelo.productapi.services; + +import com.ferreirabrunomarcelo.productapi.mappers.ProductMapper; +import com.ferreirabrunomarcelo.productapi.repositories.ProductRepository; +import com.ferreirabrunomarcelo.productapi.dtos.ProductDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + + +@Service +@RequiredArgsConstructor +public class DefaultProductService implements ProductService { + + private final ProductRepository productRepository; + + private final ProductMapper productMapper; + + @Override + public Mono getProduct(String id) { + return productRepository.findById(id) + .map(productMapper::toProductDTO) + .switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND))); + } + + @Override + public Flux getAllProducts() { + return productRepository.findAll() + .map(productMapper::toProductDTO); + } + + @Override + public Mono createProduct(ProductDTO productDTO) { + return productRepository.save(productMapper.toProductResource(productDTO)) + .map(productMapper::toProductDTO); + } + + @Override + public Mono updateProduct(ProductDTO productDTO, String id) { + return productRepository.findById(id) + .flatMap(p -> { + p.setName(productDTO.getName()); + p.setDescription(productDTO.getDescription()); + p.setPrice(productDTO.getPrice()); + p.setAmount(productDTO.getAmount()); + return productRepository.save(p) + .map(productMapper::toProductDTO); + }) + .switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND))); + } + + @Override + public Mono deleteProduct(String id) { + return productRepository.deleteById(id); + } +} diff --git a/src/main/java/com/ferreirabrunomarcelo/productapi/services/ProductService.java b/src/main/java/com/ferreirabrunomarcelo/productapi/services/ProductService.java new file mode 100644 index 0000000..058354f --- /dev/null +++ b/src/main/java/com/ferreirabrunomarcelo/productapi/services/ProductService.java @@ -0,0 +1,13 @@ +package com.ferreirabrunomarcelo.productapi.services; + +import com.ferreirabrunomarcelo.productapi.dtos.ProductDTO; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface ProductService { + Mono getProduct(String id); + Flux getAllProducts(); + Mono createProduct(ProductDTO productDTO); + Mono updateProduct(ProductDTO productDTO, String id); + Mono deleteProduct(String id); +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..a28098f --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,11 @@ +server: + port: 8081 + error: + include-message: always + include-binding-errors: always + +spring: + data: + mongodb: + database: productsdb + uri: mongodb+srv://user:ZpZs20VRAuCarKdz@desenvolvimento-web.w6ya3e7.mongodb.net/productsdb diff --git a/src/test/java/com/ferreirabrunomarcelo/productapi/ProductApiApplicationTests.java b/src/test/java/com/ferreirabrunomarcelo/productapi/ProductApiApplicationTests.java new file mode 100644 index 0000000..c25063f --- /dev/null +++ b/src/test/java/com/ferreirabrunomarcelo/productapi/ProductApiApplicationTests.java @@ -0,0 +1,13 @@ +package com.ferreirabrunomarcelo.productapi; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ProductApiApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/com/ferreirabrunomarcelo/productapi/ProductHandlerTest.java b/src/test/java/com/ferreirabrunomarcelo/productapi/ProductHandlerTest.java new file mode 100644 index 0000000..36b05fb --- /dev/null +++ b/src/test/java/com/ferreirabrunomarcelo/productapi/ProductHandlerTest.java @@ -0,0 +1,131 @@ +package com.ferreirabrunomarcelo.productapi; + +import com.ferreirabrunomarcelo.productapi.configurations.RoutesConfiguration; +import com.ferreirabrunomarcelo.productapi.handlers.ProductHandler; +import com.ferreirabrunomarcelo.productapi.dtos.ProductDTO; +import com.ferreirabrunomarcelo.productapi.services.ProductService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.ResponseStatusException; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.math.BigDecimal; + +@WebFluxTest +public class ProductHandlerTest { + + @Autowired + private WebTestClient webTestClient; + @MockBean + private ProductService productService; + + @MockBean + private WebProperties.Resources resources; + + @BeforeEach + void setUp() { + + ProductHandler productHandler = new ProductHandler(productService); + + RouterFunction routes = new RoutesConfiguration() + .routes(productHandler); + + webTestClient = WebTestClient.bindToRouterFunction(routes) + .build(); + } + + @Test + public void shouldReturnHttpStatusOkWhenGetProductById() { + + Mockito.when(productService.getProduct("1")) + .thenReturn(buildProduct()); + + webTestClient + .get() + .uri("/product/" + "1") + .exchange() + .expectStatus() + .isOk() + .expectBody() + .jsonPath("name").isEqualTo("Product name") + .jsonPath("description").isEqualTo("Product description") + .jsonPath("price").isEqualTo(10.00) + .jsonPath("amount").isEqualTo(100); + } + + @Test + public void shouldReturnHttpStatusNotFoundWhenGetProductById() { + + Mockito.when(productService.getProduct("1")) + .thenThrow(new ResponseStatusException(HttpStatus.NOT_FOUND)); + + webTestClient + .get() + .uri("/product/" + "1") + .exchange() + .expectStatus() + .isNotFound(); + } + + @Test + public void shouldReturnAProductListWhenGetAllProducts() { + Flux products = Flux.just(buildProduct().block(), buildProduct().block(), buildProduct().block()); + + Mockito.when(productService.getAllProducts()) + .thenReturn(products); + + webTestClient + .get() + .uri("/product/") + .exchange() + .expectStatus() + .isOk() + .expectBodyList(ProductDTO.class) + .hasSize(3); + } + @Test + public void shouldReturnHttpStatusNoContentWhenDeleteProduct() { + Mockito.when(productService.deleteProduct("1")) + .thenReturn(Mono.empty()); + + webTestClient + .delete() + .uri("/product/" + "1") + .exchange() + .expectStatus() + .isNoContent(); + } + + @Test + public void shouldReturnHttpStatusNotFoundWhenDeleteProduct() { + Mockito.when(productService.deleteProduct("1")) + .thenThrow(new ResponseStatusException(HttpStatus.NOT_FOUND)); + + webTestClient + .delete() + .uri("/product/" + "1") + .exchange() + .expectStatus() + .isNotFound(); + } + + private Mono buildProduct() { + return Mono.just(ProductDTO + .builder() + .name("Product name") + .description("Product description") + .price(BigDecimal.valueOf(10.00)) + .amount(100) + .build()); + } +}