diff --git a/.github/workflows/continuous-delivery.yml b/.github/workflows/continuous-delivery.yml index 9d8b789..0603cf9 100644 --- a/.github/workflows/continuous-delivery.yml +++ b/.github/workflows/continuous-delivery.yml @@ -13,14 +13,6 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - name: Modify application.yml - run: | - sed -i "s|^ *active:.*| active: default|" src/main/resources/application.yml - sed -i "s|^ *url:.*| url: ${{ secrets.DATASOURCE_URL }}|" src/main/resources/application.yml - sed -i "s|^ *username:.*| username: ${{ secrets.DATASOURCE_USERNAME }}|" src/main/resources/application.yml - sed -i "s|^ *password:.*| password: ${{ secrets.DATASOURCE_PASSWORD }}|" src/main/resources/application.yml - cat src/main/resources/application.yml - - name: Set up JDK 17 uses: actions/setup-java@v2 with: @@ -28,7 +20,14 @@ jobs: java-version: '17' - name: Build with Maven - run: mvn clean install + env: + PGHOST: ${{ secrets.PGHOST }} + PGPORT: ${{ secrets.PGPORT }} + PGDATABASE: ${{ secrets.PGDATABASE }} + PGUSER: ${{ secrets.PGUSER }} + PGPASSWORD: ${{ secrets.PGPASSWORD }} + ISSUER_URI: ${{ secrets.ISSUER_URI }} + run: mvn clean install -Dspring.profiles.active=prd - name: Build Docker image run: | @@ -49,7 +48,6 @@ jobs: PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} HOST_NAME: ${{ secrets.EC2_HOST }} USER_NAME: ${{ secrets.EC2_USERNAME }} - run: | echo "$PRIVATE_KEY" > private_key && chmod 600 private_key ssh -o StrictHostKeyChecking=no -i private_key ${USER_NAME}@${HOST_NAME} ' diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 5dbac9d..62d6f7a 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -9,6 +9,8 @@ on: jobs: build: runs-on: ubuntu-latest + env: + ISSUER_URI: https://test-issuer-uri steps: - name: Checkout project uses: actions/checkout@v3 @@ -20,4 +22,4 @@ jobs: java-version: '17' - name: Build & Test - run: mvn clean install \ No newline at end of file + run: mvn clean install -Dspring.profiles.active=dev \ No newline at end of file diff --git a/pom.xml b/pom.xml index b4d0f17..709cb69 100644 --- a/pom.xml +++ b/pom.xml @@ -19,22 +19,35 @@ org.springframework.boot - spring-boot-starter-data-jpa + spring-boot-starter org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + org.springframework.boot spring-boot-starter-validation + + org.springframework.boot + spring-boot-starter-data-jpa + org.springframework.boot spring-boot-devtools runtime true + org.postgresql postgresql @@ -50,10 +63,7 @@ springdoc-openapi-starter-webmvc-ui 2.3.0 - - org.apache.commons - commons-lang3 - + org.springframework.boot spring-boot-starter-test @@ -64,6 +74,11 @@ h2 test + + org.springframework.security + spring-security-test + test + @@ -82,5 +97,4 @@ - \ No newline at end of file diff --git a/src/main/java/com/gilberto/logistockapi/config/SecurityConfig.java b/src/main/java/com/gilberto/logistockapi/config/SecurityConfig.java new file mode 100644 index 0000000..272177d --- /dev/null +++ b/src/main/java/com/gilberto/logistockapi/config/SecurityConfig.java @@ -0,0 +1,27 @@ +package com.gilberto.logistockapi.config; + +import com.gilberto.logistockapi.security.JwtConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfig { + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + return httpSecurity + .csrf(AbstractHttpConfigurer::disable) + .oauth2ResourceServer(oauth2 -> + oauth2.jwt(jwt -> + jwt.jwtAuthenticationConverter(new JwtConverter()) + ) + ) + .build(); + } +} diff --git a/src/main/java/com/gilberto/logistockapi/config/WebConfig.java b/src/main/java/com/gilberto/logistockapi/config/WebConfig.java new file mode 100644 index 0000000..76aad98 --- /dev/null +++ b/src/main/java/com/gilberto/logistockapi/config/WebConfig.java @@ -0,0 +1,18 @@ +package com.gilberto.logistockapi.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:5173") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*"); + } +} + diff --git a/src/main/java/com/gilberto/logistockapi/controllers/ProductController.java b/src/main/java/com/gilberto/logistockapi/controllers/ProductController.java index ab27a57..c30854f 100644 --- a/src/main/java/com/gilberto/logistockapi/controllers/ProductController.java +++ b/src/main/java/com/gilberto/logistockapi/controllers/ProductController.java @@ -1,9 +1,6 @@ package com.gilberto.logistockapi.controllers; -import com.gilberto.logistockapi.exceptions.ProductAlreadyRegisteredException; -import com.gilberto.logistockapi.exceptions.ProductNotFoundException; -import com.gilberto.logistockapi.exceptions.ProductStockExceededException; -import com.gilberto.logistockapi.exceptions.ProductStockUnderThanZeroException; +import com.gilberto.logistockapi.models.dto.SummaryProduct; import com.gilberto.logistockapi.models.dto.request.ProductFilter; import com.gilberto.logistockapi.models.dto.request.ProductForm; import com.gilberto.logistockapi.models.dto.request.ProductUpdateForm; @@ -11,11 +8,13 @@ import com.gilberto.logistockapi.models.dto.response.ProductDTO; import com.gilberto.logistockapi.services.IProductService; import jakarta.validation.Valid; + import java.net.URI; -import java.util.List; + import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.data.domain.Page; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; @@ -24,76 +23,70 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/api/v1/product") +@RequestMapping("/api/v1/products") public class ProductController { - - private final IProductService productService; - - public ProductController(@Autowired IProductService productService) { - this.productService = productService; - } - - @PostMapping - @ResponseStatus(HttpStatus.CREATED) - public ResponseEntity create(@RequestBody @Valid ProductForm productForm) - throws ProductAlreadyRegisteredException { - return ResponseEntity.created(URI.create("")) - .body(this.productService.create(productForm)); - } - - @GetMapping - @ResponseStatus(HttpStatus.OK) - public ResponseEntity> listAll(@Valid ProductFilter filter) { - return ResponseEntity.ok(this.productService.listAll(filter)); - } - - @GetMapping("/{id}") - @ResponseStatus(HttpStatus.OK) - public ResponseEntity findById(@PathVariable Long id) - throws ProductNotFoundException { - return ResponseEntity.ok(this.productService.findById(id)); - } - - @GetMapping("/barcode/{barcode}") - @ResponseStatus(HttpStatus.OK) - public ResponseEntity findByBarCode(@PathVariable String barcode) - throws ProductNotFoundException { - return ResponseEntity.ok(this.productService.findByBarCode(barcode)); - } - - @PutMapping("/{id}") - @ResponseStatus(HttpStatus.OK) - public ResponseEntity updateById(@PathVariable Long id, - @RequestBody @Valid ProductUpdateForm updateForm) - throws ProductNotFoundException { - return ResponseEntity.ok(this.productService.updateById(id, updateForm)); - } - - @DeleteMapping("/{id}") - @ResponseStatus(HttpStatus.NO_CONTENT) - public ResponseEntity deleteById(@PathVariable Long id) throws ProductNotFoundException { - this.productService.delete(id); - return ResponseEntity.noContent().build(); - } - - @PatchMapping("/{id}/increase") - @ResponseStatus(HttpStatus.OK) - public ResponseEntity increaseStock(@PathVariable Long id, - @RequestBody @Valid QuantityForm quantityForm) - throws ProductNotFoundException, ProductStockExceededException { - return ResponseEntity.ok(this.productService.increaseStock(id, quantityForm)); - } - - @PatchMapping("/{id}/decrease") - @ResponseStatus(HttpStatus.OK) - public ResponseEntity decreaseStock(@PathVariable Long id, - @RequestBody @Valid QuantityForm quantityForm) - throws ProductStockUnderThanZeroException, ProductNotFoundException { - return ResponseEntity.ok(this.productService.decreaseStock(id, quantityForm)); - } - + + private final IProductService productService; + + @Autowired + public ProductController(IProductService productService) { + this.productService = productService; + } + + @PostMapping + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity create(@RequestBody @Valid ProductForm productForm) { + return ResponseEntity.created(URI.create("")) + .body(this.productService.create(productForm)); + } + + @GetMapping + @PreAuthorize("hasRole('USER')") + public ResponseEntity> listAll(@Valid ProductFilter filter) { + return ResponseEntity.ok(this.productService.listAll(filter)); + } + + @GetMapping("/{id}") + @PreAuthorize("hasRole('USER')") + public ResponseEntity findById(@PathVariable Long id) { + return ResponseEntity.ok(this.productService.findById(id)); + } + + @GetMapping("/barcode/{barcode}") + @PreAuthorize("hasRole('USER')") + public ResponseEntity findByCode(@PathVariable String code) { + return ResponseEntity.ok(this.productService.findByCode(code)); + } + + @PutMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity updateById(@PathVariable Long id, + @RequestBody @Valid ProductUpdateForm updateForm) { + return ResponseEntity.ok(this.productService.updateById(id, updateForm)); + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") + public ResponseEntity deleteById(@PathVariable Long id) { + this.productService.delete(id); + return ResponseEntity.noContent().build(); + } + + @PatchMapping("/{id}/increase") + @PreAuthorize("hasRole('USER')") + public ResponseEntity increaseStock(@PathVariable Long id, + @RequestBody @Valid QuantityForm quantityForm) { + return ResponseEntity.ok(this.productService.increaseStock(id, quantityForm)); + } + + @PatchMapping("/{id}/decrease") + @PreAuthorize("hasRole('USER')") + public ResponseEntity decreaseStock(@PathVariable Long id, + @RequestBody @Valid QuantityForm quantityForm) { + return ResponseEntity.ok(this.productService.decreaseStock(id, quantityForm)); + } + } diff --git a/src/main/java/com/gilberto/logistockapi/exceptions/handler/HttpResponseHandler.java b/src/main/java/com/gilberto/logistockapi/controllers/handler/HttpResponseHandler.java similarity index 59% rename from src/main/java/com/gilberto/logistockapi/exceptions/handler/HttpResponseHandler.java rename to src/main/java/com/gilberto/logistockapi/controllers/handler/HttpResponseHandler.java index 52b273a..7c006d9 100644 --- a/src/main/java/com/gilberto/logistockapi/exceptions/handler/HttpResponseHandler.java +++ b/src/main/java/com/gilberto/logistockapi/controllers/handler/HttpResponseHandler.java @@ -1,4 +1,4 @@ -package com.gilberto.logistockapi.exceptions.handler; +package com.gilberto.logistockapi.controllers.handler; import com.gilberto.logistockapi.exceptions.HttpException; import com.gilberto.logistockapi.models.dto.response.ErrorDTO; @@ -9,11 +9,11 @@ @RestControllerAdvice public class HttpResponseHandler extends ResponseEntityExceptionHandler { - - @ExceptionHandler(HttpException.class) - public ResponseEntity handlerHttpException(HttpException exception) { - return ResponseEntity.status(exception.getStatus()) - .body(new ErrorDTO(exception.getStatus().value(), exception.getMessage())); - } + + @ExceptionHandler(HttpException.class) + public ResponseEntity handlerHttpException(HttpException exception) { + return ResponseEntity.status(exception.getStatus()) + .body(new ErrorDTO(exception.getStatus().value(), exception.getMessage())); + } } diff --git a/src/main/java/com/gilberto/logistockapi/exceptions/HttpException.java b/src/main/java/com/gilberto/logistockapi/exceptions/HttpException.java index 4ebfdca..4151b5a 100644 --- a/src/main/java/com/gilberto/logistockapi/exceptions/HttpException.java +++ b/src/main/java/com/gilberto/logistockapi/exceptions/HttpException.java @@ -4,8 +4,12 @@ import org.springframework.http.HttpStatus; @Getter -public sealed class HttpException extends Exception permits ProductAlreadyRegisteredException, - ProductNotFoundException, ProductStockExceededException, ProductStockUnderThanZeroException { +public sealed class HttpException extends RuntimeException + permits + ProductAlreadyRegisteredException, + ProductNotFoundException, + ProductStockExceededException, + ProductStockUnderThanZeroException { private HttpStatus status; diff --git a/src/main/java/com/gilberto/logistockapi/mappers/IProductMapper.java b/src/main/java/com/gilberto/logistockapi/mappers/IProductMapper.java index b3d199e..a7f6fb0 100644 --- a/src/main/java/com/gilberto/logistockapi/mappers/IProductMapper.java +++ b/src/main/java/com/gilberto/logistockapi/mappers/IProductMapper.java @@ -6,8 +6,8 @@ public interface IProductMapper { - Product toProduct(ProductForm productForm); + Product toEntity(ProductForm productForm); - ProductDTO toProductDTO(Product product); + ProductDTO toDTO(Product product); } diff --git a/src/main/java/com/gilberto/logistockapi/mappers/ProductMapper.java b/src/main/java/com/gilberto/logistockapi/mappers/ProductMapper.java index d6f512d..89bde0d 100644 --- a/src/main/java/com/gilberto/logistockapi/mappers/ProductMapper.java +++ b/src/main/java/com/gilberto/logistockapi/mappers/ProductMapper.java @@ -9,13 +9,13 @@ public class ProductMapper implements IProductMapper { @Override - public Product toProduct(ProductForm productForm) { + public Product toEntity(ProductForm productForm) { return Product.builder() .name(productForm.name()) .category(productForm.category()) .description(productForm.description()) .unitPrice(productForm.unitPrice()) - .barCode(productForm.barCode()) + .code(productForm.code()) .stockQuantity(productForm.stockQuantity()) .measureUnit(productForm.measureUnit()) .maxStockLevel(productForm.maxStockLevel()) @@ -23,16 +23,17 @@ public Product toProduct(ProductForm productForm) { } @Override - public ProductDTO toProductDTO(Product product) { + public ProductDTO toDTO(Product product) { return new ProductDTO( product.getId(), product.getName(), - product.getBarCode(), + product.getCode(), product.getCategory(), createSupplierDTO(product.getSupplier()), product.getUnitPrice(), product.getMeasureUnit(), product.getStockQuantity(), + product.getMaxStockLevel(), product.getDescription() ); } @@ -45,7 +46,7 @@ private SupplierDTO createSupplierDTO(Supplier supplier) { return new SupplierDTO( supplier.getId(), supplier.getName(), - supplier.getLegalDocument(), + supplier.getTaxId(), supplier.getEmail(), supplier.getPhone() ); diff --git a/src/main/java/com/gilberto/logistockapi/models/dto/SummaryProduct.java b/src/main/java/com/gilberto/logistockapi/models/dto/SummaryProduct.java new file mode 100644 index 0000000..1657ff4 --- /dev/null +++ b/src/main/java/com/gilberto/logistockapi/models/dto/SummaryProduct.java @@ -0,0 +1,15 @@ +package com.gilberto.logistockapi.models.dto; + +import com.gilberto.logistockapi.models.enums.Category; + +import java.math.BigDecimal; + +public record SummaryProduct( + Long id, + String name, + Category category, + BigDecimal unitPrice, + Integer stockQuantity +) { + +} diff --git a/src/main/java/com/gilberto/logistockapi/models/dto/request/ProductForm.java b/src/main/java/com/gilberto/logistockapi/models/dto/request/ProductForm.java index 6b4a2a6..22e64b4 100644 --- a/src/main/java/com/gilberto/logistockapi/models/dto/request/ProductForm.java +++ b/src/main/java/com/gilberto/logistockapi/models/dto/request/ProductForm.java @@ -15,7 +15,7 @@ public record ProductForm( @NotNull @Size(min = 2, max = 100) - String barCode, + String code, @NotNull Category category, @@ -25,7 +25,8 @@ public record ProductForm( @NotNull BigDecimal unitPrice, - + + @NotNull Integer stockQuantity, @NotNull diff --git a/src/main/java/com/gilberto/logistockapi/models/dto/request/ProductUpdateForm.java b/src/main/java/com/gilberto/logistockapi/models/dto/request/ProductUpdateForm.java index 6919cb6..16dfd34 100644 --- a/src/main/java/com/gilberto/logistockapi/models/dto/request/ProductUpdateForm.java +++ b/src/main/java/com/gilberto/logistockapi/models/dto/request/ProductUpdateForm.java @@ -20,8 +20,7 @@ public record ProductUpdateForm( @NotNull BigDecimal unitPrice, - - @NotNull + Integer maxStockLevel, @NotNull diff --git a/src/main/java/com/gilberto/logistockapi/models/dto/request/SupplierForm.java b/src/main/java/com/gilberto/logistockapi/models/dto/request/SupplierForm.java index db9db13..7abf102 100644 --- a/src/main/java/com/gilberto/logistockapi/models/dto/request/SupplierForm.java +++ b/src/main/java/com/gilberto/logistockapi/models/dto/request/SupplierForm.java @@ -14,7 +14,7 @@ public record SupplierForm( @NotNull @Pattern(regexp = "\\d{3}\\.\\d{3}\\.\\d{3}-\\d{2}|\\d{2}\\.\\d{3}\\.\\d{3}/\\d{4}-\\d{2}", message = "Document must be a CPF or CNPJ!") - String legalDocument, + String taxId, @Email String email, diff --git a/src/main/java/com/gilberto/logistockapi/models/dto/response/ProductDTO.java b/src/main/java/com/gilberto/logistockapi/models/dto/response/ProductDTO.java index 074c064..cb95c05 100644 --- a/src/main/java/com/gilberto/logistockapi/models/dto/response/ProductDTO.java +++ b/src/main/java/com/gilberto/logistockapi/models/dto/response/ProductDTO.java @@ -7,12 +7,13 @@ public record ProductDTO( Long id, String name, - String barCode, + String code, Category category, SupplierDTO supplier, BigDecimal unitPrice, MeasureUnit measureUnit, Integer stockQuantity, + Integer maxStockLevel, String description ) { diff --git a/src/main/java/com/gilberto/logistockapi/models/entity/Product.java b/src/main/java/com/gilberto/logistockapi/models/entity/Product.java index 1c03386..aa33831 100644 --- a/src/main/java/com/gilberto/logistockapi/models/entity/Product.java +++ b/src/main/java/com/gilberto/logistockapi/models/entity/Product.java @@ -14,7 +14,8 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import java.math.BigDecimal; -import java.time.LocalDate; +import java.time.LocalDateTime; + import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -36,8 +37,8 @@ public class Product { @Column(name = "pro_name", nullable = false) private String name; - @Column(name = "pro_barcode", nullable = false, unique = true) - private String barCode; + @Column(name = "pro_code", nullable = false, unique = true) + private String code; @Column(name = "pro_category", nullable = false) @Enumerated(value = EnumType.STRING) @@ -62,7 +63,7 @@ public class Product { @Column(name = "pro_entry_date", nullable = false) @CreationTimestamp - private LocalDate entryDate; + private LocalDateTime entryDate; @Column(name = "pro_description") private String description; diff --git a/src/main/java/com/gilberto/logistockapi/models/entity/Supplier.java b/src/main/java/com/gilberto/logistockapi/models/entity/Supplier.java index de4b346..952a573 100644 --- a/src/main/java/com/gilberto/logistockapi/models/entity/Supplier.java +++ b/src/main/java/com/gilberto/logistockapi/models/entity/Supplier.java @@ -29,8 +29,8 @@ public class Supplier { @Column(name = "sup_name", nullable = false) private String name; - @Column(name = "sup_legal_document", nullable = false, unique = true) - private String legalDocument; + @Column(name = "sup_tax_id", nullable = false, unique = true) + private String taxId; @Column(name = "sup_email") private String email; diff --git a/src/main/java/com/gilberto/logistockapi/repositories/IProductRepository.java b/src/main/java/com/gilberto/logistockapi/repositories/IProductRepository.java index 87222d6..acf8686 100644 --- a/src/main/java/com/gilberto/logistockapi/repositories/IProductRepository.java +++ b/src/main/java/com/gilberto/logistockapi/repositories/IProductRepository.java @@ -4,6 +4,9 @@ import com.gilberto.logistockapi.models.enums.Category; import java.util.List; import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @@ -11,17 +14,13 @@ @Repository public interface IProductRepository extends JpaRepository { - Optional findByBarCode(String barCode); + Optional findByCode(String barCode); @Query("select product from pro_product as product " + " where (product.category in :categories) " + " and ((:search = '' OR CAST(product.id AS text) = :search) " + - " or (upper(product.barCode) like concat('%', upper(:search), '%')) " + - " or (upper(product.name) like concat('%', upper(:search), '%'))) " + - " order by product.entryDate desc " + - " limit :pageSize " + - " offset :pageNumber") - List findAllByFilters(List categories, String search, int pageSize, - int pageNumber); + " or (upper(product.code) like concat('%', upper(:search), '%')) " + + " or (upper(product.name) like concat('%', upper(:search), '%'))) ") + Page findAllByFilters(List categories, String search, Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/com/gilberto/logistockapi/repositories/ISupplierRepository.java b/src/main/java/com/gilberto/logistockapi/repositories/ISupplierRepository.java index f04a0d4..d141c9b 100644 --- a/src/main/java/com/gilberto/logistockapi/repositories/ISupplierRepository.java +++ b/src/main/java/com/gilberto/logistockapi/repositories/ISupplierRepository.java @@ -8,6 +8,6 @@ @Repository public interface ISupplierRepository extends JpaRepository { - Optional findByLegalDocument(String legalDocument); + Optional findByTaxId(String taxId); } diff --git a/src/main/java/com/gilberto/logistockapi/security/JwtConverter.java b/src/main/java/com/gilberto/logistockapi/security/JwtConverter.java new file mode 100644 index 0000000..5dc2457 --- /dev/null +++ b/src/main/java/com/gilberto/logistockapi/security/JwtConverter.java @@ -0,0 +1,22 @@ +package com.gilberto.logistockapi.security; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import java.util.Collection; +import java.util.Map; + +public class JwtConverter implements Converter { + @Override + public AbstractAuthenticationToken convert(Jwt jwt) { + Map> realmAccess = jwt.getClaim("realm_access"); + var roles = realmAccess.get("roles"); + var grants = roles.stream() + .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) + .toList(); + return new JwtAuthenticationToken(jwt, grants); + } +} diff --git a/src/main/java/com/gilberto/logistockapi/services/IProductService.java b/src/main/java/com/gilberto/logistockapi/services/IProductService.java index 7aac1e2..229757b 100644 --- a/src/main/java/com/gilberto/logistockapi/services/IProductService.java +++ b/src/main/java/com/gilberto/logistockapi/services/IProductService.java @@ -1,34 +1,29 @@ package com.gilberto.logistockapi.services; -import com.gilberto.logistockapi.exceptions.ProductAlreadyRegisteredException; -import com.gilberto.logistockapi.exceptions.ProductNotFoundException; -import com.gilberto.logistockapi.exceptions.ProductStockExceededException; -import com.gilberto.logistockapi.exceptions.ProductStockUnderThanZeroException; +import com.gilberto.logistockapi.models.dto.SummaryProduct; import com.gilberto.logistockapi.models.dto.request.ProductFilter; import com.gilberto.logistockapi.models.dto.request.ProductUpdateForm; import com.gilberto.logistockapi.models.dto.request.ProductForm; import com.gilberto.logistockapi.models.dto.request.QuantityForm; import com.gilberto.logistockapi.models.dto.response.ProductDTO; -import java.util.List; +import org.springframework.data.domain.Page; public interface IProductService { - ProductDTO create(ProductForm productForm) throws ProductAlreadyRegisteredException; + ProductDTO create(ProductForm productForm); - List listAll(ProductFilter filter); + Page listAll(ProductFilter filter); - ProductDTO findById(Long id) throws ProductNotFoundException; + ProductDTO findById(Long id); - ProductDTO findByBarCode(String barCode) throws ProductNotFoundException; + ProductDTO findByCode(String barCode); - void delete(Long id) throws ProductNotFoundException; + void delete(Long id); - ProductDTO updateById(Long id, ProductUpdateForm updateForm) throws ProductNotFoundException; + ProductDTO updateById(Long id, ProductUpdateForm updateForm); - ProductDTO increaseStock(Long id, QuantityForm quantity) - throws ProductNotFoundException, ProductStockExceededException; + ProductDTO increaseStock(Long id, QuantityForm quantity); - ProductDTO decreaseStock(Long id, QuantityForm quantity) - throws ProductNotFoundException, ProductStockUnderThanZeroException; + ProductDTO decreaseStock(Long id, QuantityForm quantity); } diff --git a/src/main/java/com/gilberto/logistockapi/services/implementations/ProductService.java b/src/main/java/com/gilberto/logistockapi/services/implementations/ProductService.java index 72440d0..53f723a 100644 --- a/src/main/java/com/gilberto/logistockapi/services/implementations/ProductService.java +++ b/src/main/java/com/gilberto/logistockapi/services/implementations/ProductService.java @@ -2,6 +2,7 @@ import com.gilberto.logistockapi.mappers.IProductMapper; import com.gilberto.logistockapi.mappers.ProductMapper; +import com.gilberto.logistockapi.models.dto.SummaryProduct; import com.gilberto.logistockapi.models.dto.request.ProductFilter; import com.gilberto.logistockapi.models.dto.request.ProductUpdateForm; import com.gilberto.logistockapi.models.dto.request.ProductForm; @@ -19,11 +20,11 @@ import java.util.Arrays; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; -import java.util.List; -import java.util.stream.Collectors; - @Service public class ProductService implements IProductService { @@ -32,64 +33,73 @@ public class ProductService implements IProductService { private final IProductMapper productMapper; private final ISupplierService supplierService; - - public ProductService(@Autowired IProductRepository productRepository, - @Autowired ISupplierService supplierService) { + + @Autowired + public ProductService(IProductRepository productRepository, + ISupplierService supplierService) { this.productRepository = productRepository; - this.supplierService = supplierService; this.productMapper = new ProductMapper(); + this.supplierService = supplierService; } @Override - public ProductDTO create(ProductForm productForm) throws ProductAlreadyRegisteredException { + public ProductDTO create(ProductForm productForm) { - verifyIfIsAlreadyRegistered(productForm.barCode()); + verifyIfIsAlreadyRegistered(productForm.code()); - var product = this.productMapper.toProduct(productForm); + var product = this.productMapper.toEntity(productForm); product.setSupplier(this.supplierService.save(productForm.supplier())); + var savedProduct = this.productRepository.save(product); - return this.productMapper.toProductDTO(savedProduct); + + return this.productMapper.toDTO(savedProduct); } @Override - public List listAll(ProductFilter filter) { + public Page listAll(ProductFilter filter) { var search = StringUtils.defaultIfBlank(filter.search(), ""); - + var categories = filter.categories() == null ? - Arrays.asList(Category.values()) : - filter.categories(); - - var pageNumber = filter.pageNumber() * filter.pageSize(); - - return this.productRepository.findAllByFilters(categories, search, filter.pageSize(), - pageNumber).stream() - .map(this.productMapper::toProductDTO) - .collect(Collectors.toList()); + Arrays.asList(Category.values()) : + filter.categories(); + + var pageable = PageRequest.of( + filter.pageNumber(), + filter.pageSize(), + Sort.by(Sort.Direction.DESC, "entryDate") + ); + + return this.productRepository.findAllByFilters(categories, search, pageable) + .map(product -> new SummaryProduct( + product.getId(), + product.getName(), + product.getCategory(), + product.getUnitPrice(), + product.getStockQuantity()) + ); } @Override - public ProductDTO findById(Long id) throws ProductNotFoundException { - return this.productRepository.findById(id) - .map(this.productMapper::toProductDTO) - .orElseThrow(ProductNotFoundException::new); + public ProductDTO findById(Long id) { + var product = this.verifyIfExists(id); + return this.productMapper.toDTO(product); } @Override - public ProductDTO findByBarCode(String barCode) throws ProductNotFoundException { - return this.productRepository.findByBarCode(barCode) - .map(this.productMapper::toProductDTO) + public ProductDTO findByCode(String barCode) { + return this.productRepository.findByCode(barCode) + .map(this.productMapper::toDTO) .orElseThrow(ProductNotFoundException::new); } @Override - public void delete(Long id) throws ProductNotFoundException { + public void delete(Long id) { this.verifyIfExists(id); this.productRepository.deleteById(id); } @Override - public ProductDTO updateById(Long id, ProductUpdateForm updateForm) - throws ProductNotFoundException { + public ProductDTO updateById(Long id, ProductUpdateForm updateForm) { var product = this.verifyIfExists(id); product.setName(updateForm.name()); product.setCategory(updateForm.category()); @@ -101,12 +111,11 @@ public ProductDTO updateById(Long id, ProductUpdateForm updateForm) var updatedProduct = this.productRepository.save(product); - return this.productMapper.toProductDTO(updatedProduct); + return this.productMapper.toDTO(updatedProduct); } @Override - public ProductDTO increaseStock(Long id, QuantityForm quantityForm) - throws ProductNotFoundException, ProductStockExceededException { + public ProductDTO increaseStock(Long id, QuantityForm quantityForm) { var product = verifyIfExists(id); var updatedQuantity = sumQuantity(product.getStockQuantity(), quantityForm.quantity()); @@ -116,12 +125,11 @@ public ProductDTO increaseStock(Long id, QuantityForm quantityForm) product.setStockQuantity(updatedQuantity); var updatedProduct = this.productRepository.save(product); - return this.productMapper.toProductDTO(updatedProduct); + return this.productMapper.toDTO(updatedProduct); } @Override - public ProductDTO decreaseStock(Long id, QuantityForm quantityForm) - throws ProductNotFoundException, ProductStockUnderThanZeroException { + public ProductDTO decreaseStock(Long id, QuantityForm quantityForm) { var product = verifyIfExists(id); var updatedQuantity = subtractQuantity(product.getStockQuantity(), quantityForm.quantity()); @@ -131,7 +139,7 @@ public ProductDTO decreaseStock(Long id, QuantityForm quantityForm) product.setStockQuantity(updatedQuantity); var updatedProduct = this.productRepository.save(product); - return this.productMapper.toProductDTO(updatedProduct); + return this.productMapper.toDTO(updatedProduct); } private Integer sumQuantity(Integer stockQuantity, Integer quantityToIncrement) { @@ -142,17 +150,16 @@ private Integer subtractQuantity(Integer stockQuantity, Integer quantityToDecrem return stockQuantity - quantityToDecrement; } - private Product verifyIfExists(Long id) throws ProductNotFoundException { + private Product verifyIfExists(Long id) { return this.productRepository.findById(id) .orElseThrow(ProductNotFoundException::new); } - - private void verifyIfIsAlreadyRegistered(String barCode) - throws ProductAlreadyRegisteredException { - var optSavedProduct = this.productRepository.findByBarCode(barCode); - if (optSavedProduct.isPresent()) { - throw new ProductAlreadyRegisteredException(); - } + + private void verifyIfIsAlreadyRegistered(String barCode) { + this.productRepository.findByCode(barCode) + .ifPresent(product -> { + throw new ProductAlreadyRegisteredException(); + }); } } diff --git a/src/main/java/com/gilberto/logistockapi/services/implementations/SupplierService.java b/src/main/java/com/gilberto/logistockapi/services/implementations/SupplierService.java index 7754d49..c4964d7 100644 --- a/src/main/java/com/gilberto/logistockapi/services/implementations/SupplierService.java +++ b/src/main/java/com/gilberto/logistockapi/services/implementations/SupplierService.java @@ -13,8 +13,9 @@ public class SupplierService implements ISupplierService { private final ISupplierRepository supplierRepository; - - public SupplierService(@Autowired ISupplierRepository supplierRepository) { + + @Autowired + public SupplierService(ISupplierRepository supplierRepository) { this.supplierRepository = supplierRepository; } @@ -24,11 +25,11 @@ public Supplier save(SupplierForm supplierForm) { return null; } - return this.supplierRepository.findByLegalDocument(supplierForm.legalDocument()) + return this.supplierRepository.findByTaxId(supplierForm.taxId()) .orElseGet(() -> { var supplier = Supplier.builder() .name(supplierForm.name()) - .legalDocument(supplierForm.legalDocument()) + .taxId(supplierForm.taxId()) .email(supplierForm.email()) .phone(supplierForm.phone()) .address(createAddress(supplierForm.address())) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..e6e836d --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,17 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + driver-class-name: org.h2.Driver + jpa: + database-platform: org.hibernate.dialect.H2Dialect + h2: + console.enabled: true + security: + oauth2: + resourceserver: + jwt: + issuer-uri: ${ISSUER_URI} + jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs + +server: + port: 8081 \ No newline at end of file diff --git a/src/main/resources/application-prd.yml b/src/main/resources/application-prd.yml new file mode 100644 index 0000000..a6436a1 --- /dev/null +++ b/src/main/resources/application-prd.yml @@ -0,0 +1,21 @@ +spring: + application: + name: LogiStockAPI + datasource: + url: jdbc:postgresql://${PGHOST}:${PGPORT}/${PGDATABASE} + username: ${PGUSER} + password: ${PGPASSWORD} + jpa: + hibernate: + ddl-auto: create + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + security: + oauth2: + resourceserver: + jwt: + issuer-uri: ${ISSUER_URI} + jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs +server: + port: 8081 \ No newline at end of file diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml deleted file mode 100644 index 0101368..0000000 --- a/src/main/resources/application-test.yml +++ /dev/null @@ -1,4 +0,0 @@ -spring: - datasource: - url: jdbc:h2:mem:testdb - driver-class-name: org.h2.Driver \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index faca53e..0000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,17 +0,0 @@ -spring: - profiles: - active: test - application: - name: LogiStockAPI - datasource: - url: "jdbc:postgresql://localhost:5432/products" - username: "postgres_user_product" - password: "super_password" - output: - ansi: - enabled: always - jpa: - open-in-view: true - -server: - port: 8080 \ No newline at end of file diff --git a/src/main/resources/db/v1.0.0.sql b/src/main/resources/db/v1.0.0.sql index 765ea7d..e42ea91 100644 --- a/src/main/resources/db/v1.0.0.sql +++ b/src/main/resources/db/v1.0.0.sql @@ -12,7 +12,7 @@ CREATE TABLE IF NOT EXISTS add_address( CREATE TABLE IF NOT EXISTS sup_supplier( sup_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, sup_name VARCHAR(255) NOT NULL, - sup_legal_document VARCHAR(20) NOT NULL UNIQUE, + sup_tax_id VARCHAR(20) NOT NULL UNIQUE, sup_email VARCHAR(255), sup_phone VARCHAR(20), sup_address_id BIGINT, diff --git a/src/test/java/com/gilberto/logistockapi/controllers/ProductControllerTest.java b/src/test/java/com/gilberto/logistockapi/controllers/ProductControllerTest.java index d844173..8e5099c 100644 --- a/src/test/java/com/gilberto/logistockapi/controllers/ProductControllerTest.java +++ b/src/test/java/com/gilberto/logistockapi/controllers/ProductControllerTest.java @@ -3,6 +3,7 @@ import com.gilberto.logistockapi.exceptions.ProductAlreadyRegisteredException; import com.gilberto.logistockapi.exceptions.ProductStockExceededException; import com.gilberto.logistockapi.exceptions.ProductStockUnderThanZeroException; +import com.gilberto.logistockapi.models.dto.SummaryProduct; import com.gilberto.logistockapi.models.dto.request.ProductForm; import com.gilberto.logistockapi.models.dto.request.ProductUpdateForm; import com.gilberto.logistockapi.models.dto.request.QuantityForm; @@ -15,7 +16,11 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; + +import static org.mockito.ArgumentMatchers.any; import static org.springframework.http.MediaType.APPLICATION_JSON; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -25,7 +30,10 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.servlet.view.json.MappingJackson2JsonView; + import java.util.Collections; +import java.util.List; + import static com.gilberto.logistockapi.utils.JsonConvertionUtils.asJsonString; import static org.hamcrest.core.Is.is; import static org.mockito.Mockito.doNothing; @@ -39,12 +47,10 @@ public class ProductControllerTest { private static final long PRODUCT_ID = 1L; private static final long INVALID_PRODUCT_ID = 2L; - private static final String PRODUCT_API_URL_PATH = "/api/v1/product"; + private static final String PRODUCT_API_URL_PATH = "/api/v1/products"; private static final String PRODUCT_API_SUBPATH_INCREASE_URL = "/increase"; private static final String PRODUCT_API_SUBPATH_DECREASE_URL = "/decrease"; private static final String PRODUCT_API_SUBPATH_BAR_CODE = "/barcode"; - public static final String QUERY_PARAMS = "?pageNumber=1&pageSize=10&" + - "search=search&categories=ELECTRONIC, CLOTHING, FOOD, OTHER"; private MockMvc mockMvc; @@ -80,7 +86,7 @@ void whenPOSTIsCalledThenAProductMustBeCreated() throws Exception { .andExpect(status().isCreated()) .andExpect(jsonPath("$.id", is(productDTO.id().intValue()))) .andExpect(jsonPath("$.name", is(productDTO.name()))) - .andExpect(jsonPath("$.barCode", is(productDTO.barCode()))); + .andExpect(jsonPath("$.code", is(productDTO.code()))); } @Test @@ -130,7 +136,7 @@ void whenPUTIsCalledThenAProductMustBeUpdated() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.id", is(productDTO.id().intValue()))) .andExpect(jsonPath("$.name", is(productDTO.name()))) - .andExpect(jsonPath("$.barCode", is(productDTO.barCode()))); + .andExpect(jsonPath("$.code", is(productDTO.code()))); } @Test @@ -156,8 +162,7 @@ void whenPUTIsCalledWithInvalidIdThenNotFoundStatusMustBeReturned() throws Excep // when doThrow(ProductNotFoundException.class) .when(this.productService).updateById(INVALID_PRODUCT_ID, productUpdateForm); - - + // then this.mockMvc.perform(put(PRODUCT_API_URL_PATH + "/" + INVALID_PRODUCT_ID) .contentType(APPLICATION_JSON) @@ -173,15 +178,15 @@ void whenGETIsCalledWithValidBarCodeThenAProductMustBeReturned() throws Exceptio var productDTO = ModelUtils.getProductDTO(); // when - when(this.productService.findByBarCode(productDTO.barCode())) + when(this.productService.findByCode(productDTO.code())) .thenReturn(productDTO); // then - this.mockMvc.perform(get(PRODUCT_API_URL_PATH + PRODUCT_API_SUBPATH_BAR_CODE + "/" + productDTO.barCode()) + this.mockMvc.perform(get(PRODUCT_API_URL_PATH + PRODUCT_API_SUBPATH_BAR_CODE + "/" + productDTO.code()) .contentType(APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id", is(productDTO.id().intValue()))) - .andExpect(jsonPath("$.barCode", is(productDTO.barCode()))) + .andExpect(jsonPath("$.code", is(productDTO.code()))) .andExpect(jsonPath("$.name", is(productDTO.name()))); } @@ -191,11 +196,11 @@ void whenGETIsCalledWithoutRegisteredBarCodeThenNotFoundStatusMustBeReturned() t var productDTO = ModelUtils.getProductDTO(); // when - when(this.productService.findByBarCode(productDTO.barCode())) + when(this.productService.findByCode(productDTO.code())) .thenThrow(ProductNotFoundException.class); // then - this.mockMvc.perform(get(PRODUCT_API_URL_PATH + PRODUCT_API_SUBPATH_BAR_CODE + "/" + productDTO.barCode()) + this.mockMvc.perform(get(PRODUCT_API_URL_PATH + PRODUCT_API_SUBPATH_BAR_CODE + "/" + productDTO.code()) .contentType(APPLICATION_JSON)) .andExpect(status().isNotFound()); } @@ -215,7 +220,7 @@ void whenGETIsCalledWithValidIdThenAProductMustBeReturned() throws Exception { .contentType(APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.id", is(productDTO.id().intValue()))) - .andExpect(jsonPath("$.barCode", is(productDTO.barCode()))) + .andExpect(jsonPath("$.code", is(productDTO.code()))) .andExpect(jsonPath("$.name", is(productDTO.name()))); } @@ -235,35 +240,46 @@ void whenGETIsCalledWithInvalidIdThenNotFoundStatusMustBeReturned() throws Excep @Test void whenGETIsCalledToListProductsThenAProductListMustBeReturned() throws Exception { // given - var productFilter = ModelUtils.getProductFilter(); - var productDTO = ModelUtils.getProductDTO(); + var productDTO = ModelUtils.getSummaryProduct(); + var pageable = PageRequest.of(1, 10); + var productDTOList = Collections.singletonList(productDTO); // when - when(this.productService.listAll(productFilter)) - .thenReturn(Collections.singletonList(productDTO)); + when(productService.listAll(any())) + .thenReturn(new PageImpl<>(productDTOList, pageable, productDTOList.size())); // then - this.mockMvc.perform(get(PRODUCT_API_URL_PATH + QUERY_PARAMS) - .contentType(APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].id", is(productDTO.id().intValue()))) - .andExpect(jsonPath("$[0].barCode", is(productDTO.barCode()))) - .andExpect(jsonPath("$[0].name", is(productDTO.name()))); + this.mockMvc.perform(get(PRODUCT_API_URL_PATH) + .contentType(APPLICATION_JSON) + .param("pageNumber", "1") + .param("pageSize", "10") + .param("search", "search") + .param("categories", "ELECTRONIC, CLOTHING, FOOD, OTHER")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content[0].id", is(productDTO.id().intValue()))) + .andExpect(jsonPath("$.content[0].category", is(productDTO.category().name()))) + .andExpect(jsonPath("$.content[0].name", is(productDTO.name()))); } @Test void whenGETIsCalledToListProductsThenAProductEmptyListMustBeReturned() throws Exception { - // given - var productFilter = ModelUtils.getProductFilter(); - + //Given + var pageable = PageRequest.of(1, 10); + List productDTOList = Collections.emptyList(); + + // when - when(productService.listAll(productFilter)) - .thenReturn(Collections.emptyList()); + when(productService.listAll(any())) + .thenReturn(new PageImpl<>(productDTOList, pageable, 0)); // then - this.mockMvc.perform(get(PRODUCT_API_URL_PATH + QUERY_PARAMS) - .contentType(APPLICATION_JSON)) - .andExpect(status().isOk()); + this.mockMvc.perform(get(PRODUCT_API_URL_PATH) + .contentType(APPLICATION_JSON) + .param("pageNumber", "1") + .param("pageSize", "10") + .param("search", "test") + .param("categories", "ELECTRONIC, CLOTHING, FOOD, OTHER")) + .andExpect(status().isOk()); } // DELETE diff --git a/src/test/java/com/gilberto/logistockapi/services/ProductServiceTest.java b/src/test/java/com/gilberto/logistockapi/services/ProductServiceTest.java index 7fd8b66..8bf1913 100644 --- a/src/test/java/com/gilberto/logistockapi/services/ProductServiceTest.java +++ b/src/test/java/com/gilberto/logistockapi/services/ProductServiceTest.java @@ -5,6 +5,7 @@ import com.gilberto.logistockapi.exceptions.ProductStockExceededException; import com.gilberto.logistockapi.exceptions.ProductStockUnderThanZeroException; import com.gilberto.logistockapi.models.dto.request.QuantityForm; +import com.gilberto.logistockapi.models.entity.Product; import com.gilberto.logistockapi.repositories.IProductRepository; import com.gilberto.logistockapi.services.implementations.ProductService; import com.gilberto.logistockapi.utils.ModelUtils; @@ -18,6 +19,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static org.mockito.ArgumentMatchers.any; + +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import static org.mockito.Mockito.doNothing; @@ -25,6 +28,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageImpl; import java.util.Collections; import java.util.Optional; @@ -36,6 +40,8 @@ public class ProductServiceTest { private static final long PRODUCT_ID = 1L; private static final long INVALID_PRODUCT_ID = 2L; + + ArgumentCaptor productCaptor = ArgumentCaptor.forClass(Product.class); @Mock private IProductRepository productRepository; @@ -48,24 +54,25 @@ public class ProductServiceTest { // Create Product @Test - void shouldCreateProduct() throws ProductAlreadyRegisteredException { + void shouldCreateProduct() { // given var productForm = ModelUtils.getProductForm(); - var product = ModelUtils.getProduct(); - + // when - when(this.productRepository.findByBarCode(productForm.barCode())) - .thenReturn(Optional.empty()); + when(this.productRepository.findByCode(productForm.code())) + .thenReturn(Optional.empty()); when(this.supplierService.save(productForm.supplier())) - .thenReturn(null); - when(this.productRepository.save(any())) - .thenReturn(product); - + .thenReturn(null); + when(this.productRepository.save(productCaptor.capture())) + .thenAnswer(invocation -> productCaptor.getValue()); + // then var productDTO = this.productService.create(productForm); - + + var product = productCaptor.getValue(); + assertThat(productDTO.id(), is(product.getId())); - assertThat(productDTO.barCode(), is(product.getBarCode())); + assertThat(productDTO.code(), is(product.getCode())); assertThat(productDTO.name(), is(product.getName())); } @@ -76,7 +83,7 @@ void whenAProductHasAlreadyBeenRegisteredThenAnExceptionMustBeThrown() { var product = ModelUtils.getProduct(); // when - when(this.productRepository.findByBarCode(productForm.barCode())) + when(this.productRepository.findByCode(productForm.code())) .thenReturn(Optional.of(product)); // then @@ -90,53 +97,50 @@ void shouldReturnAProductList() { // given var productFilter = ModelUtils.getProductFilter(); var product = ModelUtils.getProduct(); - var pageNumber = productFilter.pageNumber() * productFilter.pageSize(); // when - when(this.productRepository.findAllByFilters(productFilter.categories(), - productFilter.search(), productFilter.pageSize(), pageNumber)) - .thenReturn(Collections.singletonList(product)); + when(this.productRepository.findAllByFilters(any(), any(), any())) + .thenReturn(new PageImpl<>(Collections.singletonList(product))); // then var productDTOList = this.productService.listAll(productFilter); - - assertThat(productDTOList, is(not(empty()))); - assertThat(productDTOList.get(0).name(), is(product.getName())); - assertThat(productDTOList.get(0).barCode(), is(product.getBarCode())); + + assertThat(productDTOList.getContent(), is(not(empty()))); + assertThat(productDTOList.getContent().get(0).id(), is(product.getId())); + assertThat(productDTOList.getContent().get(0).name(), is(product.getName())); + assertThat(productDTOList.getContent().get(0).category(), is(product.getCategory())); } @Test void shouldReturnAnEmptyProductList() { // given var productFilter = ModelUtils.getProductFilter(); - var pageNumber = productFilter.pageNumber() * productFilter.pageSize(); // when - when(this.productRepository.findAllByFilters(productFilter.categories(), - productFilter.search(), productFilter.pageSize(), pageNumber)) - .thenReturn(Collections.emptyList()); + when(this.productRepository.findAllByFilters(any(), any(), any())) + .thenReturn(new PageImpl<>(Collections.emptyList())); // then var productDTOList = this.productService.listAll(productFilter); - assertThat(productDTOList, is(empty())); + assertThat(productDTOList.getContent(), is(empty())); } // Find By Barcode @Test - void whenAProductBarcodeIsValidThenReturnAProduct() throws ProductNotFoundException { + void whenAProductBarcodeIsValidThenReturnAProduct() { // given var product = ModelUtils.getProduct(); // when - when(this.productRepository.findByBarCode(product.getBarCode())) + when(this.productRepository.findByCode(product.getCode())) .thenReturn(Optional.of(product)); // then - var productDTO = this.productService.findByBarCode(product.getBarCode()); + var productDTO = this.productService.findByCode(product.getCode()); assertThat(productDTO.id(), is(product.getId())); - assertThat(productDTO.barCode(), is(product.getBarCode())); + assertThat(productDTO.code(), is(product.getCode())); assertThat(productDTO.name(), is(product.getName())); } @@ -146,17 +150,17 @@ void whenAProductBarcodeHasNotBeenRegisteredThenAnExceptionMustBeThrown() { var product = ModelUtils.getProduct(); // when - when(this.productRepository.findByBarCode(product.getBarCode())) + when(this.productRepository.findByCode(product.getCode())) .thenReturn(Optional.empty()); // then assertThrows(ProductNotFoundException.class, - () -> this.productService.findByBarCode(product.getBarCode())); + () -> this.productService.findByCode(product.getCode())); } // Find By Id @Test - public void whenAProductIdIsValidThenReturnAProduct() throws ProductNotFoundException { + public void whenAProductIdIsValidThenReturnAProduct() { // given var product = ModelUtils.getProduct(); @@ -168,7 +172,7 @@ public void whenAProductIdIsValidThenReturnAProduct() throws ProductNotFoundExce var productDTO = this.productService.findById(PRODUCT_ID); assertThat(productDTO.id(), is(product.getId())); - assertThat(productDTO.barCode(), is(product.getBarCode())); + assertThat(productDTO.code(), is(product.getCode())); assertThat(productDTO.name(), is(product.getName())); } @@ -232,7 +236,7 @@ void shouldUpdateAProduct() throws ProductNotFoundException { var productDTO = this.productService.updateById(PRODUCT_ID, productUpdateForm); assertThat(productDTO.name(), is(product.getName())); - assertThat(productDTO.barCode(), is(product.getBarCode())); + assertThat(productDTO.code(), is(product.getCode())); } @Test diff --git a/src/test/java/com/gilberto/logistockapi/utils/ModelUtils.java b/src/test/java/com/gilberto/logistockapi/utils/ModelUtils.java index 239716b..7e56219 100644 --- a/src/test/java/com/gilberto/logistockapi/utils/ModelUtils.java +++ b/src/test/java/com/gilberto/logistockapi/utils/ModelUtils.java @@ -1,5 +1,6 @@ package com.gilberto.logistockapi.utils; +import com.gilberto.logistockapi.models.dto.SummaryProduct; import com.gilberto.logistockapi.models.dto.request.ProductFilter; import com.gilberto.logistockapi.models.dto.request.ProductForm; import com.gilberto.logistockapi.models.dto.request.ProductUpdateForm; @@ -42,7 +43,7 @@ public static Product getProduct() { .category(Category.FOOD) .description("description") .unitPrice(BigDecimal.valueOf(1.99)) - .barCode("barcode") + .code("barcode") .stockQuantity(10) .measureUnit(MeasureUnit.KILOGRAM) .maxStockLevel(100) @@ -71,7 +72,18 @@ public static ProductDTO getProductDTO() { BigDecimal.valueOf(1.99), MeasureUnit.PACK, 10, + 10, "description"); } + + public static SummaryProduct getSummaryProduct() { + return new SummaryProduct( + 1L, + "teste", + Category.CLOTHING, + BigDecimal.TEN, + 1 + ); + } }