diff --git a/pom.xml b/pom.xml index 611e651b..40764dd4 100644 --- a/pom.xml +++ b/pom.xml @@ -181,6 +181,19 @@ spring-test + + org.mockito + mockito-inline + 5.2.0 + test + + + + org.mockito + mockito-core + 5.12.0 + test + diff --git a/src/main/java/org/petmarket/users/controller/ComplaintsAdminController.java b/src/main/java/org/petmarket/users/controller/ComplaintsAdminController.java new file mode 100644 index 00000000..e0a1cb4b --- /dev/null +++ b/src/main/java/org/petmarket/users/controller/ComplaintsAdminController.java @@ -0,0 +1,98 @@ +package org.petmarket.users.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.petmarket.users.dto.ComplaintResponseDto; +import org.petmarket.users.entity.ComplaintStatus; +import org.petmarket.users.service.ComplaintService; +import org.petmarket.utils.annotations.parametrs.ParameterPageNumber; +import org.petmarket.utils.annotations.parametrs.ParameterPageSize; +import org.petmarket.utils.annotations.responses.ApiResponseForbidden; +import org.petmarket.utils.annotations.responses.ApiResponseNotFound; +import org.petmarket.utils.annotations.responses.ApiResponseSuccessful; +import org.petmarket.utils.annotations.responses.ApiResponseUnauthorized; +import org.springframework.data.domain.Sort; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Tag(name = "Complaints", description = "the user complaints API") +@Slf4j +@RequiredArgsConstructor +@RestController +@Validated +@RequestMapping(value = "/v1/admin/complaints") +public class ComplaintsAdminController { + private final ComplaintService complaintService; + + @Operation(summary = "Delete complaint") + @ApiResponseSuccessful + @ApiResponseUnauthorized + @ApiResponseForbidden + @PreAuthorize("isAuthenticated()") + @DeleteMapping("/{id}") + public void deleteComplaint(@PathVariable Long id) { + log.info("Deleting complaint"); + complaintService.deleteComplaint(id); + } + + @Operation(summary = "Get complaint") + @ApiResponseSuccessful + @ApiResponseNotFound + @ApiResponseUnauthorized + @ApiResponseForbidden + @PreAuthorize("isAuthenticated()") + @GetMapping("/{id}") + public ComplaintResponseDto getComplaint(@PathVariable Long id) { + log.info("Getting complaint"); + return complaintService.getComplaint(id); + } + + @Operation(summary = "Get complaints") + @ApiResponseSuccessful + @ApiResponseUnauthorized + @ApiResponseForbidden + @PreAuthorize("isAuthenticated()") + @GetMapping + public List getComplaints( + @RequestParam(required = false, defaultValue = "PENDING") ComplaintStatus complaintStatus, + @ParameterPageNumber @RequestParam(defaultValue = "1") @Positive int page, + @ParameterPageSize @RequestParam(defaultValue = "30") @Positive int size, + @RequestParam(defaultValue = "DESC") Sort.Direction direction) { + log.info("Getting complaints"); + return complaintService.getComplaints(complaintStatus, size, page, direction); + } + + @Operation(summary = "Get complaints by user id") + @ApiResponseSuccessful + @ApiResponseUnauthorized + @ApiResponseForbidden + @PreAuthorize("isAuthenticated()") + @GetMapping("/user/{userId}") + public List getComplaintsByUserId( + @PathVariable Long userId, + @RequestParam(required = false, defaultValue = "PENDING") ComplaintStatus status, + @ParameterPageNumber @RequestParam(defaultValue = "1") @Positive int page, + @ParameterPageSize @RequestParam(defaultValue = "30") @Positive int size, + @RequestParam(defaultValue = "DESC") Sort.Direction direction) { + log.info("Getting complaints by user id"); + return complaintService.getComplaintsByUserId(userId, status, size, page, direction); + } + + @Operation(summary = "Update complaint status") + @ApiResponseSuccessful + @ApiResponseUnauthorized + @ApiResponseForbidden + @PreAuthorize("isAuthenticated()") + @PutMapping("/{id}") + public void updateComplaintStatus(@PathVariable Long id, + @RequestParam(defaultValue = "RESOLVED") ComplaintStatus status) { + log.info("Updating complaint status"); + complaintService.updateStatusById(id, status); + } +} diff --git a/src/main/java/org/petmarket/users/controller/ComplaintsController.java b/src/main/java/org/petmarket/users/controller/ComplaintsController.java index 287906b2..87027086 100644 --- a/src/main/java/org/petmarket/users/controller/ComplaintsController.java +++ b/src/main/java/org/petmarket/users/controller/ComplaintsController.java @@ -1,17 +1,36 @@ package org.petmarket.users.controller; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.petmarket.users.dto.ComplaintRequestDto; +import org.petmarket.users.service.ComplaintService; +import org.petmarket.utils.annotations.responses.ApiResponseBadRequest; +import org.petmarket.utils.annotations.responses.ApiResponseSuccessful; +import org.petmarket.utils.annotations.responses.ApiResponseUnauthorized; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -@Tag(name = "Users", description = "the users API") +@Tag(name = "Complaints", description = "the user complaints API") @Slf4j @RequiredArgsConstructor @RestController @Validated @RequestMapping(value = "/v1/complaints") public class ComplaintsController { + private final ComplaintService complaintService; + + @Operation(summary = "Add complaint") + @ApiResponseSuccessful + @ApiResponseUnauthorized + @ApiResponseBadRequest + @PreAuthorize("isAuthenticated()") + @PostMapping + public void addComplaint(@RequestBody @Valid ComplaintRequestDto complaintRequestDto) { + log.info("Adding complaint"); + complaintService.addComplaint(complaintRequestDto); + } } diff --git a/src/main/java/org/petmarket/users/dto/ComplaintRequestDto.java b/src/main/java/org/petmarket/users/dto/ComplaintRequestDto.java index 20544837..98e3948e 100644 --- a/src/main/java/org/petmarket/users/dto/ComplaintRequestDto.java +++ b/src/main/java/org/petmarket/users/dto/ComplaintRequestDto.java @@ -1,13 +1,25 @@ package org.petmarket.users.dto; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.*; +import org.springframework.validation.annotation.Validated; @Setter @Getter @Builder @NoArgsConstructor @AllArgsConstructor +@Validated public class ComplaintRequestDto { + @NotBlank(message = "The 'text' cannot be empty") + @Schema(example = "Hello, how are you?") + @Size(max = 10000, message = "The 'text' length must be less than or equal to 10000") private String complaint; + + @Schema(example = "1") + @JsonProperty("complained_user_id") private Long complainedUserId; } diff --git a/src/main/java/org/petmarket/users/dto/ComplaintResponseDto.java b/src/main/java/org/petmarket/users/dto/ComplaintResponseDto.java new file mode 100644 index 00000000..4a26e42e --- /dev/null +++ b/src/main/java/org/petmarket/users/dto/ComplaintResponseDto.java @@ -0,0 +1,20 @@ +package org.petmarket.users.dto; + +import lombok.*; + +import java.util.Date; + +@Setter +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ComplaintResponseDto { + private Long id; + private String complaint; + private Long userId; + private Long complainedUserId; + private String status; + private Date created; + private Date updated; +} diff --git a/src/main/java/org/petmarket/users/mapper/ComplaintMapper.java b/src/main/java/org/petmarket/users/mapper/ComplaintMapper.java index 4eac2d78..556b64e5 100644 --- a/src/main/java/org/petmarket/users/mapper/ComplaintMapper.java +++ b/src/main/java/org/petmarket/users/mapper/ComplaintMapper.java @@ -1,11 +1,21 @@ package org.petmarket.users.mapper; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import org.petmarket.config.MapperConfig; import org.petmarket.users.dto.ComplaintRequestDto; +import org.petmarket.users.dto.ComplaintResponseDto; import org.petmarket.users.entity.Complaint; +import java.util.List; + @Mapper(config = MapperConfig.class) public interface ComplaintMapper { Complaint mapDtoToComplaint(ComplaintRequestDto complaintRequestDto); + + @Mapping(target = "userId", source = "user.id") + @Mapping(target = "complainedUserId", source = "complainedUser.id") + ComplaintResponseDto mapComplaintToDto(Complaint complaint); + + List mapComplaintToDto(List complaints); } diff --git a/src/main/java/org/petmarket/users/repository/ComplaintRepository.java b/src/main/java/org/petmarket/users/repository/ComplaintRepository.java index 6c5da6b6..f3c6f312 100644 --- a/src/main/java/org/petmarket/users/repository/ComplaintRepository.java +++ b/src/main/java/org/petmarket/users/repository/ComplaintRepository.java @@ -5,6 +5,9 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -12,4 +15,8 @@ public interface ComplaintRepository extends JpaRepository { Page findAllByComplainedUserIdAndStatus(Long userId, ComplaintStatus complaintStatus, Pageable pageable); Page findAllByStatus(ComplaintStatus complaintStatus, Pageable pageable); + + @Modifying + @Query("UPDATE Complaint c SET c.status = :status WHERE c.id = :id") + void updateStatusById(@Param("id") Long id, @Param("status") ComplaintStatus status); } diff --git a/src/main/java/org/petmarket/users/service/ComplaintService.java b/src/main/java/org/petmarket/users/service/ComplaintService.java index 71dbef5a..0b7e22a3 100644 --- a/src/main/java/org/petmarket/users/service/ComplaintService.java +++ b/src/main/java/org/petmarket/users/service/ComplaintService.java @@ -1,17 +1,20 @@ package org.petmarket.users.service; +import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.petmarket.users.dto.ComplaintRequestDto; +import org.petmarket.users.dto.ComplaintResponseDto; import org.petmarket.users.entity.Complaint; import org.petmarket.users.entity.ComplaintStatus; import org.petmarket.users.mapper.ComplaintMapper; import org.petmarket.users.repository.ComplaintRepository; -import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; +import java.util.List; + @Slf4j @RequiredArgsConstructor @Service @@ -20,39 +23,43 @@ public class ComplaintService { private final ComplaintMapper complaintMapper; private final UserService userService; + @Transactional public void addComplaint(ComplaintRequestDto complaintRequestDto) { if (complaintRequestDto.getComplainedUserId().equals(UserService.getCurrentUserId())) { log.error("User cannot complain about himself"); throw new IllegalArgumentException("User cannot complain about himself"); } - log.info("Adding complaint: {}", complaintRequestDto); Complaint complaint = complaintMapper.mapDtoToComplaint(complaintRequestDto); complaint.setUser(userService.getCurrentUser()); + complaint.setComplainedUser(userService.findById(complaintRequestDto.getComplainedUserId())); + complaint.setStatus(ComplaintStatus.PENDING); complaintRepository.save(complaint); } public void deleteComplaint(Long id) { - log.info("Deleting complaint with id: {}", id); complaintRepository.deleteById(id); } - public Complaint getComplaint(Long id) { - log.info("Getting complaint with id: {}", id); - return complaintRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("Complaint not found")); + public ComplaintResponseDto getComplaint(Long id) { + return complaintMapper.mapComplaintToDto(complaintRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("Complaint not found"))); } - public Page getComplaints(ComplaintStatus complaintStatus, int size, - int page, Sort.Direction direction) { - log.info("Getting all complaints"); - return complaintRepository.findAllByStatus(complaintStatus, - PageRequest.of(page - 1, size, Sort.by(direction, "created"))); + public List getComplaints(ComplaintStatus complaintStatus, int size, + int page, Sort.Direction direction) { + return complaintMapper.mapComplaintToDto(complaintRepository.findAllByStatus(complaintStatus, + PageRequest.of(page - 1, size, Sort.by(direction, "created"))).toList()); } - public Page getComplaintsByUserId(Long userId, ComplaintStatus status, + public List getComplaintsByUserId(Long userId, ComplaintStatus status, int size, int page, Sort.Direction direction) { - log.info("Getting all complaints by user id: {}", userId); - return complaintRepository.findAllByComplainedUserIdAndStatus(userId, status, PageRequest - .of(page - 1, size, Sort.by(direction, "created"))); + return complaintMapper.mapComplaintToDto(complaintRepository.findAllByComplainedUserIdAndStatus(userId, + status, PageRequest.of(page - 1, size, Sort.by(direction, "created"))).toList()); + } + + @Transactional + public void updateStatusById(Long id, ComplaintStatus status) { + complaintRepository.updateStatusById(id, status); } } diff --git a/src/test/java/org/petmarket/users/service/ComplaintServiceTest.java b/src/test/java/org/petmarket/users/service/ComplaintServiceTest.java new file mode 100644 index 00000000..db05b320 --- /dev/null +++ b/src/test/java/org/petmarket/users/service/ComplaintServiceTest.java @@ -0,0 +1,182 @@ +package org.petmarket.users.service; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.*; +import org.mockito.junit.jupiter.MockitoExtension; +import org.petmarket.users.dto.ComplaintRequestDto; +import org.petmarket.users.dto.ComplaintResponseDto; +import org.petmarket.users.entity.Complaint; +import org.petmarket.users.entity.ComplaintStatus; +import org.petmarket.users.entity.User; +import org.petmarket.users.mapper.ComplaintMapper; +import org.petmarket.users.repository.ComplaintRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@ExtendWith(MockitoExtension.class) +class ComplaintServiceTest { + @Mock + private ComplaintRepository complaintRepository; + + @Mock + private ComplaintMapper complaintMapper; + + @Mock + private UserService userService; + + @InjectMocks + private ComplaintService complaintService; + + @Test + public void testAddComplaintSelfComplaintThrowsException() { + try (MockedStatic mockedUserService = mockStatic(UserService.class)) { + // Arrange + ComplaintRequestDto dto = new ComplaintRequestDto(); + dto.setComplainedUserId(1L); + + mockedUserService.when(UserService::getCurrentUserId).thenReturn(1L); + + // Act + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> complaintService.addComplaint(dto)); + + // Assert + assertEquals("User cannot complain about himself", exception.getMessage()); + } + } + + @Test + public void testAddComplaintSuccess() { + try (MockedStatic mockedUserService = mockStatic(UserService.class)) { + // Arrange + ComplaintRequestDto dto = new ComplaintRequestDto(); + dto.setComplainedUserId(2L); + + Complaint complaint = new Complaint(); + mockedUserService.when(UserService::getCurrentUserId).thenReturn(1L); + when(userService.getCurrentUser()).thenReturn(new User()); + when(complaintMapper.mapDtoToComplaint(dto)).thenReturn(complaint); + + // Act + complaintService.addComplaint(dto); + + // Assert + verify(complaintRepository, times(1)).save(complaint); + } + } + + @Test + public void testDeleteComplaintSuccess() { + // Arrange + Long complaintId = 1L; + + // Act + complaintService.deleteComplaint(complaintId); + + // Assert + verify(complaintRepository, times(1)).deleteById(complaintId); + } + + @Test + public void testGetComplaintNotFound() { + // Arrange + Long complaintId = 1L; + when(complaintRepository.findById(complaintId)).thenReturn(Optional.empty()); + + // Act + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, + () -> complaintService.getComplaint(complaintId)); + + // Assert + assertEquals("Complaint not found", exception.getMessage()); + } + + @Test + public void testGetComplaintSuccess() { + // Arrange + Long complaintId = 1L; + Complaint complaint = new Complaint(); + ComplaintResponseDto complaintResponseDto = new ComplaintResponseDto(); + + when(complaintRepository.findById(complaintId)).thenReturn(Optional.of(complaint)); + when(complaintMapper.mapComplaintToDto(complaint)).thenReturn(complaintResponseDto); + + // Act + ComplaintResponseDto result = complaintService.getComplaint(complaintId); + + // Assert + assertEquals(complaintResponseDto, result); + } + + @Test + public void testGetComplaintsSuccess() { + // Arrange + ComplaintStatus status = ComplaintStatus.PENDING; + int page = 1; + int size = 10; + Sort.Direction direction = Sort.Direction.ASC; + + Page complaintPage = new PageImpl<>(Collections.emptyList()); + List complaintList = complaintPage.getContent(); + List complaintResponseDtoList = Collections.emptyList(); + + when(complaintRepository.findAllByStatus(status, + PageRequest.of(0, size, Sort.by(direction, "created")))).thenReturn(complaintPage); + when(complaintMapper.mapComplaintToDto(complaintList)).thenReturn(complaintResponseDtoList); + + // Act + List result = complaintService.getComplaints(status, size, page, direction); + + // Assert + assertEquals(complaintResponseDtoList, result); + } + + @Test + public void testGetComplaintsByUserIdSuccess() { + // Arrange + Long userId = 1L; + ComplaintStatus status = ComplaintStatus.PENDING; + int page = 1; + int size = 10; + Sort.Direction direction = Sort.Direction.ASC; + + Page complaintPage = new PageImpl<>(Collections.emptyList()); + List complaintList = complaintPage.getContent(); + List complaintResponseDtoList = Collections.emptyList(); + + when(complaintRepository.findAllByComplainedUserIdAndStatus(userId, status, + PageRequest.of(0, size, Sort.by(direction, "created")))) + .thenReturn(complaintPage); + when(complaintMapper.mapComplaintToDto(complaintList)).thenReturn(complaintResponseDtoList); + + // Act + List result = complaintService + .getComplaintsByUserId(userId, status, size, page, direction); + + // Assert + assertEquals(complaintResponseDtoList, result); + } + + @Test + public void testUpdateStatusById_Success() { + // Arrange + Long complaintId = 1L; + ComplaintStatus status = ComplaintStatus.RESOLVED; + + // Act + complaintService.updateStatusById(complaintId, status); + + // Assert + verify(complaintRepository, times(1)).updateStatusById(complaintId, status); + } +}