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..11e293f --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/controllers/UserController.java @@ -0,0 +1,57 @@ +package com.orderflow.ecommerce.controllers; + +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; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +import java.net.URI; + +@RestController +@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(@RequestParam String email) { + return ResponseEntity.ok().body(service.findByEmail(email)); + } + + @Valid + @PostMapping + public ResponseEntity insert(@Valid @RequestBody UserDto dto) { + dto = service.insert(dto); + URI uri = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}") + .buildAndExpand(dto.id()).toUri(); + return ResponseEntity.created(uri).body(dto); + } + + @DeleteMapping(value = "/{id}") + public ResponseEntity delete(@PathVariable Long id, @RequestParam(required = false) boolean verify) { + service.delete(id, verify); + return ResponseEntity.noContent().build(); + } + + @PutMapping(value = "/{id}") + 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/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/dtos/UserDto.java b/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java new file mode 100644 index 0000000..d912469 --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/dtos/UserDto.java @@ -0,0 +1,52 @@ +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; + +public record UserDto( + Long id, + @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, + String phone, + 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) { + 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/entities/User.java b/api/src/main/java/com/orderflow/ecommerce/entities/User.java new file mode 100644 index 0000000..d47dd20 --- /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 +@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; + + /** + * State registration (IE) + */ + @Column(length = 30) + private String stateRegistration; + + private String phone; + private LocalDate birthDate; + + /** + * Used in Invoices (NF-e) + */ + private Boolean taxpayer; + + /** + * 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; + +} 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/repositories/UserRepository.java b/api/src/main/java/com/orderflow/ecommerce/repositories/UserRepository.java new file mode 100644 index 0000000..2185dd4 --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/repositories/UserRepository.java @@ -0,0 +1,17 @@ +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); + + 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 new file mode 100644 index 0000000..f9abf1b --- /dev/null +++ b/api/src/main/java/com/orderflow/ecommerce/services/UserService.java @@ -0,0 +1,123 @@ +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.DuplicateResourceValidationException; +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.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +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); + } + + @Transactional + public UserDto insert(UserDto dto) { + return new UserDto(saveEntity(null, 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); + } + } + + @Transactional + 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) { + throw new DataIntegrityViolationException("Integrity violation"); + } + } + + private User saveEntity(Long id, UserDto dto) { + User entity = new User(); + if(id != null){ // if updating + entity = repository.getReferenceById(id); + validate(id, dto.email(), dto.taxId(), "Email já cadastrado para outro usuário!", "CPF/CNPJ já cadastrado para outro usuário!"); + } else { + validate(null, dto.email(), dto.taxId(), "Email já cadastrado!", "CPF/CNPJ já cadastrado!"); + } + + 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); + } + + 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/auxiliar/Factory.java b/api/src/test/java/com/orderflow/ecommerce/auxiliar/Factory.java new file mode 100644 index 0000000..e77c47b --- /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..1secret"); + 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/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()); + } + +} 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()); + } + +} 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..7cd1920 --- /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.DuplicateResourceValidationException; +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(DuplicateResourceValidationException.class, () -> service.insert(userDto)); + + Mockito.verify(repository, times(0)).save(ArgumentMatchers.any()); + } + + @Test + void insertShouldThrowDuplicateResourceExceptionWhenTaxIdDuplicate() { + Mockito.when(repository.existsByTaxId(existingTaxId)).thenReturn(true); + + Assertions.assertThrows(DuplicateResourceValidationException.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(DuplicateResourceValidationException.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(DuplicateResourceValidationException.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 + +}