diff --git a/pom.xml b/pom.xml index 8a7b22b..306a02f 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,16 @@ spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-security + + org.postgresql postgresql @@ -56,6 +66,13 @@ 1.18.30 provided + + + org.projectlombok + lombok + true + + junit junit diff --git a/src/main/java/com/orderflow/ecommerce/config/SecurityConfig.java b/src/main/java/com/orderflow/ecommerce/config/SecurityConfig.java new file mode 100644 index 0000000..0e9ff97 --- /dev/null +++ b/src/main/java/com/orderflow/ecommerce/config/SecurityConfig.java @@ -0,0 +1,32 @@ +package com.orderflow.ecommerce.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) // Desabilita CSRF (comum em APIs REST) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // API sem estado + .authorizeHttpRequests(auth -> auth + .anyRequest().permitAll() // Por enquanto, libera tudo para você não se travar + ); + + return http.build(); + } + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/com/orderflow/ecommerce/controllers/CategoryController.java b/src/main/java/com/orderflow/ecommerce/controllers/CategoryController.java index 7d1292d..0c3ffe1 100644 --- a/src/main/java/com/orderflow/ecommerce/controllers/CategoryController.java +++ b/src/main/java/com/orderflow/ecommerce/controllers/CategoryController.java @@ -2,11 +2,13 @@ import com.orderflow.ecommerce.entities.Category; import com.orderflow.ecommerce.repositories.CategoryRepository; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.NoSuchElementException; @RestController @RequestMapping(value = "/categories") @@ -22,15 +24,13 @@ public ResponseEntity> findAll() { @GetMapping(value = "/{id}") public ResponseEntity findById(@PathVariable Long id) { - - - return repository.findById(id) - .map(obj -> ResponseEntity.ok().body(obj)) - .orElse(ResponseEntity.notFound().build()); + Category obj = repository.findById(id) + .orElseThrow(() -> new NoSuchElementException("Categoria não encontrada com o ID: " + id)); + return ResponseEntity.ok().body(obj); } @PostMapping - public ResponseEntity insert(@RequestBody Category obj) { + public ResponseEntity insert(@Valid @RequestBody Category obj) { return ResponseEntity.ok().body(repository.save(obj)); } @@ -41,13 +41,11 @@ public ResponseEntity delete(@PathVariable Long id) { } @PutMapping(value = "/{id}") - public ResponseEntity update(@PathVariable Long id, @RequestBody Category obj) { - return repository.findById(id) - .map(entity -> { - entity.setName(obj.getName()); - Category updated = repository.save(entity); - return ResponseEntity.ok().body(updated); - }) - .orElse(ResponseEntity.notFound().build()); + public ResponseEntity update(@PathVariable Long id, @Valid @RequestBody Category obj) { + Category entity = repository.findById(id) + .orElseThrow(() -> new NoSuchElementException("Categoria não encontrada para atualizar")); + + entity.setName(obj.getName()); + return ResponseEntity.ok().body(repository.save(entity)); } } \ No newline at end of file diff --git a/src/main/java/com/orderflow/ecommerce/controllers/ProductController.java b/src/main/java/com/orderflow/ecommerce/controllers/ProductController.java index 8c14bb0..57962ff 100644 --- a/src/main/java/com/orderflow/ecommerce/controllers/ProductController.java +++ b/src/main/java/com/orderflow/ecommerce/controllers/ProductController.java @@ -2,11 +2,13 @@ import com.orderflow.ecommerce.entities.Product; import com.orderflow.ecommerce.repositories.ProductRepository; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.NoSuchElementException; @RestController @RequestMapping(value = "/products") @@ -22,13 +24,13 @@ public ResponseEntity> findAll() { @GetMapping(value = "/{id}") public ResponseEntity findById(@PathVariable Long id) { - return repository.findById(id) - .map(obj -> ResponseEntity.ok().body(obj)) - .orElse(ResponseEntity.notFound().build()); + Product obj = repository.findById(id) + .orElseThrow(() -> new NoSuchElementException("Produto não encontrado")); + return ResponseEntity.ok().body(obj); } @PostMapping - public ResponseEntity insert(@RequestBody Product obj) { + public ResponseEntity insert(@Valid @RequestBody Product obj) { return ResponseEntity.ok().body(repository.save(obj)); } @@ -39,15 +41,16 @@ public ResponseEntity delete(@PathVariable Long id) { } @PutMapping(value = "/{id}") - public ResponseEntity update(@PathVariable Long id, @RequestBody Product obj) { - Product entity = repository.findById(id).get(); + public ResponseEntity update(@PathVariable Long id, @Valid @RequestBody Product obj) { + Product entity = repository.findById(id) + .orElseThrow(() -> new NoSuchElementException("Produto não encontrado para atualizar")); + entity.setName(obj.getName()); entity.setDescription(obj.getDescription()); entity.setPrice(obj.getPrice()); entity.setStockQuantity(obj.getStockQuantity()); entity.setCategory(obj.getCategory()); - System.out.println("aavavvvv"); - return ResponseEntity.ok().body(repository.save(entity)); + return ResponseEntity.ok().body(repository.save(entity)); } } \ No newline at end of file diff --git a/src/main/java/com/orderflow/ecommerce/dtos/ErrorResponse.java b/src/main/java/com/orderflow/ecommerce/dtos/ErrorResponse.java new file mode 100644 index 0000000..0fe7b4e --- /dev/null +++ b/src/main/java/com/orderflow/ecommerce/dtos/ErrorResponse.java @@ -0,0 +1,11 @@ +package com.orderflow.ecommerce.dtos; + +import java.time.Instant; + +public record ErrorResponse( + Instant timestamp, + Integer status, + String message, + String path +) { +} \ No newline at end of file diff --git a/src/main/java/com/orderflow/ecommerce/entities/Category.java b/src/main/java/com/orderflow/ecommerce/entities/Category.java index 3c08a5e..fdc39a3 100644 --- a/src/main/java/com/orderflow/ecommerce/entities/Category.java +++ b/src/main/java/com/orderflow/ecommerce/entities/Category.java @@ -1,6 +1,8 @@ package com.orderflow.ecommerce.entities; import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import lombok.*; @Entity @@ -16,5 +18,7 @@ public class Category { private Long id; @Column(nullable = false, unique = true) + @NotBlank(message = "O nome da categoria é obrigatório") + @Pattern(regexp = "^[a-zA-ZÀ-ÿ ]+$", message = "O nome deve conter apenas letras") private String name; } diff --git a/src/main/java/com/orderflow/ecommerce/entities/Product.java b/src/main/java/com/orderflow/ecommerce/entities/Product.java index e72b010..5f2c96e 100644 --- a/src/main/java/com/orderflow/ecommerce/entities/Product.java +++ b/src/main/java/com/orderflow/ecommerce/entities/Product.java @@ -1,6 +1,9 @@ package com.orderflow.ecommerce.entities; import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; import lombok.*; import java.math.BigDecimal; @@ -16,12 +19,15 @@ public class Product { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @NotBlank(message = "O nome do produto é obrigatório") + @Pattern(regexp = "^[a-zA-ZÀ-ÿ ]+$", message = "O nome deve conter apenas letras") @Column(nullable = false) private String name; @Column(columnDefinition = "TEXT") private String description; + @NotNull(message = "O preço é obrigatório") @Column(nullable = false) private BigDecimal price; diff --git a/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java b/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java new file mode 100644 index 0000000..c850fbf --- /dev/null +++ b/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java @@ -0,0 +1,62 @@ +package com.orderflow.ecommerce.exceptions; + +import org.springframework.security.access.AccessDeniedException; +import com.orderflow.ecommerce.dtos.ErrorResponse; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import java.time.Instant; +import java.util.NoSuchElementException; +import java.util.stream.Collectors; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidation(MethodArgumentNotValidException ex, HttpServletRequest request) { + String errors = ex.getBindingResult().getFieldErrors() + .stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .collect(Collectors.joining(", ")); + + ErrorResponse err = new ErrorResponse(Instant.now(), HttpStatus.BAD_REQUEST.value(), errors, request.getRequestURI()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(err); + } + + @ExceptionHandler(NoSuchElementException.class) + public ResponseEntity handleNotFound(NoSuchElementException ex, HttpServletRequest request) { + ErrorResponse err = new ErrorResponse(Instant.now(), HttpStatus.NOT_FOUND.value(), ex.getMessage(), request.getRequestURI()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(err); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGenericException(Exception ex, HttpServletRequest request) { + ErrorResponse err = new ErrorResponse(Instant.now(), HttpStatus.INTERNAL_SERVER_ERROR.value(), "Erro interno no servidor", request.getRequestURI()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(err); + } + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpServletRequest request) { + String message = "Erro na estrutura do JSON: Verifique se você enviou números onde deveria ser texto, ou vice-versa."; + + if (ex.getMessage().contains("Cannot deserialize")) { + message = "Erro de tipo de dado: Você tentou inserir um valor incompatível com o campo (ex: letras em números ou formato inválido)."; + } + + ErrorResponse err = new ErrorResponse(Instant.now(), HttpStatus.BAD_REQUEST.value(), message, request.getRequestURI()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(err); + } + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDenied(AccessDeniedException ex, HttpServletRequest request) { + ErrorResponse err = new ErrorResponse( + Instant.now(), + HttpStatus.FORBIDDEN.value(), + "Você não tem permissão para acessar este recurso.", + request.getRequestURI() + ); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(err); + } +} diff --git a/src/test/java/com/orderflow/ecommerce/entities/CategoryTest.java b/src/test/java/com/orderflow/ecommerce/entities/CategoryTest.java new file mode 100644 index 0000000..677b804 --- /dev/null +++ b/src/test/java/com/orderflow/ecommerce/entities/CategoryTest.java @@ -0,0 +1,39 @@ +package com.orderflow.ecommerce.entities; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class CategoryTest { + + @Test + void shouldCreateCategoryWithCorrectName() { + Category cat = new Category(null, "Eletrônicos"); + + assertNotNull(cat); + assertEquals("Eletrônicos", cat.getName()); + } + + @Test + void shouldUpdateCategoryName() { + Category cat = new Category(1L, "Livros"); + cat.setName("Games"); + + assertEquals("Games", cat.getName()); + } + @Test + void shouldCreateCategoryWithNullId() { + Category cat = new Category(); + cat.setName("Moda"); + + assertNotNull(cat); + assertEquals("Moda", cat.getName()); + } + + @Test + void shouldHandleEmptyCategory() { + Category cat = new Category(); + assertEquals(null, cat.getName()); + } + } +