diff --git a/src/main/java/com/gamzabat/algohub/enums/JoinRequestStatus.java b/src/main/java/com/gamzabat/algohub/enums/JoinRequestStatus.java new file mode 100644 index 00000000..3c34eba1 --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/enums/JoinRequestStatus.java @@ -0,0 +1,17 @@ +package com.gamzabat.algohub.enums; + +public enum JoinRequestStatus { + PENDING("pending"), + APPROVE("approve"), + CANCEL("cancel"), + REJECT("reject"); + private String value; + + private JoinRequestStatus(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/com/gamzabat/algohub/exception/CustomExceptionHandler.java b/src/main/java/com/gamzabat/algohub/exception/CustomExceptionHandler.java index 5ff9adca..8b028fcd 100644 --- a/src/main/java/com/gamzabat/algohub/exception/CustomExceptionHandler.java +++ b/src/main/java/com/gamzabat/algohub/exception/CustomExceptionHandler.java @@ -17,6 +17,7 @@ import com.gamzabat.algohub.feature.group.studygroup.exception.CannotFoundUserException; import com.gamzabat.algohub.feature.group.studygroup.exception.GroupMemberValidationException; import com.gamzabat.algohub.feature.group.studygroup.exception.InvalidRoleException; +import com.gamzabat.algohub.feature.group.studygroup.exception.JoinRequestException; import com.gamzabat.algohub.feature.image.exception.AwsS3Exception; import com.gamzabat.algohub.feature.notice.exception.NoticeValidationException; import com.gamzabat.algohub.feature.notification.exception.CannotFoundNotificationException; @@ -233,7 +234,7 @@ protected ResponseEntity handleCannotFoundVerificationCodeExcepti .status(HttpStatus.BAD_REQUEST) .body(new ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage(), null)); } - + @ExceptionHandler(CannotFoundEdgeCaseException.class) protected ResponseEntity handleCannotFoundEdgeCaseException( CannotFoundEdgeCaseException e) { @@ -249,5 +250,12 @@ protected ResponseEntity handleNotAuthorizedUserException( .status(e.getHttpStatus()) .body(new ErrorResponse(e.getHttpStatus().value(), e.getError(), null)); } - + + @ExceptionHandler(JoinRequestException.class) + protected ResponseEntity handleCannotFoundVerificationCodeException( + JoinRequestException e) { + return ResponseEntity + .status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse(HttpStatus.BAD_REQUEST.value(), e.getMessage(), null)); + } } diff --git a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/controller/JoinRequestController.java b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/controller/JoinRequestController.java new file mode 100644 index 00000000..355df0d7 --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/controller/JoinRequestController.java @@ -0,0 +1,60 @@ +package com.gamzabat.algohub.feature.group.studygroup.controller; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.gamzabat.algohub.common.annotation.AuthedUser; +import com.gamzabat.algohub.feature.group.studygroup.domain.JoinRequest; +import com.gamzabat.algohub.feature.group.studygroup.dto.UpdateJoinRequestStatusRequest; +import com.gamzabat.algohub.feature.group.studygroup.exception.JoinRequestException; +import com.gamzabat.algohub.feature.group.studygroup.service.JoinRequestService; +import com.gamzabat.algohub.feature.user.domain.User; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +@Tag(name = "그룹 가입 요청API", description = "스터디 그룹 가입 요청 관련 API") +public class JoinRequestController { + private final JoinRequestService joinRequestService; + + @PostMapping(value = "/groups/{groupId}/join-request") + @Operation(summary = "그룹 가입 요청 API", description = "스터디 그룹에 가입 요청을 보내는 API") + public ResponseEntity joinRequest(@AuthedUser User user, @PathVariable Long groupId) { + joinRequestService.joinRequest(user, groupId); + return ResponseEntity.ok().build(); + } + + @GetMapping(value = "/groups/{groupId}/join-request") + @Operation(summary = "그룹 가입 요청 목록 조회 API", description = "스터디 그룹 가입 요청 목록을 조회하는 API") + public ResponseEntity> getAllJoinRequests(@AuthedUser User user, @PathVariable Long groupId) { + List response = joinRequestService.getAllJoinRequests(user, groupId); + + return ResponseEntity.ok().body(response); + } + + @PostMapping(value = "/join-request/{requestId}") + @Operation(summary = "그룹 가입 요청 승인 / 거절", description = "스터디 그룹 가입 요청을 승인 / 거절하는 API") + public ResponseEntity updateRequest( + @AuthedUser User user, + @PathVariable Long requestId, + @RequestBody @Valid UpdateJoinRequestStatusRequest request, Errors errors) { + if (errors.hasErrors()) + throw new JoinRequestException("가입 요청이 올바르지 않습니다."); + joinRequestService.updateJoinRequest(user, requestId, request); + return ResponseEntity.ok().build(); + } + +} diff --git a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/controller/StudyGroupController.java b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/controller/StudyGroupController.java index f054d9d0..ec97c5da 100644 --- a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/controller/StudyGroupController.java +++ b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/controller/StudyGroupController.java @@ -208,4 +208,5 @@ public ResponseEntity> getSearchedGroupList(@RequestParam Page responses = studyGroupService.getSearchedStudyGroupList(searchPattern, pageable); return ResponseEntity.ok().body(responses); } + } diff --git a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/domain/JoinRequest.java b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/domain/JoinRequest.java new file mode 100644 index 00000000..ed5b7c3d --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/domain/JoinRequest.java @@ -0,0 +1,50 @@ +package com.gamzabat.algohub.feature.group.studygroup.domain; + +import com.gamzabat.algohub.enums.JoinRequestStatus; +import com.gamzabat.algohub.feature.user.domain.User; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class JoinRequest { + @Id + @GeneratedValue + Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "group_id") + private StudyGroup group; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User requester; + + @Enumerated(EnumType.STRING) + private JoinRequestStatus status = JoinRequestStatus.PENDING; + + public JoinRequest(StudyGroup group, User requester) { + this.group = group; + this.requester = requester; + } + + public void updateStatus(JoinRequestStatus status) { + if (status != JoinRequestStatus.PENDING) { + return; + } + this.status = status; + } + +} + + diff --git a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/dto/UpdateJoinRequestStatusRequest.java b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/dto/UpdateJoinRequestStatusRequest.java new file mode 100644 index 00000000..71920e38 --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/dto/UpdateJoinRequestStatusRequest.java @@ -0,0 +1,8 @@ +package com.gamzabat.algohub.feature.group.studygroup.dto; + +import com.gamzabat.algohub.enums.JoinRequestStatus; + +import jakarta.validation.constraints.NotNull; + +public record UpdateJoinRequestStatusRequest(@NotNull(message = "status 는 필수입니다.") JoinRequestStatus status) { +} diff --git a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/exception/JoinRequestException.java b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/exception/JoinRequestException.java new file mode 100644 index 00000000..da3a990e --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/exception/JoinRequestException.java @@ -0,0 +1,7 @@ +package com.gamzabat.algohub.feature.group.studygroup.exception; + +public class JoinRequestException extends RuntimeException { + public JoinRequestException(String error) { + super(error); + } +} diff --git a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/repository/JoinRequestRepository.java b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/repository/JoinRequestRepository.java new file mode 100644 index 00000000..da0b4b52 --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/repository/JoinRequestRepository.java @@ -0,0 +1,14 @@ +package com.gamzabat.algohub.feature.group.studygroup.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.gamzabat.algohub.feature.group.studygroup.domain.JoinRequest; + +public interface JoinRequestRepository extends JpaRepository { + + boolean existsByGroup_IdAndRequester_Id(Long groupId, Long userId); + + List findAllByGroup_Id(Long groupId); +} diff --git a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/service/JoinRequestService.java b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/service/JoinRequestService.java new file mode 100644 index 00000000..9056c907 --- /dev/null +++ b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/service/JoinRequestService.java @@ -0,0 +1,111 @@ +package com.gamzabat.algohub.feature.group.studygroup.service; + +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.gamzabat.algohub.enums.JoinRequestStatus; +import com.gamzabat.algohub.exception.StudyGroupValidationException; +import com.gamzabat.algohub.feature.group.ranking.domain.Ranking; +import com.gamzabat.algohub.feature.group.ranking.repository.RankingRepository; +import com.gamzabat.algohub.feature.group.studygroup.domain.GroupMember; +import com.gamzabat.algohub.feature.group.studygroup.domain.JoinRequest; +import com.gamzabat.algohub.feature.group.studygroup.domain.StudyGroup; +import com.gamzabat.algohub.feature.group.studygroup.dto.UpdateJoinRequestStatusRequest; +import com.gamzabat.algohub.feature.group.studygroup.etc.RoleOfGroupMember; +import com.gamzabat.algohub.feature.group.studygroup.exception.GroupMemberValidationException; +import com.gamzabat.algohub.feature.group.studygroup.exception.JoinRequestException; +import com.gamzabat.algohub.feature.group.studygroup.repository.GroupMemberRepository; +import com.gamzabat.algohub.feature.group.studygroup.repository.JoinRequestRepository; +import com.gamzabat.algohub.feature.group.studygroup.repository.StudyGroupRepository; +import com.gamzabat.algohub.feature.notification.domain.NotificationSetting; +import com.gamzabat.algohub.feature.notification.repository.NotificationSettingRepository; +import com.gamzabat.algohub.feature.user.domain.User; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class JoinRequestService { + private final StudyGroupRepository studyGroupRepository; + private final GroupMemberRepository groupMemberRepository; + private final JoinRequestRepository joinRequestRepository; + private final NotificationSettingRepository notificationSettingRepository; + private final RankingRepository rankingRepository; + private final StudyGroupService studyGroupService; + + @Transactional + public void joinRequest(User user, Long groupId) { + StudyGroup studyGroup = studyGroupRepository.findById(groupId) + .orElseThrow(() -> new StudyGroupValidationException(HttpStatus.NOT_FOUND.value(), "존재하지 않는 그룹 입니다.")); + if (groupMemberRepository.existsByUserAndStudyGroup(user, studyGroup)) { + throw new GroupMemberValidationException(HttpStatus.BAD_REQUEST.value(), "이미 가입한 그룹입니다"); + } + if (joinRequestRepository.existsByGroup_IdAndRequester_Id(groupId, user.getId())) { + throw new JoinRequestException("이미 요청한 그룹입니다."); + } + + JoinRequest request = new JoinRequest(studyGroup, user); + joinRequestRepository.save(request); + log.info("success to join request group = {}", groupId); + } + + @Transactional(readOnly = true) + public List getAllJoinRequests(User user, Long groupId) { + StudyGroup studyGroup = studyGroupRepository.findById(groupId) + .orElseThrow(() -> new StudyGroupValidationException(HttpStatus.NOT_FOUND.value(), "존재하지 않는 그룹 입니다.")); + Optional groupMember = groupMemberRepository.findByUserAndStudyGroup(user, studyGroup); + if (groupMember.isPresent() && RoleOfGroupMember.isParticipant(groupMember.get()) || groupMember.isEmpty()) { + throw new JoinRequestException("요청 목록을 조회할 권한이 없습니다."); + } + return joinRequestRepository.findAllByGroup_Id(groupId); + } + + @Transactional + public void updateJoinRequest(User user, Long requestId, UpdateJoinRequestStatusRequest request) { + JoinRequest joinRequest = joinRequestRepository.findById(requestId) + .orElseThrow(() -> new JoinRequestException("해당 요청이 존재하지 않습니다")); + StudyGroup studyGroup = studyGroupRepository.findById(joinRequest.getGroup().getId()) + .orElseThrow(() -> new StudyGroupValidationException(HttpStatus.NOT_FOUND.value(), "존재하지 않는 그룹입니다.")); + GroupMember groupMember = groupMemberRepository.findByUserAndStudyGroup(user, studyGroup) + .orElseThrow(() -> new GroupMemberValidationException(HttpStatus.NOT_FOUND.value(), "해당 그룹의 멤버가 아닙니다.")); + if (RoleOfGroupMember.isParticipant(groupMember)) { + throw new JoinRequestException("승인 권한이 없습니다."); + } + if (request.status() == JoinRequestStatus.APPROVE) { + joinRequest.updateStatus(request.status()); + GroupMember newGroupMember = GroupMember.builder() + .user(joinRequest.getRequester()) + .studyGroup(studyGroup) + .joinDate(LocalDate.now()) + .role(RoleOfGroupMember.PARTICIPANT) + .build(); + groupMemberRepository.save(newGroupMember); + + notificationSettingRepository.save( + NotificationSetting.builder().member(newGroupMember).build() + ); + + rankingRepository.save(Ranking.builder() + .member(newGroupMember) + .currentRank(groupMemberRepository.countByStudyGroup(studyGroup)) + .solvedCount(0) + .rankDiff("-") + .build() + ); + studyGroupService.sendNewMemberNotification(studyGroup, newGroupMember); + + joinRequestRepository.delete(joinRequest); + } else if (request.status() == JoinRequestStatus.REJECT) { + joinRequestRepository.delete(joinRequest); + } + log.info("success to approve/reject for join request group = {}", studyGroup.getId()); + } + +} diff --git a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/service/StudyGroupService.java b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/service/StudyGroupService.java index 0460ce3d..fa352f5d 100644 --- a/src/main/java/com/gamzabat/algohub/feature/group/studygroup/service/StudyGroupService.java +++ b/src/main/java/com/gamzabat/algohub/feature/group/studygroup/service/StudyGroupService.java @@ -52,6 +52,7 @@ import com.gamzabat.algohub.feature.group.studygroup.exception.GroupMemberValidationException; import com.gamzabat.algohub.feature.group.studygroup.repository.BookmarkedStudyGroupRepository; import com.gamzabat.algohub.feature.group.studygroup.repository.GroupMemberRepository; +import com.gamzabat.algohub.feature.group.studygroup.repository.JoinRequestRepository; import com.gamzabat.algohub.feature.group.studygroup.repository.StudyGroupRepository; import com.gamzabat.algohub.feature.image.service.ImageService; import com.gamzabat.algohub.feature.notice.repository.NoticeCommentRepository; @@ -87,6 +88,7 @@ public class StudyGroupService { private final RankingRepository rankingRepository; private final NoticeRepository noticeRepository; private final NoticeCommentRepository noticeCommentRepository; + private final JoinRequestRepository joinRequestRepository; private final SolutionCommentRepository solutionCommentRepository; private final NoticeReadRepository noticeReadRepository; private final ObjectProvider studyGroupServiceProvider; @@ -609,7 +611,7 @@ private boolean isVisible(StudyGroup group, User user) { return groupMemberRepository.existsByUserAndStudyGroupAndIsVisible(user, group, true); } - private void sendNewMemberNotification(StudyGroup studyGroup, GroupMember newMember) { + void sendNewMemberNotification(StudyGroup studyGroup, GroupMember newMember) { List members = groupMemberRepository.findAllByStudyGroup(studyGroup) .stream() .filter(member -> !member.getId().equals(newMember.getId())) @@ -657,4 +659,4 @@ public Page getSearchedStudyGroupList(String searchPattern, Pa )); } -} +} \ No newline at end of file diff --git a/src/test/java/com/gamzabat/algohub/feature/joinRequest/JoinRequestControllerTest.java b/src/test/java/com/gamzabat/algohub/feature/joinRequest/JoinRequestControllerTest.java new file mode 100644 index 00000000..7a247fd4 --- /dev/null +++ b/src/test/java/com/gamzabat/algohub/feature/joinRequest/JoinRequestControllerTest.java @@ -0,0 +1,170 @@ +package com.gamzabat.algohub.feature.joinRequest; + +import static org.hamcrest.Matchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.gamzabat.algohub.common.jwt.TokenProvider; +import com.gamzabat.algohub.config.SpringSecurityConfig; +import com.gamzabat.algohub.enums.JoinRequestStatus; +import com.gamzabat.algohub.feature.group.studygroup.controller.JoinRequestController; +import com.gamzabat.algohub.feature.group.studygroup.domain.JoinRequest; +import com.gamzabat.algohub.feature.group.studygroup.dto.UpdateJoinRequestStatusRequest; +import com.gamzabat.algohub.feature.group.studygroup.exception.JoinRequestException; +import com.gamzabat.algohub.feature.group.studygroup.service.JoinRequestService; +import com.gamzabat.algohub.feature.user.domain.User; +import com.gamzabat.algohub.feature.user.repository.UserRepository; + +@WebMvcTest(JoinRequestController.class) +@WithMockUser +@Import(SpringSecurityConfig.class) +class JoinRequestControllerTest { + private final String token = "token"; + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private JoinRequestService joinRequestService; + @MockBean + private TokenProvider tokenProvider; + @MockBean + private UserRepository userRepository; + private User user; + + @BeforeEach + void setUp() { + user = User.builder().email("email").password("password").build(); + when(tokenProvider.getUserEmail(token)).thenReturn("email"); + when(userRepository.findByEmail("email")).thenReturn(Optional.ofNullable(user)); + } + + @Test + @DisplayName("가입 요청 성공") + void joinRequestSuccess() throws Exception { + //given + Long groupId = 10L; + willDoNothing().given(joinRequestService).joinRequest(any(User.class), eq(groupId)); + + mockMvc.perform(post("/api/groups/{groupId}/join-request", groupId) + .header("Authorization", token)) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("가입 요청 실패 : 이미 요청한 그룹") + void joinRequest_fail_alreadyRequested() throws Exception { + Long groupId = 10L; + willThrow(new JoinRequestException("이미 요청한 그룹입니다.")) + .given(joinRequestService).joinRequest(any(User.class), eq(groupId)); + + mockMvc.perform(post("/api/groups/{groupId}/join-request", groupId) + .header("Authorization", token)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("가입 요청 목록 조회") + void getAllJoinRequests_success() throws Exception { + Long groupId = 10L; + // 직렬화가 비어도 배열 길이만 확인할 수 있게 더미 객체 2개 + given(joinRequestService.getAllJoinRequests(any(User.class), eq(groupId))) + .willReturn(List.of(new JoinRequest(), new JoinRequest())); + + mockMvc.perform(get("/api/groups/{groupId}/join-request", groupId) + .header("Authorization", token)) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$", hasSize(2))); + } + + @Test + @DisplayName("요청 목록 조회 실패 : 권한 없음") + void getAllJoinRequests_fail_alreadyRequested() throws Exception { + Long groupId = 10L; + given(joinRequestService.getAllJoinRequests(any(User.class), eq(groupId))) + .willThrow(new JoinRequestException("요청 목록을 조회할 권한이 없습니다.")); + + mockMvc.perform(get("/api/groups/{groupId}/join-request", groupId) + .header("Authorization", token)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("승인 성공") + void approve_success() throws Exception { + Long requestId = 77L; + UpdateJoinRequestStatusRequest request = new UpdateJoinRequestStatusRequest(JoinRequestStatus.APPROVE); + willDoNothing().given(joinRequestService) + .updateJoinRequest(any(User.class), eq(requestId), eq(request)); + + mockMvc.perform(post("/api/join-request/{requestId}", requestId) + .header("Authorization", token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("승인 실패 해당 요청 없음") + void approve_fail_requestNotFound() throws Exception { + Long groupId = 10L; + Long requestId = 999L; + UpdateJoinRequestStatusRequest request = new UpdateJoinRequestStatusRequest(JoinRequestStatus.APPROVE); + willThrow(new JoinRequestException("해당 요청이 존재하지 않습니다")) + .given(joinRequestService).updateJoinRequest(any(User.class), eq(requestId), eq(request)); + + mockMvc.perform(post("/api/join-request/{requestId}", requestId) + .header("Authorization", token)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("가입 요청 거절 성공") + void reject_success() throws Exception { + Long requestId = 77L; + UpdateJoinRequestStatusRequest request = new UpdateJoinRequestStatusRequest(JoinRequestStatus.REJECT); + + willDoNothing().given(joinRequestService) + .updateJoinRequest(any(User.class), eq(requestId), eq(request)); + + mockMvc.perform(post("/api/join-request/{requestId}", requestId) + .header("Authorization", token) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("가입 요청 거절 실패 권한 없음") + void reject_fail_noPermission() throws Exception { + Long requestId = 77L; + UpdateJoinRequestStatusRequest request = new UpdateJoinRequestStatusRequest(JoinRequestStatus.REJECT); + + willThrow(new JoinRequestException("승인 권한이 없습니다.")) + .given(joinRequestService).updateJoinRequest(any(User.class), eq(requestId), eq(request)); + + mockMvc.perform(post("/api/join-request/{requestId}", requestId) + .header("Authorization", token)) + .andExpect(status().isBadRequest()); + } +} diff --git a/src/test/java/com/gamzabat/algohub/feature/studygroup/controller/StudyGroupControllerTest.java b/src/test/java/com/gamzabat/algohub/feature/studygroup/controller/StudyGroupControllerTest.java index 6ee99d6e..634b0f85 100644 --- a/src/test/java/com/gamzabat/algohub/feature/studygroup/controller/StudyGroupControllerTest.java +++ b/src/test/java/com/gamzabat/algohub/feature/studygroup/controller/StudyGroupControllerTest.java @@ -2,9 +2,13 @@ import static org.hamcrest.Matchers.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.doNothing; +import static org.mockito.BDDMockito.doThrow; +import static org.mockito.BDDMockito.times; +import static org.mockito.BDDMockito.verify; +import static org.mockito.BDDMockito.when; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -55,6 +59,7 @@ import com.gamzabat.algohub.feature.group.studygroup.exception.GroupMemberValidationException; import com.gamzabat.algohub.feature.group.studygroup.repository.GroupMemberRepository; import com.gamzabat.algohub.feature.group.studygroup.repository.StudyGroupRepository; +import com.gamzabat.algohub.feature.group.studygroup.service.JoinRequestService; import com.gamzabat.algohub.feature.group.studygroup.service.StudyGroupService; import com.gamzabat.algohub.feature.image.service.ImageService; import com.gamzabat.algohub.feature.problem.repository.ProblemRepository; @@ -79,6 +84,8 @@ class StudyGroupControllerTest { @MockBean private StudyGroupService studyGroupService; @MockBean + private JoinRequestService joinRequestService; + @MockBean private StudyGroupRepository studyGroupRepository; @MockBean private GroupMemberRepository groupMemberRepository; @@ -865,4 +872,5 @@ void editGroupVisibilityFailed_2() throws Exception { .andExpect(jsonPath("$.error").value("참여하지 않은 그룹입니다.")); verify(studyGroupService, times(1)).editStudyGroupVisibility(user, groupId, request); } + } \ No newline at end of file diff --git a/src/test/java/com/gamzabat/algohub/service/JoinRequestServiceTest.java b/src/test/java/com/gamzabat/algohub/service/JoinRequestServiceTest.java new file mode 100644 index 00000000..39400ba9 --- /dev/null +++ b/src/test/java/com/gamzabat/algohub/service/JoinRequestServiceTest.java @@ -0,0 +1,278 @@ +package com.gamzabat.algohub.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Field; +import java.time.LocalDate; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.gamzabat.algohub.enums.JoinRequestStatus; +import com.gamzabat.algohub.enums.Role; +import com.gamzabat.algohub.feature.group.ranking.repository.RankingRepository; +import com.gamzabat.algohub.feature.group.studygroup.domain.GroupMember; +import com.gamzabat.algohub.feature.group.studygroup.domain.JoinRequest; +import com.gamzabat.algohub.feature.group.studygroup.domain.StudyGroup; +import com.gamzabat.algohub.feature.group.studygroup.dto.UpdateJoinRequestStatusRequest; +import com.gamzabat.algohub.feature.group.studygroup.etc.RoleOfGroupMember; +import com.gamzabat.algohub.feature.group.studygroup.exception.GroupMemberValidationException; +import com.gamzabat.algohub.feature.group.studygroup.exception.JoinRequestException; +import com.gamzabat.algohub.feature.group.studygroup.repository.GroupMemberRepository; +import com.gamzabat.algohub.feature.group.studygroup.repository.JoinRequestRepository; +import com.gamzabat.algohub.feature.group.studygroup.repository.StudyGroupRepository; +import com.gamzabat.algohub.feature.group.studygroup.service.JoinRequestService; +import com.gamzabat.algohub.feature.group.studygroup.service.StudyGroupService; +import com.gamzabat.algohub.feature.notification.repository.NotificationSettingRepository; +import com.gamzabat.algohub.feature.problem.domain.Problem; +import com.gamzabat.algohub.feature.user.domain.User; + +@ExtendWith(MockitoExtension.class) +class JoinRequestServiceTest { + @InjectMocks + private JoinRequestService joinRequestService; + @Mock + private StudyGroupService studyGroupService; + @Mock + private StudyGroupRepository studyGroupRepository; + @Mock + private JoinRequestRepository joinRequestRepository; + @Mock + private GroupMemberRepository groupMemberRepository; + @Mock + private NotificationSettingRepository notificationSettingRepository; + @Mock + private RankingRepository rankingRepository; + + private User user, owner, user2, user3, requester; + private StudyGroup group; + private Problem problem1, problem2; + private GroupMember groupMember1, groupMember2, groupMember3; + private GroupMember ownerGroupmember; + private JoinRequest joinRequest; + @Captor + private ArgumentCaptor groupCaptor; + @Captor + private ArgumentCaptor memberCaptor; + + @BeforeEach + void setUp() throws NoSuchFieldException, IllegalAccessException { + user = User.builder().email("email1").password("password").nickname("nickname1") + .role(Role.USER).profileImage("image1").build(); + owner = User.builder().email("email1").password("password").nickname("nickname1") + .role(Role.USER).profileImage("image1").build(); + user2 = User.builder().email("email2").password("password").nickname("nickname2") + .role(Role.USER).profileImage("image2").build(); + user3 = User.builder().email("email3").password("password").nickname("nickname3") + .role(Role.USER).profileImage("image3").build(); + requester = User.builder().email("eamilRequester").password("password").nickname("requester") + .role(Role.USER).profileImage("imageForRequester").build(); + + group = StudyGroup.builder() + .name("name") + .startDate(LocalDate.now()) + .endDate(LocalDate.now().plusDays(1)) + .groupImage("imageUrl") + .groupCode("code") + .build(); + ownerGroupmember = GroupMember.builder() + .studyGroup(group) + .user(owner) + .role(RoleOfGroupMember.OWNER) + .joinDate(LocalDate.now()) + .build(); + groupMember1 = GroupMember.builder() + .studyGroup(group) + .user(user) + .role(RoleOfGroupMember.OWNER) + .joinDate(LocalDate.now()) + .build(); + groupMember2 = GroupMember.builder() + .studyGroup(group) + .user(user2) + .role(RoleOfGroupMember.PARTICIPANT) + .joinDate(LocalDate.now()) + .build(); + groupMember3 = GroupMember.builder() + .studyGroup(group) + .user(user3) + .role(RoleOfGroupMember.ADMIN) + .joinDate(LocalDate.now()) + .build(); + + problem1 = Problem.builder() + .studyGroup(group) + .build(); + problem2 = Problem.builder() + .studyGroup(group) + .build(); + + Field userField = User.class.getDeclaredField("id"); + userField.setAccessible(true); + userField.set(user, 1L); + userField.set(owner, 1L); + userField.set(user2, 2L); + userField.set(user3, 3L); + userField.set(requester, 4L); + + Field groupId = StudyGroup.class.getDeclaredField("id"); + groupId.setAccessible(true); + groupId.set(group, 10L); + + Field memberId = GroupMember.class.getDeclaredField("id"); + memberId.setAccessible(true); + memberId.set(groupMember1, 100L); + memberId.set(groupMember2, 200L); + memberId.set(groupMember3, 300L); + //For Join Request Service Test + joinRequest = new JoinRequest(group, requester); + Field requestId = JoinRequest.class.getDeclaredField("id"); + Field requestGroup = JoinRequest.class.getDeclaredField("group"); + requestGroup.setAccessible(true); + requestGroup.set(joinRequest, group); + requestId.setAccessible(true); + requestId.set(joinRequest, 1000L); + + } + + @Test + @DisplayName("그룹 가입 요청 성공") + void joinRequest_Success() { + // given + when(studyGroupRepository.findById(10L)).thenReturn(Optional.of(group)); + when(groupMemberRepository.existsByUserAndStudyGroup(requester, group)).thenReturn(false); + when(joinRequestRepository.existsByGroup_IdAndRequester_Id(group.getId(), requester.getId())) + .thenReturn(false); + + // when + joinRequestService.joinRequest(requester, 10L); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(JoinRequest.class); + verify(joinRequestRepository, times(1)).save(captor.capture()); + JoinRequest savedRequest = captor.getValue(); + + assertThat(savedRequest.getRequester()).isEqualTo(requester); + assertThat(savedRequest.getGroup()).isEqualTo(group); + } + + @Test + @DisplayName("그룹 가입 요청 실패 : 이미 가입한 그룹") + void joinRequest_Fail_AlreadyMember() { + // given + when(studyGroupRepository.findById(10L)).thenReturn(Optional.of(group)); + when(groupMemberRepository.existsByUserAndStudyGroup(requester, group)).thenReturn(true); + + // when, then + assertThatThrownBy(() -> joinRequestService.joinRequest(requester, 10L)) + .isInstanceOf(GroupMemberValidationException.class) + .hasFieldOrPropertyWithValue("error", "이미 가입한 그룹입니다"); + } + + @Test + @DisplayName("그룹 가입 요청 실패 : 이미 요청한 그룹") + void joinRequest_Fail_AlreadyRequested() { + // given + when(studyGroupRepository.findById(10L)).thenReturn(Optional.of(group)); + when(groupMemberRepository.existsByUserAndStudyGroup(requester, group)).thenReturn(false); + when(joinRequestRepository.existsByGroup_IdAndRequester_Id(group.getId(), requester.getId())).thenReturn( + true); + + // when, then + assertThatThrownBy(() -> joinRequestService.joinRequest(requester, 10L)) + .isInstanceOf(JoinRequestException.class) + .hasMessage("이미 요청한 그룹입니다."); + } + + @Test + @DisplayName("가입 요청 목록 조회 성공") + void getAllJoinRequests_Success() { + // given + when(studyGroupRepository.findById(10L)).thenReturn(Optional.of(group)); + when(groupMemberRepository.findByUserAndStudyGroup(owner, group)).thenReturn(Optional.of(ownerGroupmember)); + when(joinRequestRepository.findAllByGroup_Id(10L)).thenReturn(List.of(joinRequest)); + + // when + List requests = joinRequestService.getAllJoinRequests(owner, 10L); + + // then + assertThat(requests).hasSize(1); + assertThat(requests.get(0).getRequester().getNickname()).isEqualTo("requester"); + } + + @Test + @DisplayName("가입 요청 목록 조회 실패 : 권한 없음") + void getAllJoinRequests_Fail_NoPermission() { + // given + when(studyGroupRepository.findById(10L)).thenReturn(Optional.of(group)); + when(groupMemberRepository.findByUserAndStudyGroup(groupMember2.getUser(), group)).thenReturn( + Optional.of(groupMember2)); + + // when, then + assertThatThrownBy(() -> joinRequestService.getAllJoinRequests(groupMember2.getUser(), 10L)) + .isInstanceOf(JoinRequestException.class) + .hasMessage("요청 목록을 조회할 권한이 없습니다."); + } + + @Test + @DisplayName("가입 요청 승인 성공") + void approveJoinRequest_Success() { + // given + when(joinRequestRepository.findById(1000L)).thenReturn(Optional.of(joinRequest)); + when(studyGroupRepository.findById(joinRequest.getGroup().getId())).thenReturn(Optional.of(group)); + when(groupMemberRepository.findByUserAndStudyGroup(owner, group)).thenReturn(Optional.of(ownerGroupmember)); + UpdateJoinRequestStatusRequest request = new UpdateJoinRequestStatusRequest(JoinRequestStatus.APPROVE); + // when + joinRequestService.updateJoinRequest(owner, 1000L, request); + + // then + ArgumentCaptor captor = ArgumentCaptor.forClass(GroupMember.class); + verify(groupMemberRepository, times(1)).save(captor.capture()); + GroupMember newMember = captor.getValue(); + + assertThat(newMember.getUser()).isEqualTo(requester); + assertThat(newMember.getRole()).isEqualTo(RoleOfGroupMember.PARTICIPANT); + verify(joinRequestRepository, times(1)).delete(joinRequest); + + } + + @Test + @DisplayName("가입 요청 승인 실패 : 요청 없음") + void approveJoinRequest_Fail_RequestNotFound() { + // given + when(joinRequestRepository.findById(100L)).thenReturn(Optional.empty()); + UpdateJoinRequestStatusRequest request = new UpdateJoinRequestStatusRequest(JoinRequestStatus.APPROVE); + + // when, then + assertThatThrownBy(() -> joinRequestService.updateJoinRequest(owner, 100L, request)) + .isInstanceOf(JoinRequestException.class) + .hasMessage("해당 요청이 존재하지 않습니다"); + } + + @Test + @DisplayName("가입 요청 거절 성공 STATUS : PENDING -> REJECT 로 변경") + void rejectJoinRequest_Success() { + // given + when(joinRequestRepository.findById(1000L)).thenReturn(Optional.of(joinRequest)); + when(studyGroupRepository.findById(10L)).thenReturn(Optional.of(group)); + when(groupMemberRepository.findByUserAndStudyGroup(owner, group)).thenReturn(Optional.of(ownerGroupmember)); + UpdateJoinRequestStatusRequest request = new UpdateJoinRequestStatusRequest(JoinRequestStatus.REJECT); + + // when + joinRequestService.updateJoinRequest(owner, 1000L, request); + + // then + verify(joinRequestRepository, times(1)).delete(joinRequest); + } + +} diff --git a/src/test/java/com/gamzabat/algohub/service/StudyGroupServiceTest.java b/src/test/java/com/gamzabat/algohub/service/StudyGroupServiceTest.java index 4bc6501b..3c2543e7 100644 --- a/src/test/java/com/gamzabat/algohub/service/StudyGroupServiceTest.java +++ b/src/test/java/com/gamzabat/algohub/service/StudyGroupServiceTest.java @@ -35,6 +35,7 @@ import com.gamzabat.algohub.feature.group.ranking.repository.RankingRepository; import com.gamzabat.algohub.feature.group.studygroup.domain.BookmarkedStudyGroup; import com.gamzabat.algohub.feature.group.studygroup.domain.GroupMember; +import com.gamzabat.algohub.feature.group.studygroup.domain.JoinRequest; import com.gamzabat.algohub.feature.group.studygroup.domain.StudyGroup; import com.gamzabat.algohub.feature.group.studygroup.dto.BookmarkStatus; import com.gamzabat.algohub.feature.group.studygroup.dto.CreateGroupRequest; @@ -53,6 +54,7 @@ import com.gamzabat.algohub.feature.group.studygroup.exception.GroupMemberValidationException; import com.gamzabat.algohub.feature.group.studygroup.repository.BookmarkedStudyGroupRepository; import com.gamzabat.algohub.feature.group.studygroup.repository.GroupMemberRepository; +import com.gamzabat.algohub.feature.group.studygroup.repository.JoinRequestRepository; import com.gamzabat.algohub.feature.group.studygroup.repository.StudyGroupRepository; import com.gamzabat.algohub.feature.group.studygroup.service.StudyGroupService; import com.gamzabat.algohub.feature.image.service.ImageService; @@ -78,6 +80,7 @@ class StudyGroupServiceTest { private StudyGroupService studyGroupService; @Mock private NotificationService notificationService; + @Mock private StudyGroupRepository studyGroupRepository; @Mock @@ -109,13 +112,16 @@ class StudyGroupServiceTest { @Mock private ObjectProvider studyGroupServiceObjectProvider; @Mock + private JoinRequestRepository joinRequestRepository; + @Mock private ImageService imageService; - private User user, owner, user2, user3; + private User user, owner, user2, user3, requester; private StudyGroup group; private Problem problem1, problem2; private Solution solution1, solution2, solution3; private GroupMember groupMember1, groupMember2, groupMember3; private GroupMember ownerGroupmember; + private JoinRequest joinRequest; @Captor private ArgumentCaptor groupCaptor; @Captor @@ -131,6 +137,8 @@ void setUp() throws NoSuchFieldException, IllegalAccessException { .role(Role.USER).profileImage("image2").build(); user3 = User.builder().email("email3").password("password").nickname("nickname3") .role(Role.USER).profileImage("image3").build(); + requester = User.builder().email("eamilRequester").password("password").nickname("requester") + .role(Role.USER).profileImage("imageForRequester").build(); group = StudyGroup.builder() .name("name") @@ -195,6 +203,7 @@ void setUp() throws NoSuchFieldException, IllegalAccessException { userField.set(owner, 1L); userField.set(user2, 2L); userField.set(user3, 3L); + userField.set(requester, 4L); Field groupId = StudyGroup.class.getDeclaredField("id"); groupId.setAccessible(true); @@ -205,6 +214,15 @@ void setUp() throws NoSuchFieldException, IllegalAccessException { memberId.set(groupMember1, 100L); memberId.set(groupMember2, 200L); memberId.set(groupMember3, 300L); + //For Join Request Service Test + joinRequest = new JoinRequest(group, requester); + Field requestId = JoinRequest.class.getDeclaredField("id"); + Field requestGroup = JoinRequest.class.getDeclaredField("group"); + requestGroup.setAccessible(true); + requestGroup.set(joinRequest, group); + requestId.setAccessible(true); + requestId.set(joinRequest, 1000L); + } @Test @@ -247,7 +265,8 @@ void joinGroupWithCode() { assertThat(result.getStudyGroup()).isEqualTo(group); assertThat(result.getUser()).isEqualTo(user2); verify(groupMemberRepository, times(1)).save(any(GroupMember.class)); - verify(notificationService, times(1)).sendNotificationToMembers(any(), any(), any(), any(), any(), any(), any()); + verify(notificationService, times(1)).sendNotificationToMembers(any(), any(), any(), any(), any(), any(), + any()); } @Test