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());
+ }
+ }
+