From 88b988f57b6412c488e5fffe372696253d19d3bf Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Fri, 22 May 2026 13:15:42 -0300 Subject: [PATCH 01/16] feat(user): add user entity --- .../orderflow/ecommerce/entities/User.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 api/src/main/java/com/orderflow/ecommerce/entities/User.java diff --git a/api/src/main/java/com/orderflow/ecommerce/entities/User.java b/api/src/main/java/com/orderflow/ecommerce/entities/User.java new file mode 100644 index 0000000..734898d --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/entities/User.java @@ -0,0 +1,74 @@ +package com.orderflow.ecommerce.entities; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDate; + +@Entity +@Table(name = "tb_user") +@Getter +@Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@EqualsAndHashCode(of = "id") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + @Column(nullable = false, unique = true) + private String email; + private String password; + + + // Customer information for Invoices + /** + * CPF or CNPJ + */ + @Column(name = "tax_id", nullable = false, unique = true, length = 20) + private String taxId; + + /** + * Estadual registration (IE) + */ + @Column(length = 30) + private String stateRegistration; + + private String phone; + private LocalDate birthDate; + + /** + * Used in Invoices (NF-e) + */ + private Boolean taxpayerType; + + /** + * Google API + */ + @Column(unique = true) + private String googleId; + + /** + * private Address address; + */ + @Column(length = 40) + private String street; + @Column(length = 40) + private String complement; + @Column(length = 10) + private String number; + @Column(length = 40) + private String neighborhood; + @Column(length = 40) + private String city; + @Column(length = 40) + private String country; + @Column(length = 2) + private String state; + @Column(length = 10) + private String zipCode; + +} From b4a54a63915353600cef1c644bec10da0ed7cdc1 Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Fri, 22 May 2026 13:28:09 -0300 Subject: [PATCH 02/16] feat(user): add user repository --- .../ecommerce/repositories/UserRepository.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 api/src/main/java/com/orderflow/ecommerce/repositories/UserRepository.java diff --git a/api/src/main/java/com/orderflow/ecommerce/repositories/UserRepository.java b/api/src/main/java/com/orderflow/ecommerce/repositories/UserRepository.java new file mode 100644 index 0000000..c078faf --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/repositories/UserRepository.java @@ -0,0 +1,12 @@ +package com.orderflow.ecommerce.repositories; + +import com.orderflow.ecommerce.entities.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + + Optional findByEmailIgnoreCase(String email); + +} From 3f43a09ee0bd5a12e8fbeabfe1ab068923234422 Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Fri, 22 May 2026 13:59:47 -0300 Subject: [PATCH 03/16] refactor(user): rename taxpayer property --- api/src/main/java/com/orderflow/ecommerce/entities/User.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/com/orderflow/ecommerce/entities/User.java b/api/src/main/java/com/orderflow/ecommerce/entities/User.java index 734898d..1fbb45d 100644 --- a/api/src/main/java/com/orderflow/ecommerce/entities/User.java +++ b/api/src/main/java/com/orderflow/ecommerce/entities/User.java @@ -32,7 +32,7 @@ public class User { private String taxId; /** - * Estadual registration (IE) + * State registration (IE) */ @Column(length = 30) private String stateRegistration; @@ -43,7 +43,7 @@ public class User { /** * Used in Invoices (NF-e) */ - private Boolean taxpayerType; + private Boolean taxpayer; /** * Google API From 36fea87627fcae4f9df1b09eff581e496dcfa947 Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Fri, 22 May 2026 15:38:56 -0300 Subject: [PATCH 04/16] feat(user): implement user listing --- .../ecommerce/controllers/UserController.java | 37 +++++++++++++++++++ .../com/orderflow/ecommerce/dtos/UserDto.java | 32 ++++++++++++++++ .../ecommerce/services/UserService.java | 32 ++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java create mode 100644 api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java create mode 100644 api/src/main/java/com/orderflow/ecommerce/services/UserService.java diff --git a/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java new file mode 100644 index 0000000..c661073 --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java @@ -0,0 +1,37 @@ +package com.orderflow.ecommerce.controllers; + +import com.orderflow.ecommerce.dtos.UserDto; +import com.orderflow.ecommerce.services.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.util.UUID; + +@Controller +@RequestMapping("/users") +public class UserController { + + @Autowired + private UserService service; + + @GetMapping(value = "/{id}") + public ResponseEntity findById(@PathVariable Long id) { + return ResponseEntity.ok(service.findById(id)); + } + + @GetMapping + public ResponseEntity> findAll(Pageable pageable) { + return ResponseEntity.ok().body(service.findAllPaged(pageable)); + } + + @GetMapping(params = "email") + public ResponseEntity findByEmail(String email) { + return ResponseEntity.ok().body(service.findByEmail(email)); + } +} diff --git a/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java b/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java new file mode 100644 index 0000000..3d3d577 --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java @@ -0,0 +1,32 @@ +package com.orderflow.ecommerce.dtos; + +import com.orderflow.ecommerce.entities.User; + +import java.time.LocalDate; + +public record UserDto( + Long id, + String name, + String email, + String password, + String taxId, + String stateRegistration, + String phone, + LocalDate birthDate, + Boolean taxpayer, + String googleId, + String street, + String complement, + String number, + String neighborhood, + String city, + String country, + String state, + String zipCode +) { + public UserDto(User entity) { + this(entity.getId(), entity.getName(), entity.getEmail(), entity.getPassword(), entity.getTaxId(), entity.getStateRegistration(), entity.getPhone(), entity.getBirthDate(), entity.getTaxpayer(), entity.getGoogleId(), entity.getStreet(), entity.getComplement(), entity.getNumber(), entity.getNeighborhood(), entity.getCity(), entity.getCountry(), entity.getState(), entity.getZipCode()); + } +} + + diff --git a/api/src/main/java/com/orderflow/ecommerce/services/UserService.java b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java new file mode 100644 index 0000000..04e1a95 --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java @@ -0,0 +1,32 @@ +package com.orderflow.ecommerce.services; + +import com.orderflow.ecommerce.dtos.UserDto; +import com.orderflow.ecommerce.repositories.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.NoSuchElementException; + +@Service +public class UserService { + + @Autowired + private UserRepository repository; + @Transactional(readOnly = true) + public UserDto findById(Long id) { + return new UserDto(repository.findById(id).orElseThrow(() -> new NoSuchElementException("User not found"))); + } + + @Transactional(readOnly = true) + public UserDto findByEmail(String email) { + return new UserDto(repository.findByEmailIgnoreCase(email).orElseThrow(() -> new NoSuchElementException("User not found"))); + } + + @Transactional(readOnly = true) + public Page findAllPaged(Pageable pageable) { + return repository.findAll(pageable).map(UserDto::new); + } +} From aeab4c40e5df123099abec837316ee5efa9ac3b3 Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Fri, 22 May 2026 16:28:44 -0300 Subject: [PATCH 05/16] feat(user): implement user creation --- .../ecommerce/controllers/UserController.java | 15 +++++++--- .../orderflow/ecommerce/entities/User.java | 2 +- .../ecommerce/services/UserService.java | 30 +++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java index c661073..4563ad2 100644 --- a/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java +++ b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java @@ -7,11 +7,10 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import java.util.UUID; +import java.net.URI; @Controller @RequestMapping("/users") @@ -34,4 +33,12 @@ public ResponseEntity> findAll(Pageable pageable) { public ResponseEntity findByEmail(String email) { return ResponseEntity.ok().body(service.findByEmail(email)); } + + @PostMapping + public ResponseEntity insert(@RequestBody UserDto dto) { + dto = service.insert(dto); + URI uri = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}") + .buildAndExpand(dto.id()).toUri(); + return ResponseEntity.created(uri).body(dto); + } } diff --git a/api/src/main/java/com/orderflow/ecommerce/entities/User.java b/api/src/main/java/com/orderflow/ecommerce/entities/User.java index 1fbb45d..d47dd20 100644 --- a/api/src/main/java/com/orderflow/ecommerce/entities/User.java +++ b/api/src/main/java/com/orderflow/ecommerce/entities/User.java @@ -9,7 +9,7 @@ @Table(name = "tb_user") @Getter @Setter -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode(of = "id") public class User { diff --git a/api/src/main/java/com/orderflow/ecommerce/services/UserService.java b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java index 04e1a95..8909ac2 100644 --- a/api/src/main/java/com/orderflow/ecommerce/services/UserService.java +++ b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java @@ -1,6 +1,7 @@ package com.orderflow.ecommerce.services; import com.orderflow.ecommerce.dtos.UserDto; +import com.orderflow.ecommerce.entities.User; import com.orderflow.ecommerce.repositories.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -29,4 +30,33 @@ public UserDto findByEmail(String email) { public Page findAllPaged(Pageable pageable) { return repository.findAll(pageable).map(UserDto::new); } + + @Transactional + public UserDto insert(UserDto dto) { + return new UserDto(saveEntity(dto)); + } + + private User saveEntity(UserDto dto) { + User entity = new User(); + + entity.setName(dto.name()); + entity.setEmail(dto.email()); + entity.setPassword(dto.password()); + entity.setTaxId(dto.taxId()); + entity.setStateRegistration(dto.stateRegistration()); + entity.setPhone(dto.phone()); + entity.setBirthDate(dto.birthDate()); + entity.setTaxpayer(dto.taxpayer()); + entity.setGoogleId(dto.googleId()); + entity.setStreet(dto.street()); + entity.setComplement(dto.complement()); + entity.setNumber(dto.number()); + entity.setNeighborhood(dto.neighborhood()); + entity.setCity(dto.city()); + entity.setCountry(dto.country()); + entity.setState(dto.state()); + entity.setZipCode(dto.zipCode()); + + return repository.save(entity); + } } From e6863ea0db1a4148d3e981caaa584e1aee6e9216 Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Fri, 22 May 2026 16:35:37 -0300 Subject: [PATCH 06/16] feat(user): implement user update --- .../ecommerce/controllers/UserController.java | 5 +++++ .../ecommerce/services/UserService.java | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java index 4563ad2..9846cc4 100644 --- a/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java +++ b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java @@ -41,4 +41,9 @@ public ResponseEntity insert(@RequestBody UserDto dto) { .buildAndExpand(dto.id()).toUri(); return ResponseEntity.created(uri).body(dto); } + + @PutMapping(value = "/{id}") + public ResponseEntity update(@PathVariable Long id, @RequestBody UserDto dto) { + return ResponseEntity.ok().body(service.update(id, dto)); + } } diff --git a/api/src/main/java/com/orderflow/ecommerce/services/UserService.java b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java index 8909ac2..4f3bfd1 100644 --- a/api/src/main/java/com/orderflow/ecommerce/services/UserService.java +++ b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java @@ -3,6 +3,7 @@ import com.orderflow.ecommerce.dtos.UserDto; import com.orderflow.ecommerce.entities.User; import com.orderflow.ecommerce.repositories.UserRepository; +import jakarta.persistence.EntityNotFoundException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -33,12 +34,24 @@ public Page findAllPaged(Pageable pageable) { @Transactional public UserDto insert(UserDto dto) { - return new UserDto(saveEntity(dto)); + return new UserDto(saveEntity(null, dto)); } - private User saveEntity(UserDto dto) { + @Transactional + public UserDto update(Long id, UserDto dto) { + try { + return new UserDto(saveEntity(id, dto)); + } + catch (EntityNotFoundException e) { + throw new NoSuchElementException("Id not found " + id); + } + } + + private User saveEntity(Long id, UserDto dto) { User entity = new User(); + if(id != null) entity = repository.getReferenceById(id); + entity.setName(dto.name()); entity.setEmail(dto.email()); entity.setPassword(dto.password()); From 5d2d8ac04294f33f9278e4d9fec6b36d473d6e94 Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Fri, 22 May 2026 16:44:53 -0300 Subject: [PATCH 07/16] feat(user): implement user deletion --- .../ecommerce/controllers/UserController.java | 6 ++++++ .../exceptions/GlobalExceptionHandler.java | 7 +++++++ .../orderflow/ecommerce/services/UserService.java | 15 +++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java index 9846cc4..0ae48d0 100644 --- a/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java +++ b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java @@ -42,6 +42,12 @@ public ResponseEntity insert(@RequestBody UserDto dto) { return ResponseEntity.created(uri).body(dto); } + @DeleteMapping(value = "/{id}") + public ResponseEntity delete(@PathVariable Long id) { + service.delete(id); + return ResponseEntity.noContent().build(); + } + @PutMapping(value = "/{id}") public ResponseEntity update(@PathVariable Long id, @RequestBody UserDto dto) { return ResponseEntity.ok().body(service.update(id, dto)); diff --git a/api/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java b/api/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java index c850fbf..ece2297 100644 --- a/api/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java +++ b/api/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package com.orderflow.ecommerce.exceptions; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.security.access.AccessDeniedException; import com.orderflow.ecommerce.dtos.ErrorResponse; import jakarta.servlet.http.HttpServletRequest; @@ -59,4 +60,10 @@ public ResponseEntity handleAccessDenied(AccessDeniedException ex ); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(err); } + + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleIntegrityViolation(DataIntegrityViolationException ex, HttpServletRequest request) { + ErrorResponse err = new ErrorResponse(Instant.now(), HttpStatus.BAD_REQUEST.value(), ex.getMessage(), request.getRequestURI()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(err); + } } diff --git a/api/src/main/java/com/orderflow/ecommerce/services/UserService.java b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java index 4f3bfd1..e861406 100644 --- a/api/src/main/java/com/orderflow/ecommerce/services/UserService.java +++ b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java @@ -5,10 +5,15 @@ import com.orderflow.ecommerce.repositories.UserRepository; import jakarta.persistence.EntityNotFoundException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; import java.util.NoSuchElementException; @@ -47,6 +52,16 @@ public UserDto update(Long id, UserDto dto) { } } + @Transactional + public void delete(Long id) { + try { + repository.deleteById(id); + } + catch (DataIntegrityViolationException e) { + throw new DataIntegrityViolationException("Integrity violation"); + } + } + private User saveEntity(Long id, UserDto dto) { User entity = new User(); From b05fced91955a9928099bf2cc651b6d48b285762 Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Fri, 22 May 2026 17:46:15 -0300 Subject: [PATCH 08/16] refactor(user): add bean validation --- .../ecommerce/controllers/UserController.java | 6 +++-- .../com/orderflow/ecommerce/dtos/UserDto.java | 13 +++++++++++ .../DuplicateResourceException.java | 8 +++++++ .../exceptions/GlobalExceptionHandler.java | 12 ++++++++-- .../repositories/UserRepository.java | 5 ++++ .../ecommerce/services/UserService.java | 23 +++++++++++++++---- 6 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 api/src/main/java/com/orderflow/ecommerce/exceptions/DuplicateResourceException.java diff --git a/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java index 0ae48d0..204a1ce 100644 --- a/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java +++ b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java @@ -2,6 +2,7 @@ import com.orderflow.ecommerce.dtos.UserDto; import com.orderflow.ecommerce.services.UserService; +import jakarta.validation.Valid; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -34,8 +35,9 @@ public ResponseEntity findByEmail(String email) { return ResponseEntity.ok().body(service.findByEmail(email)); } + @Valid @PostMapping - public ResponseEntity insert(@RequestBody UserDto dto) { + public ResponseEntity insert(@Valid @RequestBody UserDto dto) { dto = service.insert(dto); URI uri = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}") .buildAndExpand(dto.id()).toUri(); @@ -49,7 +51,7 @@ public ResponseEntity delete(@PathVariable Long id) { } @PutMapping(value = "/{id}") - public ResponseEntity update(@PathVariable Long id, @RequestBody UserDto dto) { + public ResponseEntity update(@PathVariable Long id, @Valid @RequestBody UserDto dto) { return ResponseEntity.ok().body(service.update(id, dto)); } } diff --git a/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java b/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java index 3d3d577..1f10c88 100644 --- a/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java +++ b/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java @@ -1,13 +1,18 @@ package com.orderflow.ecommerce.dtos; import com.orderflow.ecommerce.entities.User; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import java.time.LocalDate; public record UserDto( Long id, + @NotBlank(message = "Campo requerido") String name, + @NotBlank(message = "Campo requerido") String email, + @NotBlank(message = "Campo requerido") String password, String taxId, String stateRegistration, @@ -15,13 +20,21 @@ public record UserDto( LocalDate birthDate, Boolean taxpayer, String googleId, + @Size(max = 40, message = "Máximo 40 caracteres") String street, + @Size(max = 40, message = "Máximo 40 caracteres") String complement, + @Size(max = 10, message = "Máximo 10 caracteres") String number, + @Size(max = 40, message = "Máximo 40 caracteres") String neighborhood, + @Size(max = 40, message = "Máximo 40 caracteres") String city, + @Size(max = 40, message = "Máximo 40 caracteres") String country, + @Size(max = 2, message = "Máximo 2 caracteres") String state, + @Size(max = 10, message = "Máximo 10 caracteres") String zipCode ) { public UserDto(User entity) { diff --git a/api/src/main/java/com/orderflow/ecommerce/exceptions/DuplicateResourceException.java b/api/src/main/java/com/orderflow/ecommerce/exceptions/DuplicateResourceException.java new file mode 100644 index 0000000..889f0ca --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/exceptions/DuplicateResourceException.java @@ -0,0 +1,8 @@ +package com.orderflow.ecommerce.exceptions; + +public class DuplicateResourceException extends RuntimeException { + + public DuplicateResourceException(String message) { + super(message); + } +} diff --git a/api/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java b/api/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java index ece2297..749eafd 100644 --- a/api/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java +++ b/api/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java @@ -63,7 +63,15 @@ public ResponseEntity handleAccessDenied(AccessDeniedException ex @ExceptionHandler(DataIntegrityViolationException.class) public ResponseEntity handleIntegrityViolation(DataIntegrityViolationException ex, HttpServletRequest request) { - ErrorResponse err = new ErrorResponse(Instant.now(), HttpStatus.BAD_REQUEST.value(), ex.getMessage(), request.getRequestURI()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(err); + HttpStatus status = HttpStatus.BAD_REQUEST; + ErrorResponse err = new ErrorResponse(Instant.now(), status.value(), ex.getMessage(), request.getRequestURI()); + return ResponseEntity.status(status).body(err); + } + + @ExceptionHandler(DuplicateResourceException.class) + public ResponseEntity handleDuplicateResource(DuplicateResourceException ex, HttpServletRequest request) { + HttpStatus status = HttpStatus.UNPROCESSABLE_ENTITY; + ErrorResponse err = new ErrorResponse(Instant.now(), status.value(), ex.getMessage(), request.getRequestURI()); + return ResponseEntity.status(status).body(err); } } diff --git a/api/src/main/java/com/orderflow/ecommerce/repositories/UserRepository.java b/api/src/main/java/com/orderflow/ecommerce/repositories/UserRepository.java index c078faf..2185dd4 100644 --- a/api/src/main/java/com/orderflow/ecommerce/repositories/UserRepository.java +++ b/api/src/main/java/com/orderflow/ecommerce/repositories/UserRepository.java @@ -9,4 +9,9 @@ public interface UserRepository extends JpaRepository { Optional findByEmailIgnoreCase(String email); + boolean existsByEmail(String email); + boolean existsByEmailAndIdNot(String email, Long id); + + boolean existsByTaxId(String taxId); + boolean existsByTaxIdAndIdNot(String taxId, Long id); } diff --git a/api/src/main/java/com/orderflow/ecommerce/services/UserService.java b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java index e861406..c4b937d 100644 --- a/api/src/main/java/com/orderflow/ecommerce/services/UserService.java +++ b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java @@ -2,18 +2,15 @@ import com.orderflow.ecommerce.dtos.UserDto; import com.orderflow.ecommerce.entities.User; +import com.orderflow.ecommerce.exceptions.DuplicateResourceException; import com.orderflow.ecommerce.repositories.UserRepository; import jakarta.persistence.EntityNotFoundException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PathVariable; import java.util.NoSuchElementException; @@ -65,7 +62,23 @@ public void delete(Long id) { private User saveEntity(Long id, UserDto dto) { User entity = new User(); - if(id != null) entity = repository.getReferenceById(id); + if(id != null){ // if updating + entity = repository.getReferenceById(id); + if (repository.existsByEmailAndIdNot(dto.email(), id)) { + throw new DuplicateResourceException("Email já cadastrado para outro usuário!"); + } + if (repository.existsByTaxIdAndIdNot(dto.taxId(), id)) { + throw new DuplicateResourceException("CPF/CNPJ já cadastrado para outro usuário!"); + } + } else { + if (repository.existsByEmail(dto.email())) { + throw new DuplicateResourceException("Email já cadastrado!"); + } + if (repository.existsByTaxId(dto.taxId())) { + throw new DuplicateResourceException("CPF/CNPJ já cadastrado!"); + } + } + entity.setName(dto.name()); entity.setEmail(dto.email()); From c029e26439fbb4a9be077aadf798f9f7d56d8b9e Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Sat, 23 May 2026 16:15:02 -0300 Subject: [PATCH 09/16] test(user): add entity unit test --- .../ecommerce/entities/UserTest.java | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 api/src/test/java/com/orderflow/ecommerce/entities/UserTest.java diff --git a/api/src/test/java/com/orderflow/ecommerce/entities/UserTest.java b/api/src/test/java/com/orderflow/ecommerce/entities/UserTest.java new file mode 100644 index 0000000..bebf451 --- /dev/null +++ b/api/src/test/java/com/orderflow/ecommerce/entities/UserTest.java @@ -0,0 +1,96 @@ +package com.orderflow.ecommerce.entities; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDate; + +public class UserTest { + + @Test + void shouldInstantiateWithAllArgsConstructor() { + // Arrange & Act + User user = new User(1L, "Alice", "alice@example.com", "password123", + "12345678901", "IE123456", "11987654321", + LocalDate.of(1990, 5, 15), true, "google-id-123", + "Rua A", "Apt 101", "100", "Centro", "São Paulo", + "Brazil", "SP", "01000-000"); + + // Assert + assertEquals(1L, user.getId()); + assertEquals("Alice", user.getName()); + assertEquals("alice@example.com", user.getEmail()); + assertEquals("password123", user.getPassword()); + assertEquals("12345678901", user.getTaxId()); + assertEquals("IE123456", user.getStateRegistration()); + assertEquals("11987654321", user.getPhone()); + assertEquals(LocalDate.of(1990, 5, 15), user.getBirthDate()); + assertTrue(user.getTaxpayer()); + assertEquals("google-id-123", user.getGoogleId()); + assertEquals("Rua A", user.getStreet()); + assertEquals("Apt 101", user.getComplement()); + assertEquals("100", user.getNumber()); + assertEquals("Centro", user.getNeighborhood()); + assertEquals("São Paulo", user.getCity()); + assertEquals("Brazil", user.getCountry()); + assertEquals("SP", user.getState()); + assertEquals("01000-000", user.getZipCode()); + } + + @Test + void shouldHaveNullsWhenUsingNoArgsConstructor() { + // Arrange & Act + User user = new User(); + + // Assert + assertNull(user.getId()); + assertNull(user.getName()); + assertNull(user.getEmail()); + } + + @Test + void shouldUseSettersAndGetters() { + // Arrange + User user = new User(); + + // Act + user.setId(2L); + user.setName("Bob"); + user.setEmail("bob@example.com"); + user.setPassword("secret"); + user.setTaxId("98765432101"); + user.setPhone("119876543"); + user.setBirthDate(LocalDate.of(1985, 3, 20)); + user.setTaxpayer(false); + user.setStreet("Rua B"); + user.setState("RJ"); + user.setZipCode("20000-000"); + + // Assert + assertEquals(2L, user.getId()); + assertEquals("Bob", user.getName()); + assertEquals("bob@example.com", user.getEmail()); + assertEquals("secret", user.getPassword()); + assertEquals("98765432101", user.getTaxId()); + assertEquals("119876543", user.getPhone()); + assertEquals(LocalDate.of(1985, 3, 20), user.getBirthDate()); + assertFalse(user.getTaxpayer()); + assertEquals("Rua B", user.getStreet()); + assertEquals("RJ", user.getState()); + assertEquals("20000-000", user.getZipCode()); + } + + @Test + void equalsAndHashCodeShouldBeBasedOnlyOnId() { + User a = new User(1L, "A", "a@x", "p", "tax", null, null, null, null, null, null, null, null, null, null, null, null, null); + User b = new User(1L, "B", "b@x", "q", "tax2", null, null, null, null, null, null, null, null, null, null, null, null, null); + User c = new User(2L, "A", "a@x", "p", "tax", null, null, null, null, null, null, null, null, null, null, null, null, null); + + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + + assertNotEquals(a, c); + assertNotEquals(a.hashCode(), c.hashCode()); + } + +} From 6e7090ce3ecfc0f03bb90cc3fa23512064dd4e8b Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Fri, 29 May 2026 19:40:39 -0300 Subject: [PATCH 10/16] refactor(user): add verify id option in delete --- .../com/orderflow/ecommerce/controllers/UserController.java | 6 +++--- .../java/com/orderflow/ecommerce/services/UserService.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java index 204a1ce..d42ee6e 100644 --- a/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java +++ b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java @@ -31,7 +31,7 @@ public ResponseEntity> findAll(Pageable pageable) { } @GetMapping(params = "email") - public ResponseEntity findByEmail(String email) { + public ResponseEntity findByEmail(@RequestParam String email) { return ResponseEntity.ok().body(service.findByEmail(email)); } @@ -45,8 +45,8 @@ public ResponseEntity insert(@Valid @RequestBody UserDto dto) { } @DeleteMapping(value = "/{id}") - public ResponseEntity delete(@PathVariable Long id) { - service.delete(id); + public ResponseEntity delete(@PathVariable Long id, @RequestParam(required = false) boolean verify) { + service.delete(id, verify); return ResponseEntity.noContent().build(); } diff --git a/api/src/main/java/com/orderflow/ecommerce/services/UserService.java b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java index c4b937d..393185c 100644 --- a/api/src/main/java/com/orderflow/ecommerce/services/UserService.java +++ b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java @@ -50,8 +50,9 @@ public UserDto update(Long id, UserDto dto) { } @Transactional - public void delete(Long id) { + public void delete(Long id, boolean verify) { try { + if (verify) repository.findById(id).orElseThrow(() -> new NoSuchElementException("User not found")); repository.deleteById(id); } catch (DataIntegrityViolationException e) { @@ -79,7 +80,6 @@ private User saveEntity(Long id, UserDto dto) { } } - entity.setName(dto.name()); entity.setEmail(dto.email()); entity.setPassword(dto.password()); From 3cc439cdf7162ca496e73db26129037815ac504b Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Sat, 30 May 2026 12:19:10 -0300 Subject: [PATCH 11/16] test(user): add service unit tests --- .../orderflow/ecommerce/auxiliar/Factory.java | 30 +++ .../ecommerce/services/UserServiceTest.java | 240 ++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 api/src/test/java/com/orderflow/ecommerce/auxiliar/Factory.java create mode 100644 api/src/test/java/com/orderflow/ecommerce/services/UserServiceTest.java diff --git a/api/src/test/java/com/orderflow/ecommerce/auxiliar/Factory.java b/api/src/test/java/com/orderflow/ecommerce/auxiliar/Factory.java new file mode 100644 index 0000000..4568447 --- /dev/null +++ b/api/src/test/java/com/orderflow/ecommerce/auxiliar/Factory.java @@ -0,0 +1,30 @@ +package com.orderflow.ecommerce.auxiliar; + +import com.orderflow.ecommerce.dtos.UserDto; +import com.orderflow.ecommerce.entities.User; + +import java.time.LocalDate; + +public class Factory { + public static User createUser(){ + User user = new User(); + user.setId(1L); + user.setName("Bob"); + user.setEmail("bob@gmail.com"); + user.setPassword("shh..secret"); + user.setTaxId("98765432101"); + user.setPhone("1198765432"); + user.setBirthDate(LocalDate.of(1985, 3, 20)); + user.setTaxpayer(false); + user.setStreet("Rua B"); + user.setState("RJ"); + user.setZipCode("20000-000"); + return user; + } + + public static UserDto createUserDto(){ + User user = createUser(); + return new UserDto(user); + } + +} diff --git a/api/src/test/java/com/orderflow/ecommerce/services/UserServiceTest.java b/api/src/test/java/com/orderflow/ecommerce/services/UserServiceTest.java new file mode 100644 index 0000000..ba17810 --- /dev/null +++ b/api/src/test/java/com/orderflow/ecommerce/services/UserServiceTest.java @@ -0,0 +1,240 @@ +package com.orderflow.ecommerce.services; + +import com.orderflow.ecommerce.auxiliar.Factory; +import com.orderflow.ecommerce.dtos.UserDto; +import com.orderflow.ecommerce.entities.User; +import com.orderflow.ecommerce.exceptions.DuplicateResourceException; +import com.orderflow.ecommerce.repositories.UserRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.*; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; + +import static org.mockito.Mockito.times; + +@ExtendWith(SpringExtension.class) +public class UserServiceTest { + + @InjectMocks + private UserService service; + + @Mock + private UserRepository repository; + + private Long existingId, nonExistingId, dependentId; + private String existingUserEmail, nonExistingUserEmail, existingTaxId, nonExistingTaxId; + private User user; + private UserDto userDto; + private PageImpl page; + + @BeforeEach + void setUp() throws Exception { + user = Factory.createUser(); + existingId = user.getId(); + nonExistingId = 2L; + dependentId = 3L; + existingUserEmail = user.getEmail(); + nonExistingUserEmail = "user@gmail.com"; + existingTaxId = user.getTaxId(); + nonExistingTaxId = "99999999999"; + userDto = Factory.createUserDto(); + page = new PageImpl<>(List.of(user)); + + Mockito.when(repository.findByEmailIgnoreCase(existingUserEmail)).thenReturn(Optional.of(user)); + + Mockito.when(repository.findById(existingId)).thenReturn(Optional.of(user)); + Mockito.when(repository.findById(nonExistingId)).thenReturn(Optional.empty()); + Mockito.when(repository.findById(dependentId)).thenReturn(Optional.of(user)); + Mockito.when(repository.findAll((Pageable)ArgumentMatchers.any())).thenReturn(page); + + Mockito.when(repository.save(ArgumentMatchers.any())).thenReturn(user); + + Mockito.when(repository.existsById(existingId)).thenReturn(true); + Mockito.when(repository.existsById(nonExistingId)).thenReturn(false); + Mockito.when(repository.existsById(dependentId)).thenReturn(true); + + Mockito.doNothing().when(repository).deleteById(existingId); + Mockito.doThrow(DataIntegrityViolationException.class).when(repository).deleteById(dependentId); + + Mockito.when(repository.existsByEmail(ArgumentMatchers.anyString())).thenReturn(false); + Mockito.when(repository.existsByTaxId(ArgumentMatchers.anyString())).thenReturn(false); + Mockito.when(repository.existsByEmailAndIdNot(ArgumentMatchers.anyString(), ArgumentMatchers.anyLong())).thenReturn(false); + Mockito.when(repository.existsByTaxIdAndIdNot(ArgumentMatchers.anyString(), ArgumentMatchers.anyLong())).thenReturn(false); + + + Mockito.when(repository.getReferenceById(existingId)).thenReturn(user); + + Mockito.when(repository.getReferenceById(nonExistingId)).thenThrow(NoSuchElementException.class); + } + + //#region find + + @Test + public void findAllPagedShouldReturnPage() { + Pageable pageable = PageRequest.of(0, 12); + Page result = service.findAllPaged(pageable); + Assertions.assertNotNull(result); + Mockito.verify(repository, times(1)).findAll(pageable); + } + + @Test + public void findByIdShouldReturnUserDtoWhenIdExists() { + UserDto result = service.findById(existingId); + Assertions.assertNotNull(result); + } + + @Test + public void findByIdShouldThrowNoSuchElementExceptionWhenIdDoesNotExist() { + Assertions.assertThrows(NoSuchElementException.class, () -> { + service.findById(nonExistingId); + }); + Mockito.verify(repository).findById(nonExistingId); + } + + @Test + public void findByEmailShouldReturnUserDtoWhenValidEmail() { + UserDto result = service.findByEmail(existingUserEmail); + Assertions.assertNotNull(result); + } + + @Test + public void findByEmailShouldThrowNoSuchElementExceptionWhenUserNotFound() { + Assertions.assertThrows(NoSuchElementException.class, () -> { + service.findByEmail(nonExistingUserEmail); + }); + Mockito.verify(repository).findByEmailIgnoreCase(nonExistingUserEmail); + } + + //#endregion + + //#region insert + @Test + void insertShouldSaveWhenNoDuplicates() { + UserDto result = service.insert(userDto); + + ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); + Mockito.verify(repository).save(captor.capture()); + User saved = captor.getValue(); + + Assertions.assertNotNull(result); + Assertions.assertEquals("Bob", result.name()); + Assertions.assertEquals("bob@gmail.com", result.email()); + Mockito.verify(repository, times(1)).save(ArgumentMatchers.any(User.class)); + } + + @Test + void insertShouldThrowDuplicateResourceExceptionWhenEmailDuplicate() { + + Mockito.when(repository.existsByEmail(existingUserEmail)).thenReturn(true); + + Assertions.assertThrows(DuplicateResourceException.class, () -> service.insert(userDto)); + + Mockito.verify(repository, times(0)).save(ArgumentMatchers.any()); + } + + @Test + void insertShouldThrowDuplicateResourceExceptionWhenTaxIdDuplicate() { + Mockito.when(repository.existsByTaxId(existingTaxId)).thenReturn(true); + + Assertions.assertThrows(DuplicateResourceException.class, () -> service.insert(userDto)); + + Mockito.verify(repository, times(0)).save(ArgumentMatchers.any()); + } + + //#endregion + + //#region update + @Test + void updateShouldReturnUserDTOWhenIdExistsAndNoDuplicates() { + UserDto result = service.update(existingId, userDto); + ArgumentCaptor captor = ArgumentCaptor.forClass(User.class); + Mockito.verify(repository).save(captor.capture()); + User saved = captor.getValue(); + Assertions.assertEquals("Bob", result.name()); + Assertions.assertEquals("bob@gmail.com", result.email()); + Mockito.verify(repository, times(1)).save(ArgumentMatchers.any()); + } + + @Test + void updateShouldThrowDuplicateResourceExceptionWhenEmailUsedByAnother() { + + UserDto dto = new UserDto(existingId, "Bob New", "someoneelse@example.com", "pw", + "11111111111", null, null, null, null, null, + null, null, null, null, null, null, null, null); + + Mockito.when(repository.existsByEmailAndIdNot("someoneelse@example.com", existingId)).thenReturn(true); + + Assertions.assertThrows(DuplicateResourceException.class, () -> service.update(existingId, dto)); + + Mockito.verify(repository, times(0)).save(ArgumentMatchers.any()); + } + + @Test + void updateShouldThrowDuplicateResourceExceptionWhenTaxIdEUsedByAnother() { + + UserDto dto = new UserDto(existingId, "Bob New", "someoneelse@example.com", "pw", + "11111111111", null, null, null, null, null, + null, null, null, null, null, null, null, null); + + Mockito.when(repository.getReferenceById(existingId)).thenReturn(user); + Mockito.when(repository.existsByEmailAndIdNot("bob@gmail.com", existingId)).thenReturn(false); + Mockito.when(repository.existsByTaxIdAndIdNot("11111111111", existingId)).thenReturn(true); + + Assertions.assertThrows(DuplicateResourceException.class, () -> service.update(existingId, dto)); + + Mockito.verify(repository, times(0)).save(ArgumentMatchers.any()); + } + + @Test + public void updateShouldThrowNoSuchElementExceptionWhenIdDoesNotExist() { + Assertions.assertThrows(NoSuchElementException.class, () -> { + service.update(nonExistingId, userDto); + }); + } +//#endregion + + //#region delete + @Test + public void deleteShouldThrowDataIntegrityViolationExceptionWhenDependentId() { + + Assertions.assertThrows(DataIntegrityViolationException.class, () -> { + service.delete(dependentId, true); + }); + } + + @Test + public void deleteShouldThrowResourceNotFoundExceptionWhenIdDoesNotExistAndVerifyIsTrue() { + Assertions.assertThrows(NoSuchElementException.class, () -> { + service.delete(nonExistingId, true); + }); + } + + @Test + public void deleteShouldDoNothingWhenIdDoesNotExistAndVerifyIsFalse() { + Mockito.doNothing().when(repository).deleteById(nonExistingId); + Assertions.assertDoesNotThrow(() -> { + service.delete(nonExistingId, false); + }); + } + + @Test + public void deleteShouldDoNothingWhenIdExists() { + Assertions.assertDoesNotThrow(() -> { + service.delete(existingId, true); + }); + Mockito.verify(repository, times(1)).deleteById(existingId); + } + //#endregion + +} From 24cb12f23b721c700e7d79f4f7b1d3cf81a0735b Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Sat, 30 May 2026 17:18:52 -0300 Subject: [PATCH 12/16] refactor(user): add email and password bean validation --- .../main/java/com/orderflow/ecommerce/dtos/UserDto.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java b/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java index 1f10c88..d9f024d 100644 --- a/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java +++ b/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java @@ -1,7 +1,9 @@ package com.orderflow.ecommerce.dtos; import com.orderflow.ecommerce.entities.User; +import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import java.time.LocalDate; @@ -11,8 +13,13 @@ public record UserDto( @NotBlank(message = "Campo requerido") String name, @NotBlank(message = "Campo requerido") + @Email(message = "Email inválido") String email, @NotBlank(message = "Campo requerido") + @Pattern( + regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&]).{8,}$", + message = "A senha deve conter pelo menos 8 caracteres, incluindo letras maiúsculas, minúsculas, números e caracteres especiais" + ) String password, String taxId, String stateRegistration, From 8c776138e1665260d2abfcf78123c71bd15af8f8 Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Sat, 30 May 2026 19:01:27 -0300 Subject: [PATCH 13/16] fix(user): fix controller annotation --- .../com/orderflow/ecommerce/controllers/UserController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java index d42ee6e..11e293f 100644 --- a/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java +++ b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java @@ -13,7 +13,7 @@ import java.net.URI; -@Controller +@RestController @RequestMapping("/users") public class UserController { From 7573b74324afc7d8f2cc3255bca68961744a955d Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Sat, 30 May 2026 19:05:03 -0300 Subject: [PATCH 14/16] refactor(user): update password bean validation --- api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java b/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java index d9f024d..d912469 100644 --- a/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java +++ b/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java @@ -17,7 +17,7 @@ public record UserDto( String email, @NotBlank(message = "Campo requerido") @Pattern( - regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&]).{8,}$", + regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&.]).{8,}$", message = "A senha deve conter pelo menos 8 caracteres, incluindo letras maiúsculas, minúsculas, números e caracteres especiais" ) String password, From 9b94cf37d0b4cb632487ef3b3914e41da58c9316 Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Sat, 30 May 2026 19:06:10 -0300 Subject: [PATCH 15/16] test(user): add controller unit tests --- .../orderflow/ecommerce/auxiliar/Factory.java | 2 +- .../controllers/UserControllerTest.java | 166 ++++++++++++++++++ 2 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 api/src/test/java/com/orderflow/ecommerce/controllers/UserControllerTest.java diff --git a/api/src/test/java/com/orderflow/ecommerce/auxiliar/Factory.java b/api/src/test/java/com/orderflow/ecommerce/auxiliar/Factory.java index 4568447..e77c47b 100644 --- a/api/src/test/java/com/orderflow/ecommerce/auxiliar/Factory.java +++ b/api/src/test/java/com/orderflow/ecommerce/auxiliar/Factory.java @@ -11,7 +11,7 @@ public static User createUser(){ user.setId(1L); user.setName("Bob"); user.setEmail("bob@gmail.com"); - user.setPassword("shh..secret"); + user.setPassword("Shh..1secret"); user.setTaxId("98765432101"); user.setPhone("1198765432"); user.setBirthDate(LocalDate.of(1985, 3, 20)); diff --git a/api/src/test/java/com/orderflow/ecommerce/controllers/UserControllerTest.java b/api/src/test/java/com/orderflow/ecommerce/controllers/UserControllerTest.java new file mode 100644 index 0000000..8ce1df1 --- /dev/null +++ b/api/src/test/java/com/orderflow/ecommerce/controllers/UserControllerTest.java @@ -0,0 +1,166 @@ +package com.orderflow.ecommerce.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.orderflow.ecommerce.auxiliar.Factory; +import com.orderflow.ecommerce.dtos.UserDto; +import com.orderflow.ecommerce.services.UserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.domain.PageImpl; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import java.util.List; +import java.util.NoSuchElementException; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(value = UserController.class, excludeAutoConfiguration = {SecurityAutoConfiguration.class}) +public class UserControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private UserService service; + + private long existingId, nonExistingId, dependentId; + private String existingEmail, nonExistingEmail; + private UserDto userDto; + private PageImpl page; + + @BeforeEach + void setUp() throws Exception { + userDto = Factory.createUserDto(); + existingId = userDto.id(); + nonExistingId = 2L; + existingEmail = userDto.email(); + nonExistingEmail = "inexistent@mail.com"; + + page = new PageImpl<>(List.of(userDto)); + + when(service.findAllPaged(any())).thenReturn(page); + + when(service.findById(existingId)).thenReturn(userDto); + when(service.findById(nonExistingId)).thenThrow(NoSuchElementException.class); + + when(service.findByEmail(existingEmail)).thenReturn(userDto); + when(service.findByEmail(nonExistingEmail)).thenThrow(NoSuchElementException.class); + + when(service.insert(any())).thenReturn(userDto); + + when(service.update(eq(existingId), any())).thenReturn(userDto); + when(service.update(eq(nonExistingId), any())).thenThrow(NoSuchElementException.class); + + doNothing().when(service).delete(existingId, false); + doThrow(NoSuchElementException.class).when(service).delete(nonExistingId, true); + doThrow(DataIntegrityViolationException.class).when(service).delete(dependentId, false); + } + + @Test + void findAllShouldReturnPage() throws Exception { + mockMvc.perform(get("/users").accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); + } + + @Test + public void findByIdShouldReturnUserWhenIdExists() throws Exception { + ResultActions result = mockMvc.perform(get("/users/{id}", existingId) + .accept(MediaType.APPLICATION_JSON)); + + result.andExpect(status().isOk()); + result.andExpect(jsonPath("$.id").exists()); + } + + @Test + public void findByIdShouldThrowNotFoundWhenIdDoesNotExist() throws Exception { + ResultActions result = mockMvc.perform(get("/users/{id}", nonExistingId) + .accept(MediaType.APPLICATION_JSON)); + result.andExpect(status().isNotFound()); + } + + @Test + public void findByEmailShouldReturnUserWhenIdExists() throws Exception { + ResultActions result = mockMvc.perform(get("/users").param("email", existingEmail) + .accept(MediaType.APPLICATION_JSON)); + + result.andExpect(status().isOk()); + result.andExpect(jsonPath("$.id").exists()); + } + + @Test + public void findByEmailShouldThrowNotFoundWhenIdDoesNotExist() throws Exception { + ResultActions result = mockMvc.perform(get("/users").param("email", nonExistingEmail) + .accept(MediaType.APPLICATION_JSON)); + result.andExpect(status().isNotFound()); + } + + + @Test + public void updateShouldReturnUserDTOWhenIdExists() throws Exception { + String jsonBody = objectMapper.writeValueAsString(userDto); + ResultActions result = mockMvc.perform(put("/users/{id}", existingId) + .content(jsonBody) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)); + + result.andExpect(status().isOk()); + result.andExpect(jsonPath("$.id").exists()); + result.andExpect(jsonPath("$.name").exists()); + + } + + @Test + public void updateShouldThrowNotFoundWhenIdDoesNotExist() throws Exception { + String jsonBody = objectMapper.writeValueAsString(userDto); + ResultActions result = mockMvc.perform(put("/users/{id}", nonExistingId) + .content(jsonBody) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)); + + result.andExpect(status().isNotFound()); + } + + @Test + public void insertShouldReturnCreatedAndUserDTO() throws Exception { + String jsonBody = objectMapper.writeValueAsString(userDto); + ResultActions result = mockMvc.perform(post("/users") + .content(jsonBody) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)); + + result.andExpect(status().isCreated()); + result.andExpect(jsonPath("$.id").exists()); + result.andExpect(jsonPath("$.name").exists()); + + } + + @Test + public void deleteShouldReturnNoContentWhereIdExists() throws Exception { + mockMvc.perform(delete("/users/{id}", existingId).accept(MediaType.APPLICATION_JSON)).andExpect(status().isNoContent()); + } + + @Test + public void deleteShouldReturnNotFoundWhereIdDoesNotExist() throws Exception { + mockMvc.perform(delete("/users/{id}", nonExistingId).accept(MediaType.APPLICATION_JSON)).andExpect(status().isNoContent()); + } + +} From fe62a140ba2bce0767a5b748303189300477487f Mon Sep 17 00:00:00 2001 From: Aline-Pinotti Date: Thu, 4 Jun 2026 10:15:30 -0300 Subject: [PATCH 16/16] refactor(user): improve validation exception --- .../ControllerExceptionHandler.java | 58 +++++++++++++++++++ .../controllers/exceptions/FieldMessage.java | 15 +++++ .../controllers/exceptions/StandardError.java | 19 ++++++ .../exceptions/ValidationError.java | 15 +++++ .../DuplicateResourceException.java | 8 --- .../DuplicateResourceValidationException.java | 22 +++++++ .../exceptions/GlobalExceptionHandler.java | 14 ----- .../ecommerce/services/UserService.java | 48 ++++++++++----- .../ecommerce/services/UserServiceTest.java | 10 ++-- 9 files changed, 168 insertions(+), 41 deletions(-) create mode 100644 api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/ControllerExceptionHandler.java create mode 100644 api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/FieldMessage.java create mode 100644 api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/StandardError.java create mode 100644 api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/ValidationError.java delete mode 100644 api/src/main/java/com/orderflow/ecommerce/exceptions/DuplicateResourceException.java create mode 100644 api/src/main/java/com/orderflow/ecommerce/exceptions/DuplicateResourceValidationException.java diff --git a/api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/ControllerExceptionHandler.java b/api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/ControllerExceptionHandler.java new file mode 100644 index 0000000..d2842da --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/ControllerExceptionHandler.java @@ -0,0 +1,58 @@ +package com.orderflow.ecommerce.controllers.exceptions; + +import com.orderflow.ecommerce.dtos.ErrorResponse; +import com.orderflow.ecommerce.exceptions.DuplicateResourceValidationException; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.time.Instant; + +@ControllerAdvice +public class ControllerExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity validation(MethodArgumentNotValidException ex, HttpServletRequest request) { + HttpStatus status = HttpStatus.UNPROCESSABLE_ENTITY; + ValidationError err = new ValidationError(); + err.setTimestamp(Instant.now()); + err.setStatus(status.value()); + err.setError("Validation exception"); + err.setMessage(ex.getMessage()); + err.setPath(request.getRequestURI()); + + for (FieldError f : ex.getBindingResult().getFieldErrors()) { + err.addError(f.getField(), f.getDefaultMessage()); + } + + return ResponseEntity.status(status).body(err); + } + @ExceptionHandler(DataIntegrityViolationException.class) + public ResponseEntity handleIntegrityViolation(DataIntegrityViolationException ex, HttpServletRequest request) { + HttpStatus status = HttpStatus.BAD_REQUEST; + ErrorResponse err = new ErrorResponse(Instant.now(), status.value(), ex.getMessage(), request.getRequestURI()); + return ResponseEntity.status(status).body(err); + } + + @ExceptionHandler(DuplicateResourceValidationException.class) + public ResponseEntity handleDuplicateResource(DuplicateResourceValidationException ex, HttpServletRequest request) { + HttpStatus status = HttpStatus.UNPROCESSABLE_ENTITY; + ValidationError err = new ValidationError(); + err.setTimestamp(Instant.now()); + err.setStatus(status.value()); + err.setError("Validation exception"); + err.setMessage(ex.getMessage()); + err.setPath(request.getRequestURI()); + + ex.getErrors().forEach(f -> { + err.addError(f.getFieldName(), f.getMessage()); + }); + + return ResponseEntity.status(status).body(err); + } +} diff --git a/api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/FieldMessage.java b/api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/FieldMessage.java new file mode 100644 index 0000000..991332f --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/FieldMessage.java @@ -0,0 +1,15 @@ +package com.orderflow.ecommerce.controllers.exceptions; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class FieldMessage { + private String fieldName; + private String message; +} diff --git a/api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/StandardError.java b/api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/StandardError.java new file mode 100644 index 0000000..8ab160f --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/StandardError.java @@ -0,0 +1,19 @@ +package com.orderflow.ecommerce.controllers.exceptions; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.Instant; + +@Getter +@Setter +@NoArgsConstructor +public class StandardError { + private Instant timestamp; + private Integer status; + private String error; + private String message; + private String path; + +} diff --git a/api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/ValidationError.java b/api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/ValidationError.java new file mode 100644 index 0000000..26759ef --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/controllers/exceptions/ValidationError.java @@ -0,0 +1,15 @@ +package com.orderflow.ecommerce.controllers.exceptions; + +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public class ValidationError extends StandardError{ + private final List errors = new ArrayList<>(); + + public void addError(String fieldName, String message) { + errors.add(new FieldMessage(fieldName, message)); + } +} diff --git a/api/src/main/java/com/orderflow/ecommerce/exceptions/DuplicateResourceException.java b/api/src/main/java/com/orderflow/ecommerce/exceptions/DuplicateResourceException.java deleted file mode 100644 index 889f0ca..0000000 --- a/api/src/main/java/com/orderflow/ecommerce/exceptions/DuplicateResourceException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.orderflow.ecommerce.exceptions; - -public class DuplicateResourceException extends RuntimeException { - - public DuplicateResourceException(String message) { - super(message); - } -} diff --git a/api/src/main/java/com/orderflow/ecommerce/exceptions/DuplicateResourceValidationException.java b/api/src/main/java/com/orderflow/ecommerce/exceptions/DuplicateResourceValidationException.java new file mode 100644 index 0000000..60bb6e5 --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/exceptions/DuplicateResourceValidationException.java @@ -0,0 +1,22 @@ +package com.orderflow.ecommerce.exceptions; + +import com.orderflow.ecommerce.controllers.exceptions.FieldMessage; +import lombok.Getter; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public class DuplicateResourceValidationException extends RuntimeException { + + private final List errors = new ArrayList<>(); + + public DuplicateResourceValidationException(List errors, String message) { + super(message); + this.errors.addAll(errors); + } + + public void addError(String fieldName, String message) { + errors.add(new FieldMessage(fieldName, message)); + } +} diff --git a/api/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java b/api/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java index 749eafd..0badd8f 100644 --- a/api/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java +++ b/api/src/main/java/com/orderflow/ecommerce/exceptions/GlobalExceptionHandler.java @@ -1,6 +1,5 @@ package com.orderflow.ecommerce.exceptions; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.security.access.AccessDeniedException; import com.orderflow.ecommerce.dtos.ErrorResponse; import jakarta.servlet.http.HttpServletRequest; @@ -61,17 +60,4 @@ public ResponseEntity handleAccessDenied(AccessDeniedException ex return ResponseEntity.status(HttpStatus.FORBIDDEN).body(err); } - @ExceptionHandler(DataIntegrityViolationException.class) - public ResponseEntity handleIntegrityViolation(DataIntegrityViolationException ex, HttpServletRequest request) { - HttpStatus status = HttpStatus.BAD_REQUEST; - ErrorResponse err = new ErrorResponse(Instant.now(), status.value(), ex.getMessage(), request.getRequestURI()); - return ResponseEntity.status(status).body(err); - } - - @ExceptionHandler(DuplicateResourceException.class) - public ResponseEntity handleDuplicateResource(DuplicateResourceException ex, HttpServletRequest request) { - HttpStatus status = HttpStatus.UNPROCESSABLE_ENTITY; - ErrorResponse err = new ErrorResponse(Instant.now(), status.value(), ex.getMessage(), request.getRequestURI()); - return ResponseEntity.status(status).body(err); - } } diff --git a/api/src/main/java/com/orderflow/ecommerce/services/UserService.java b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java index 393185c..f9abf1b 100644 --- a/api/src/main/java/com/orderflow/ecommerce/services/UserService.java +++ b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java @@ -1,8 +1,9 @@ package com.orderflow.ecommerce.services; +import com.orderflow.ecommerce.controllers.exceptions.FieldMessage; import com.orderflow.ecommerce.dtos.UserDto; import com.orderflow.ecommerce.entities.User; -import com.orderflow.ecommerce.exceptions.DuplicateResourceException; +import com.orderflow.ecommerce.exceptions.DuplicateResourceValidationException; import com.orderflow.ecommerce.repositories.UserRepository; import jakarta.persistence.EntityNotFoundException; import org.springframework.beans.factory.annotation.Autowired; @@ -12,6 +13,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.List; import java.util.NoSuchElementException; @Service @@ -62,22 +65,11 @@ public void delete(Long id, boolean verify) { private User saveEntity(Long id, UserDto dto) { User entity = new User(); - if(id != null){ // if updating entity = repository.getReferenceById(id); - if (repository.existsByEmailAndIdNot(dto.email(), id)) { - throw new DuplicateResourceException("Email já cadastrado para outro usuário!"); - } - if (repository.existsByTaxIdAndIdNot(dto.taxId(), id)) { - throw new DuplicateResourceException("CPF/CNPJ já cadastrado para outro usuário!"); - } + validate(id, dto.email(), dto.taxId(), "Email já cadastrado para outro usuário!", "CPF/CNPJ já cadastrado para outro usuário!"); } else { - if (repository.existsByEmail(dto.email())) { - throw new DuplicateResourceException("Email já cadastrado!"); - } - if (repository.existsByTaxId(dto.taxId())) { - throw new DuplicateResourceException("CPF/CNPJ já cadastrado!"); - } + validate(null, dto.email(), dto.taxId(), "Email já cadastrado!", "CPF/CNPJ já cadastrado!"); } entity.setName(dto.name()); @@ -100,4 +92,32 @@ private User saveEntity(Long id, UserDto dto) { return repository.save(entity); } + + private void validate(Long id, String email, String taxId, String emailMessage, String taxIdMessage) { + List errors = new ArrayList<>(); + int ok = 0; + if(id != null){ + if (repository.existsByEmailAndIdNot(email, id)) { + ok = 1; + errors.add(new FieldMessage("email", "Email já cadastrado para outro usuário!")); + } + if (repository.existsByTaxIdAndIdNot(taxId, id)) { + ok = 1; + errors.add(new FieldMessage("taxId", "CPF/CNPJ já cadastrado para outro usuário!")); + } + } else { + if (repository.existsByEmail(email)) { + ok = 1; + errors.add(new FieldMessage("email", "Email já cadastrado!")); + } + if (repository.existsByTaxId(taxId)) { + ok = 1; + errors.add(new FieldMessage("taxId", "CPF/CNPJ já cadastrado!")); + } + } + + if (ok == 1) + throw new DuplicateResourceValidationException(errors, "Duplicated information!"); + + } } diff --git a/api/src/test/java/com/orderflow/ecommerce/services/UserServiceTest.java b/api/src/test/java/com/orderflow/ecommerce/services/UserServiceTest.java index ba17810..7cd1920 100644 --- a/api/src/test/java/com/orderflow/ecommerce/services/UserServiceTest.java +++ b/api/src/test/java/com/orderflow/ecommerce/services/UserServiceTest.java @@ -3,7 +3,7 @@ import com.orderflow.ecommerce.auxiliar.Factory; import com.orderflow.ecommerce.dtos.UserDto; import com.orderflow.ecommerce.entities.User; -import com.orderflow.ecommerce.exceptions.DuplicateResourceException; +import com.orderflow.ecommerce.exceptions.DuplicateResourceValidationException; import com.orderflow.ecommerce.repositories.UserRepository; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -138,7 +138,7 @@ void insertShouldThrowDuplicateResourceExceptionWhenEmailDuplicate() { Mockito.when(repository.existsByEmail(existingUserEmail)).thenReturn(true); - Assertions.assertThrows(DuplicateResourceException.class, () -> service.insert(userDto)); + Assertions.assertThrows(DuplicateResourceValidationException.class, () -> service.insert(userDto)); Mockito.verify(repository, times(0)).save(ArgumentMatchers.any()); } @@ -147,7 +147,7 @@ void insertShouldThrowDuplicateResourceExceptionWhenEmailDuplicate() { void insertShouldThrowDuplicateResourceExceptionWhenTaxIdDuplicate() { Mockito.when(repository.existsByTaxId(existingTaxId)).thenReturn(true); - Assertions.assertThrows(DuplicateResourceException.class, () -> service.insert(userDto)); + Assertions.assertThrows(DuplicateResourceValidationException.class, () -> service.insert(userDto)); Mockito.verify(repository, times(0)).save(ArgumentMatchers.any()); } @@ -175,7 +175,7 @@ void updateShouldThrowDuplicateResourceExceptionWhenEmailUsedByAnother() { Mockito.when(repository.existsByEmailAndIdNot("someoneelse@example.com", existingId)).thenReturn(true); - Assertions.assertThrows(DuplicateResourceException.class, () -> service.update(existingId, dto)); + Assertions.assertThrows(DuplicateResourceValidationException.class, () -> service.update(existingId, dto)); Mockito.verify(repository, times(0)).save(ArgumentMatchers.any()); } @@ -191,7 +191,7 @@ void updateShouldThrowDuplicateResourceExceptionWhenTaxIdEUsedByAnother() { Mockito.when(repository.existsByEmailAndIdNot("bob@gmail.com", existingId)).thenReturn(false); Mockito.when(repository.existsByTaxIdAndIdNot("11111111111", existingId)).thenReturn(true); - Assertions.assertThrows(DuplicateResourceException.class, () -> service.update(existingId, dto)); + Assertions.assertThrows(DuplicateResourceValidationException.class, () -> service.update(existingId, dto)); Mockito.verify(repository, times(0)).save(ArgumentMatchers.any()); }