diff --git a/src/main/java/com/back/domain/chat/room/controller/RoomChatApiController.java b/src/main/java/com/back/domain/chat/room/controller/RoomChatApiController.java index a438e68b..432178ef 100644 --- a/src/main/java/com/back/domain/chat/room/controller/RoomChatApiController.java +++ b/src/main/java/com/back/domain/chat/room/controller/RoomChatApiController.java @@ -14,7 +14,6 @@ import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @@ -27,7 +26,6 @@ public class RoomChatApiController { private final RoomChatService roomChatService; - private final SimpMessagingTemplate messagingTemplate; // 방 채팅 메시지 조회 (페이징, 특정 시간 이전 메시지) @GetMapping @@ -73,18 +71,6 @@ public ResponseEntity> clearRoomMessages( clearedByInfo.role() ); - // WebSocket을 통해 실시간 알림 전송 - ChatClearedNotification notification = ChatClearedNotification.create( - roomId, - messageCount, - clearedByInfo.userId(), - clearedByInfo.nickname(), - clearedByInfo.profileImageUrl(), - clearedByInfo.role() - ); - - messagingTemplate.convertAndSend("/topic/room/" + roomId + "/chat-cleared", notification); - return ResponseEntity .status(HttpStatus.OK) .body(RsData.success("채팅 메시지 일괄 삭제 완료", responseData)); diff --git a/src/main/java/com/back/domain/chat/room/controller/RoomChatWebSocketController.java b/src/main/java/com/back/domain/chat/room/controller/RoomChatWebSocketController.java index 2c6f830b..c29cb7ee 100644 --- a/src/main/java/com/back/domain/chat/room/controller/RoomChatWebSocketController.java +++ b/src/main/java/com/back/domain/chat/room/controller/RoomChatWebSocketController.java @@ -1,23 +1,22 @@ package com.back.domain.chat.room.controller; -import com.back.domain.chat.room.dto.ChatClearRequest; -import com.back.domain.chat.room.dto.ChatClearedNotification; +import com.back.domain.chat.room.dto.RoomChatMessageRequest; +import com.back.domain.chat.room.dto.RoomChatMessageResponse; import com.back.domain.studyroom.entity.RoomChatMessage; -import com.back.domain.chat.room.dto.RoomChatMessageDto; import com.back.global.exception.CustomException; +import com.back.global.exception.ErrorCode; import com.back.global.security.user.CustomUserDetails; import com.back.domain.chat.room.service.RoomChatService; import com.back.global.websocket.util.WebSocketAuthHelper; import com.back.global.websocket.util.WebSocketErrorHelper; -import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.DestinationVariable; +import org.springframework.messaging.handler.annotation.MessageExceptionHandler; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.security.core.Authentication; import org.springframework.stereotype.Controller; import java.security.Principal; @@ -25,12 +24,10 @@ @Slf4j @Controller @RequiredArgsConstructor -@Tag(name = "RoomChat WebSocket", description = "STOMP를 이용한 실시간 채팅 WebSocket 컨트롤러 (Swagger에서 직접 테스트 불가)") public class RoomChatWebSocketController { private final RoomChatService roomChatService; private final SimpMessagingTemplate messagingTemplate; - private final WebSocketAuthHelper authHelper; private final WebSocketErrorHelper errorHelper; /** @@ -39,117 +36,30 @@ public class RoomChatWebSocketController { */ @MessageMapping("/chat/room/{roomId}") public void handleRoomChat(@DestinationVariable Long roomId, - RoomChatMessageDto chatMessage, - SimpMessageHeaderAccessor headerAccessor, + @Payload RoomChatMessageRequest request, Principal principal) { - try { - // WebSocket에서 인증된 사용자 정보 추출 - CustomUserDetails userDetails = authHelper.extractUserDetails(principal); - - if (userDetails == null) { - errorHelper.sendUnauthorizedError(headerAccessor.getSessionId()); - return; - } - - Long currentUserId = userDetails.getUserId(); - String currentUserNickname = userDetails.getUsername(); - - // 메시지 정보 보완 - RoomChatMessageDto enrichedMessage = chatMessage - .withRoomId(roomId) - .withUserId(currentUserId) - .withNickname(currentUserNickname); - - // DB에 메시지 저장 - RoomChatMessage savedMessage = roomChatService.saveRoomChatMessage(enrichedMessage); - - // 저장된 메시지 정보로 응답 DTO 생성 - RoomChatMessageDto responseMessage = RoomChatMessageDto.createResponse( - savedMessage.getId(), - roomId, - savedMessage.getUser().getId(), - savedMessage.getUser().getNickname(), - savedMessage.getUser().getProfileImageUrl(), - savedMessage.getContent(), - chatMessage.messageType(), - null, // 텍스트 채팅에서는 null - savedMessage.getCreatedAt() - ); - - // 해당 방의 모든 구독자에게 브로드캐스트 - messagingTemplate.convertAndSend("/topic/room/" + roomId, responseMessage); - - } catch (CustomException e) { - log.warn("채팅 메시지 처리 실패 - roomId: {}, error: {}", roomId, e.getMessage()); - errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e); - - } catch (Exception e) { - log.error("채팅 메시지 처리 중 예상치 못한 오류 발생 - roomId: {}", roomId, e); - errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "메시지 전송 중 오류가 발생했습니다"); + CustomUserDetails userDetails = WebSocketAuthHelper.extractUserDetails(principal); + if (userDetails == null) { + // 인증 정보가 없는 경우 예외 처리 + throw new CustomException(ErrorCode.UNAUTHORIZED); } - } - - /** - * 스터디룸 채팅 일괄 삭제 처리 - * 클라이언트가 /app/chat/room/{roomId}/clear로 삭제 요청 시 호출 - */ - @MessageMapping("/chat/room/{roomId}/clear") - public void clearRoomChat(@DestinationVariable Long roomId, - @Payload ChatClearRequest request, - SimpMessageHeaderAccessor headerAccessor, - Principal principal) { - try { - log.info("WebSocket 채팅 일괄 삭제 요청 - roomId: {}", roomId); + RoomChatMessage savedMessage = roomChatService.saveRoomChatMessage( + roomId, + userDetails.getUserId(), + request + ); - // 사용자 인증 확인 - CustomUserDetails userDetails = authHelper.extractUserDetails(principal); - - if (userDetails == null) { - errorHelper.sendUnauthorizedError(headerAccessor.getSessionId()); - return; - } - - // 삭제 확인 메시지 검증 - if (!request.isValidConfirmMessage()) { - errorHelper.sendErrorToUser(headerAccessor.getSessionId(), "WS_011", - "삭제 확인 메시지가 일치하지 않습니다"); - return; - } - - Long currentUserId = userDetails.getUserId(); - - // 삭제 전에 메시지 수 먼저 조회 (삭제 후에는 0이 되므로) - int deletedCount = roomChatService.getRoomChatCount(roomId); - - // 채팅 일괄 삭제 실행 - ChatClearedNotification.ClearedByDto clearedByInfo = roomChatService.clearRoomChat(roomId, currentUserId); - - // 알림 생성 - ChatClearedNotification notification = ChatClearedNotification.create( - roomId, - deletedCount, // 삭제 전에 조회한 수 사용 - clearedByInfo.userId(), - clearedByInfo.nickname(), - clearedByInfo.profileImageUrl(), - clearedByInfo.role() - ); - - // 해당 방의 모든 구독자에게 브로드캐스트 - messagingTemplate.convertAndSend("/topic/room/" + roomId + "/chat-cleared", notification); - - log.info("WebSocket 채팅 일괄 삭제 완료 - roomId: {}, deletedCount: {}, userId: {}", - roomId, deletedCount, currentUserId); - - } catch (CustomException e) { - log.warn("채팅 삭제 실패 - roomId: {}, error: {}", roomId, e.getMessage()); - errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e); - - } catch (Exception e) { - log.error("채팅 일괄 삭제 중 예상치 못한 오류 발생 - roomId: {}", roomId, e); - errorHelper.sendGenericErrorToUser(headerAccessor.getSessionId(), e, "채팅 삭제 중 오류가 발생했습니다"); - } + // 응답 DTO 생성 및 브로드캐스트 + RoomChatMessageResponse responseMessage = RoomChatMessageResponse.from(savedMessage); + messagingTemplate.convertAndSend("/topic/room/" + roomId, responseMessage); } + // 채팅 메시지 처리 중 발생하는 예외 중앙 처리 + @MessageExceptionHandler(CustomException.class) + public void handleChatException(CustomException e, SimpMessageHeaderAccessor headerAccessor) { + log.warn("채팅 메시지 처리 실패 - SessionId: {}, Error: {}", headerAccessor.getSessionId(), e.getMessage()); + errorHelper.sendCustomExceptionToUser(headerAccessor.getSessionId(), e); + } } \ No newline at end of file diff --git a/src/main/java/com/back/domain/chat/room/dto/RoomChatMessageDto.java b/src/main/java/com/back/domain/chat/room/dto/RoomChatMessageDto.java deleted file mode 100644 index 42b4fb17..00000000 --- a/src/main/java/com/back/domain/chat/room/dto/RoomChatMessageDto.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.back.domain.chat.room.dto; - -import java.time.LocalDateTime; - -public record RoomChatMessageDto( - // WebSocket Request - String content, - String messageType, - Long attachmentId, - - // WebSocket Response - Long messageId, - Long roomId, - Long userId, - String nickname, - String profileImageUrl, - AttachmentDto attachment, - LocalDateTime createdAt -) { - - // 첨부파일 DTO (나중에 파일 기능 구현 시 사용) - public record AttachmentDto( - Long id, - String originalName, - String url, - Long size, - String mimeType - ) {} - - // 텍스트 채팅 요청 생성 헬퍼 - public static RoomChatMessageDto createRequest(String content, String messageType) { - return new RoomChatMessageDto( - content, - messageType, - null, // attachmentId - 텍스트 채팅에서는 null - null, // messageId - null, // roomId - null, // userId - null, // nickname - null, // profileImageUrl - null, // attachment - null // createdAt - ); - } - - // 필드 업데이트 - public RoomChatMessageDto withRoomId(Long roomId) { - return new RoomChatMessageDto(content, messageType, attachmentId, messageId, roomId, userId, nickname, profileImageUrl, attachment, createdAt); - } - - public RoomChatMessageDto withUserId(Long userId) { - return new RoomChatMessageDto(content, messageType, attachmentId, messageId, roomId, userId, nickname, profileImageUrl, attachment, createdAt); - } - - public RoomChatMessageDto withNickname(String nickname) { - return new RoomChatMessageDto(content, messageType, attachmentId, messageId, roomId, userId, nickname, profileImageUrl, attachment, createdAt); - } - - // Response용 생성자 - public static RoomChatMessageDto createResponse( - Long messageId, Long roomId, Long userId, String nickname, - String profileImageUrl, String content, String messageType, - AttachmentDto attachment, LocalDateTime createdAt) { - return new RoomChatMessageDto( - content, messageType, null, // attachmentId는 request용이므로 null - messageId, roomId, userId, nickname, profileImageUrl, attachment, createdAt - ); - } -} diff --git a/src/main/java/com/back/domain/chat/room/dto/RoomChatMessageRequest.java b/src/main/java/com/back/domain/chat/room/dto/RoomChatMessageRequest.java new file mode 100644 index 00000000..71a07503 --- /dev/null +++ b/src/main/java/com/back/domain/chat/room/dto/RoomChatMessageRequest.java @@ -0,0 +1,5 @@ +package com.back.domain.chat.room.dto; + +public record RoomChatMessageRequest( + String content +) {} diff --git a/src/main/java/com/back/domain/chat/room/dto/RoomChatMessageResponse.java b/src/main/java/com/back/domain/chat/room/dto/RoomChatMessageResponse.java new file mode 100644 index 00000000..49d4b73f --- /dev/null +++ b/src/main/java/com/back/domain/chat/room/dto/RoomChatMessageResponse.java @@ -0,0 +1,27 @@ +package com.back.domain.chat.room.dto; + +import com.back.domain.studyroom.entity.RoomChatMessage; + +import java.time.LocalDateTime; + +public record RoomChatMessageResponse( + Long messageId, + Long roomId, + Long userId, + String nickname, + String profileImageUrl, + String content, + LocalDateTime createdAt +) { + public static RoomChatMessageResponse from(RoomChatMessage entity) { + return new RoomChatMessageResponse( + entity.getId(), + entity.getRoom().getId(), + entity.getUser().getId(), + entity.getUser().getNickname(), + entity.getUser().getProfileImageUrl(), + entity.getContent(), + entity.getCreatedAt() + ); + } +} diff --git a/src/main/java/com/back/domain/chat/room/dto/RoomChatPageResponse.java b/src/main/java/com/back/domain/chat/room/dto/RoomChatPageResponse.java index 2489b721..4af642e3 100644 --- a/src/main/java/com/back/domain/chat/room/dto/RoomChatPageResponse.java +++ b/src/main/java/com/back/domain/chat/room/dto/RoomChatPageResponse.java @@ -3,7 +3,7 @@ import java.util.List; public record RoomChatPageResponse( - List content, + List content, PageableDto pageable, long totalElements ) { @@ -18,7 +18,7 @@ public record PageableDto( // 페이지 응답 생성 public static RoomChatPageResponse from( org.springframework.data.domain.Page page, - List convertedContent) { + List convertedContent) { return new RoomChatPageResponse( convertedContent, @@ -41,7 +41,7 @@ public static RoomChatPageResponse empty(int page, int size) { } // 단일 페이지 응답 생성 (테스트용) - public static RoomChatPageResponse of(List content, + public static RoomChatPageResponse of(List content, int page, int size, boolean hasNext, diff --git a/src/main/java/com/back/domain/chat/room/service/RoomChatService.java b/src/main/java/com/back/domain/chat/room/service/RoomChatService.java index c938ddf5..aab99deb 100644 --- a/src/main/java/com/back/domain/chat/room/service/RoomChatService.java +++ b/src/main/java/com/back/domain/chat/room/service/RoomChatService.java @@ -1,6 +1,8 @@ package com.back.domain.chat.room.service; import com.back.domain.chat.room.dto.ChatClearedNotification; +import com.back.domain.chat.room.dto.RoomChatMessageRequest; +import com.back.domain.chat.room.dto.RoomChatMessageResponse; import com.back.domain.studyroom.entity.Room; import com.back.domain.studyroom.entity.RoomChatMessage; import com.back.domain.studyroom.entity.RoomMember; @@ -10,11 +12,9 @@ import com.back.domain.studyroom.repository.RoomRepository; import com.back.domain.user.entity.User; import com.back.domain.user.repository.UserRepository; -import com.back.domain.chat.room.dto.RoomChatMessageDto; import com.back.domain.chat.room.dto.RoomChatPageResponse; import com.back.global.exception.CustomException; import com.back.global.exception.ErrorCode; -import com.back.global.security.user.CurrentUser; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -33,7 +33,6 @@ public class RoomChatService { private final RoomMemberRepository roomMemberRepository; private final RoomRepository roomRepository; private final UserRepository userRepository; - private final CurrentUser currentUser; // 페이징 설정 상수 private static final int DEFAULT_PAGE_SIZE = 20; @@ -41,37 +40,29 @@ public class RoomChatService { // 방 채팅 메시지 저장 @Transactional - public RoomChatMessage saveRoomChatMessage(RoomChatMessageDto roomChatMessageDto) { + public RoomChatMessage saveRoomChatMessage(Long roomId, Long userId, RoomChatMessageRequest request) { - // 방 존재 여부 확인 - Room room = roomRepository.findById(roomChatMessageDto.roomId()) + Room room = roomRepository.findById(roomId) .orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND)); - // 사용자 존재 여부 확인 - User user = userRepository.findById(roomChatMessageDto.userId()) + User user = userRepository.findById(userId) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - // RoomChatMessage 엔티티 생성 및 저장 - RoomChatMessage message = new RoomChatMessage(room, user, roomChatMessageDto.content()); - RoomChatMessage savedMessage = roomChatMessageRepository.save(message); - - return savedMessage; + // DTO에서 직접 content를 가져와 사용 + RoomChatMessage message = new RoomChatMessage(room, user, request.content()); + return roomChatMessageRepository.save(message); } // 방 채팅 기록 조회 @Transactional(readOnly = true) public RoomChatPageResponse getRoomChatHistory(Long roomId, int page, int size, LocalDateTime before) { - // 방 존재 여부 확인 roomRepository.findById(roomId) .orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND)); - // size 값 검증 및 제한 int validatedSize = validateAndLimitPageSize(size); - Pageable pageable = PageRequest.of(page, validatedSize); - // before 파라미터가 있으면 해당 시점 이전 메시지만 조회 Page messagesPage; if (before != null) { messagesPage = roomChatMessageRepository.findMessagesByRoomIdBefore(roomId, before, pageable); @@ -79,9 +70,9 @@ public RoomChatPageResponse getRoomChatHistory(Long roomId, int page, int size, messagesPage = roomChatMessageRepository.findMessagesByRoomId(roomId, pageable); } - List convertedContent = messagesPage.getContent() + List convertedContent = messagesPage.getContent() .stream() - .map(this::convertToDto) + .map(RoomChatMessageResponse::from) .toList(); return RoomChatPageResponse.from(messagesPage, convertedContent); @@ -91,38 +82,27 @@ public RoomChatPageResponse getRoomChatHistory(Long roomId, int page, int size, @Transactional public ChatClearedNotification.ClearedByDto clearRoomChat(Long roomId, Long userId) { - // 방 존재 여부 확인 Room room = roomRepository.findById(roomId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 방입니다")); + .orElseThrow(() -> new CustomException(ErrorCode.ROOM_NOT_FOUND)); - // 사용자 존재 여부 확인 User user = userRepository.findById(userId) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - // 사용자가 해당 방의 멤버인지 확인 RoomMember roomMember = roomMemberRepository.findByRoomIdAndUserId(roomId, userId) .orElseThrow(() -> new CustomException(ErrorCode.NOT_ROOM_MEMBER)); - // 권한 확인 - 방장(HOST) 또는 부방장(SUB_HOST)만 가능 if (!canManageChat(roomMember.getRole())) { - throw new SecurityException("채팅 삭제 권한이 없습니다"); + throw new CustomException(ErrorCode.CHAT_DELETE_FORBIDDEN); } - try { - // 해당 방의 모든 채팅 메시지 삭제 - int deletedCount = roomChatMessageRepository.deleteAllByRoomId(roomId); - - // 삭제를 실행한 사용자 정보 반환 - return new ChatClearedNotification.ClearedByDto( - user.getId(), - user.getNickname(), - user.getProfileImageUrl(), - roomMember.getRole().name() - ); + roomChatMessageRepository.deleteAllByRoomId(roomId); - } catch (Exception e) { - throw new CustomException(ErrorCode.CHAT_DELETE_FAILED); - } + return new ChatClearedNotification.ClearedByDto( + user.getId(), + user.getNickname(), + user.getProfileImageUrl(), + roomMember.getRole().name() + ); } // 방의 현재 채팅 메시지 수 조회 @@ -131,8 +111,6 @@ public int getRoomChatCount(Long roomId) { return roomChatMessageRepository.countByRoomId(roomId); } - // --------------------- 헬퍼 메서드들 --------------------- - // 채팅 관리 권한 확인 (방장 또는 부방장) private boolean canManageChat(RoomRole role) { return role == RoomRole.HOST || role == RoomRole.SUB_HOST; @@ -141,24 +119,8 @@ private boolean canManageChat(RoomRole role) { // size 값 검증 및 최대값 제한 private int validateAndLimitPageSize(int size) { if (size <= 0) { - return DEFAULT_PAGE_SIZE; // 0 이하면 기본값 사용 + return DEFAULT_PAGE_SIZE; } - return Math.min(size, MAX_PAGE_SIZE); // 최대값 제한 - } - - // 메시지 엔티티를 DTO로 변환 - private RoomChatMessageDto convertToDto(RoomChatMessage message) { - return RoomChatMessageDto.createResponse( - message.getId(), - message.getRoom().getId(), - message.getUser().getId(), - message.getUser().getNickname(), - message.getUser().getProfileImageUrl(), - message.getContent(), - "TEXT", // 현재는 텍스트만 지원 - null, // 텍스트 채팅에서는 null - message.getCreatedAt() - ); + return Math.min(size, MAX_PAGE_SIZE); } - } \ No newline at end of file diff --git a/src/main/java/com/back/domain/user/entity/UserProfile.java b/src/main/java/com/back/domain/user/entity/UserProfile.java index 47c9df03..e87a4554 100644 --- a/src/main/java/com/back/domain/user/entity/UserProfile.java +++ b/src/main/java/com/back/domain/user/entity/UserProfile.java @@ -6,12 +6,14 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.experimental.SuperBuilder; import java.time.LocalDate; @Entity @Getter @Setter +@SuperBuilder @NoArgsConstructor @AllArgsConstructor public class UserProfile extends BaseEntity { diff --git a/src/test/java/com/back/domain/chat/room/controller/RoomChatApiControllerTest.java b/src/test/java/com/back/domain/chat/room/controller/RoomChatApiControllerTest.java index edc3e851..e00b50c8 100644 --- a/src/test/java/com/back/domain/chat/room/controller/RoomChatApiControllerTest.java +++ b/src/test/java/com/back/domain/chat/room/controller/RoomChatApiControllerTest.java @@ -205,12 +205,6 @@ void t6() throws Exception { .andExpect(jsonPath("$.data.clearedBy.userId").value(userId)) .andExpect(jsonPath("$.data.clearedBy.nickname").value("방장")) .andExpect(jsonPath("$.data.clearedBy.role").value("HOST")); - - // WebSocket 메시지가 전송되었는지 확인 - verify(messagingTemplate).convertAndSend( - eq("/topic/room/" + roomId + "/chat-cleared"), - any(ChatClearedNotification.class) - ); } @Test diff --git a/src/test/java/com/back/domain/chat/room/controller/RoomChatWebSocketControllerTest.java b/src/test/java/com/back/domain/chat/room/controller/RoomChatWebSocketControllerTest.java index b08593b7..9be644c2 100644 --- a/src/test/java/com/back/domain/chat/room/controller/RoomChatWebSocketControllerTest.java +++ b/src/test/java/com/back/domain/chat/room/controller/RoomChatWebSocketControllerTest.java @@ -1,36 +1,37 @@ package com.back.domain.chat.room.controller; -import com.back.domain.chat.room.dto.ChatClearRequest; -import com.back.domain.chat.room.dto.ChatClearedNotification; -import com.back.domain.chat.room.dto.RoomChatMessageDto; -import com.back.domain.chat.room.service.RoomChatService; +import com.back.domain.chat.room.dto.RoomChatMessageRequest; +import com.back.domain.chat.room.dto.RoomChatMessageResponse; +import com.back.domain.studyroom.entity.Room; import com.back.domain.studyroom.entity.RoomChatMessage; import com.back.domain.user.entity.User; import com.back.domain.user.entity.UserProfile; import com.back.global.exception.CustomException; import com.back.global.exception.ErrorCode; import com.back.global.security.user.CustomUserDetails; +import com.back.domain.chat.room.service.RoomChatService; import com.back.global.websocket.util.WebSocketErrorHelper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; -import java.lang.reflect.Field; -import java.security.Principal; import java.time.LocalDateTime; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.*; @ExtendWith(MockitoExtension.class) +@DisplayName("RoomChatWebSocketController 테스트") class RoomChatWebSocketControllerTest { @Mock @@ -39,418 +40,97 @@ class RoomChatWebSocketControllerTest { @Mock private SimpMessagingTemplate messagingTemplate; - @Mock - private SimpMessageHeaderAccessor headerAccessor; - @Mock private WebSocketErrorHelper errorHelper; @InjectMocks private RoomChatWebSocketController roomChatWebSocketController; - private CustomUserDetails testUser; - private Principal testPrincipal; - private User mockUser; - private UserProfile mockUserProfile; - private RoomChatMessage mockSavedMessage; + private Authentication testAuth; + private Long testUserId = 1L; + private String testUsername = "testuser"; @BeforeEach - void setUp() throws Exception { - testUser = CustomUserDetails.builder() - .userId(1L) - .username("testuser") - .build(); - - Authentication authentication = new UsernamePasswordAuthenticationToken( - testUser, null, testUser.getAuthorities() - ); - testPrincipal = authentication; - - // UserProfile 생성 - mockUserProfile = createUserProfile("테스터", "https://example.com/profile.jpg"); - - // Mock User 객체 생성 - mockUser = User.builder() - .id(1L) - .username("testuser") - .email("test@example.com") - .build(); - - // 리플렉션으로 userProfile 필드 설정 - setUserProfile(mockUser, mockUserProfile); - - // Mock RoomChatMessage 생성 - mockSavedMessage = RoomChatMessage.builder() - .id(100L) - .user(mockUser) - .content("테스트 메시지") - .createdAt(LocalDateTime.now()) + void setUp() { + CustomUserDetails testUserDetails = CustomUserDetails.builder() + .userId(testUserId) + .username(testUsername) .build(); - } - - // UserProfile 생성 헬퍼 메소드 - private UserProfile createUserProfile(String nickname, String profileImageUrl) throws Exception { - UserProfile userProfile = new UserProfile(); - - // 리플렉션으로 private 필드 설정 - setField(userProfile, "nickname", nickname); - setField(userProfile, "profileImageUrl", profileImageUrl); - setField(userProfile, "user", mockUser); - - return userProfile; - } - - // User의 userProfiles 필드 설정 - private void setUserProfile(User user, UserProfile profile) throws Exception { - Field userProfilesField = User.class.getDeclaredField("userProfile"); - userProfilesField.setAccessible(true); - userProfilesField.set(user, profile); - } - - // 리플렉션으로 필드 값 설정하는 헬퍼 메소드 - private void setField(Object target, String fieldName, Object value) throws Exception { - Field field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(target, value); - } - - @Test - @DisplayName("WebSocket 채팅 전체 조회 성공") - void t1() { - // Given - Long roomId = 1L; - - RoomChatMessageDto inputMessage = RoomChatMessageDto.createRequest( - "테스트 메시지", - "TEXT" - ); - - // 실제로 필요한 stubbing만 설정 - given(roomChatService.saveRoomChatMessage(any(RoomChatMessageDto.class))).willReturn(mockSavedMessage); - - // When - roomChatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, testPrincipal); - - // Then - verify(roomChatService).saveRoomChatMessage(argThat(dto -> - dto.roomId().equals(roomId) && - dto.userId().equals(1L) && - dto.content().equals("테스트 메시지") - )); - - verify(messagingTemplate).convertAndSend( - eq("/topic/room/" + roomId), - argThat((RoomChatMessageDto responseDto) -> - responseDto.messageId().equals(100L) && - responseDto.roomId().equals(roomId) && - responseDto.userId().equals(1L) && - responseDto.nickname().equals("테스터") && - responseDto.profileImageUrl().equals("https://example.com/profile.jpg") && - responseDto.content().equals("테스트 메시지") && - responseDto.messageType().equals("TEXT") - ) - ); - - // 에러 메시지는 전송되지 않아야 함 - verify(messagingTemplate, never()).convertAndSendToUser(anyString(), anyString(), any()); - } - - @Test - @DisplayName("WebSocket 채팅 전체 조회 실패 - 인증되지 않은 사용자의 메시지 처리") - void t2() { - Long roomId = 1L; - - RoomChatMessageDto inputMessage = RoomChatMessageDto.createRequest( - "테스트 메시지", - "TEXT" - ); - - Principal invalidPrincipal = null; // 인증 정보 없음 - - // 에러 응답을 위해 sessionId가 필요 - given(headerAccessor.getSessionId()).willReturn("test-session-123"); - - // When - roomChatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, invalidPrincipal); - - // Then - verify(roomChatService, never()).saveRoomChatMessage(any()); - verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); - - // 에러 메시지가 해당 사용자에게만 전송되는지 확인 - verify(errorHelper).sendUnauthorizedError("test-session-123"); - } - - @Test - @DisplayName("WebSocket 채팅 전체 조회 실패 - 서비스 계층 예외 발생 시 에러 처리") - void t3() { - Long roomId = 1L; - - RoomChatMessageDto inputMessage = RoomChatMessageDto.createRequest( - "테스트 메시지", - "TEXT" - ); - - // 예외 발생 시 sessionId와 서비스 예외 설정 - given(headerAccessor.getSessionId()).willReturn("test-session-123"); - given(roomChatService.saveRoomChatMessage(any())).willThrow(new RuntimeException("DB 오류")); - - // When - roomChatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, testPrincipal); - - // Then - verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); - - verify(errorHelper).sendGenericErrorToUser( - eq("test-session-123"), - any(RuntimeException.class), - eq("메시지 전송 중 오류가 발생했습니다") - ); - } - - @Test - @DisplayName("WebSocket 채팅 전체 조회 실패 - 잘못된 Principal 타입 처리") - void t4() { - Long roomId = 1L; - - RoomChatMessageDto inputMessage = RoomChatMessageDto.createRequest( - "테스트 메시지", - "TEXT" - ); - - // Authentication이 아닌 다른 Principal - Principal invalidTypePrincipal = () -> "some-principal-name"; - - given(headerAccessor.getSessionId()).willReturn("test-session-123"); - - // When - roomChatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, invalidTypePrincipal); - - // Then - verify(roomChatService, never()).saveRoomChatMessage(any()); - verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); - - // 실제 호출되는 sendUnauthorizedError 검증 - verify(errorHelper).sendUnauthorizedError("test-session-123"); - } - - @Test - @DisplayName("WebSocket 채팅 전체 조회 실패 - CustomUserDetails가 아닌 Principal 객체 처리") - void t5() { - Long roomId = 1L; - - RoomChatMessageDto inputMessage = RoomChatMessageDto.createRequest( - "테스트 메시지", - "TEXT" - ); - - Authentication authWithWrongPrincipal = new UsernamePasswordAuthenticationToken( - "string-principal", null, null - ); - - given(headerAccessor.getSessionId()).willReturn("test-session-123"); - - // When - roomChatWebSocketController.handleRoomChat(roomId, inputMessage, headerAccessor, authWithWrongPrincipal); - - // Then - verify(roomChatService, never()).saveRoomChatMessage(any()); - verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); - - verify(errorHelper).sendUnauthorizedError("test-session-123"); - } - - @Test - @DisplayName("WebSocket 채팅 전체 삭제 성공 - 방장 권한") - void t6() { - Long roomId = 1L; - int messageCount = 25; - ChatClearRequest request = new ChatClearRequest("모든 채팅을 삭제하겠습니다"); - - ChatClearedNotification.ClearedByDto clearedByInfo = - new ChatClearedNotification.ClearedByDto(1L, "방장", "https://example.com/host.jpg", "HOST"); - - given(roomChatService.getRoomChatCount(roomId)).willReturn(messageCount); - given(roomChatService.clearRoomChat(roomId, 1L)).willReturn(clearedByInfo); - - roomChatWebSocketController.clearRoomChat(roomId, request, headerAccessor, testPrincipal); - - verify(messagingTemplate).convertAndSend(eq("/topic/room/" + roomId + "/chat-cleared"), - argThat((ChatClearedNotification notification) -> - notification.roomId().equals(roomId) && - notification.deletedCount().equals(messageCount) && - notification.clearedBy().userId().equals(1L) && - notification.clearedBy().nickname().equals("방장") && - notification.clearedBy().role().equals("HOST") - )); - } - - @Test - @DisplayName("WebSocket 채팅 전체 삭제 성공 - 부방장 권한") - void t7() { - Long roomId = 2L; - int messageCount = 10; - - ChatClearRequest request = new ChatClearRequest("모든 채팅을 삭제하겠습니다"); - - ChatClearedNotification.ClearedByDto clearedByInfo = new ChatClearedNotification.ClearedByDto( - 1L, "부방장", "https://example.com/subhost.jpg", "SUB_HOST" - ); - - // Mock 설정 - given(roomChatService.getRoomChatCount(roomId)).willReturn(messageCount); - given(roomChatService.clearRoomChat(roomId, 1L)).willReturn(clearedByInfo); - - // When - roomChatWebSocketController.clearRoomChat(roomId, request, headerAccessor, testPrincipal); - - // Then - verify(messagingTemplate).convertAndSend( - eq("/topic/room/" + roomId + "/chat-cleared"), - argThat((ChatClearedNotification notification) -> - notification.clearedBy().role().equals("SUB_HOST") && - notification.clearedBy().nickname().equals("부방장") - ) - ); - } - - @Test - @DisplayName("WebSocket 채팅 전체 삭제 실패 - 인증되지 않은 사용자") - void t8() { - Long roomId = 1L; - ChatClearRequest request = new ChatClearRequest("모든 채팅을 삭제하겠습니다"); - Principal invalidPrincipal = null; - - given(headerAccessor.getSessionId()).willReturn("test-session-123"); - - // When - roomChatWebSocketController.clearRoomChat(roomId, request, headerAccessor, invalidPrincipal); - - // Then - verify(roomChatService, never()).getRoomChatCount(any()); - verify(roomChatService, never()).clearRoomChat(any(), any()); - verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); - - verify(errorHelper).sendUnauthorizedError("test-session-123"); - } - - @Test - @DisplayName("WebSocket 채팅 전체 삭제 실패 - 잘못된 확인 메시지") - void t9() { - Long roomId = 1L; - ChatClearRequest invalidRequest = new ChatClearRequest("잘못된 확인 메시지"); - - given(headerAccessor.getSessionId()).willReturn("test-session-123"); - - // When - roomChatWebSocketController.clearRoomChat(roomId, invalidRequest, headerAccessor, testPrincipal); - - // Then - verify(roomChatService, never()).getRoomChatCount(any()); - verify(roomChatService, never()).clearRoomChat(any(), any()); - verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); - - verify(errorHelper).sendErrorToUser( - eq("test-session-123"), - eq("WS_011"), - eq("삭제 확인 메시지가 일치하지 않습니다") - ); - } - - @Test - @DisplayName("WebSocket 채팅 전체 삭제 실패 - 권한 없음 (일반 멤버)") - void t10() { - Long roomId = 1L; - ChatClearRequest request = new ChatClearRequest("모든 채팅을 삭제하겠습니다"); - - given(headerAccessor.getSessionId()).willReturn("test-session-123"); - given(roomChatService.getRoomChatCount(roomId)).willReturn(5); - given(roomChatService.clearRoomChat(roomId, 1L)) - .willThrow(new SecurityException("채팅 삭제 권한이 없습니다")); - - // When - roomChatWebSocketController.clearRoomChat(roomId, request, headerAccessor, testPrincipal); - - // Then - verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); - verify(errorHelper).sendGenericErrorToUser( - eq("test-session-123"), - any(SecurityException.class), - eq("채팅 삭제 중 오류가 발생했습니다") - ); - } - - @Test - @DisplayName("WebSocket 채팅 전체 삭제 실패 - 방 멤버가 아님") - void t11() { - Long roomId = 1L; - ChatClearRequest request = new ChatClearRequest("모든 채팅을 삭제하겠습니다"); - - given(headerAccessor.getSessionId()).willReturn("test-session-123"); - given(roomChatService.getRoomChatCount(roomId)).willReturn(5); - given(roomChatService.clearRoomChat(roomId, 1L)) - .willThrow(new CustomException(ErrorCode.NOT_ROOM_MEMBER)); - - // When - roomChatWebSocketController.clearRoomChat(roomId, request, headerAccessor, testPrincipal); - - // Then - verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); - verify(errorHelper).sendCustomExceptionToUser( - eq("test-session-123"), - any(CustomException.class) - ); - } - - @Test - @DisplayName("WebSocket 채팅 전체 삭제 실패 - 존재하지 않는 방") - void t12() { - Long roomId = 999L; - ChatClearRequest request = new ChatClearRequest("모든 채팅을 삭제하겠습니다"); - - given(headerAccessor.getSessionId()).willReturn("test-session-123"); - given(roomChatService.getRoomChatCount(roomId)).willReturn(0); - given(roomChatService.clearRoomChat(roomId, 1L)) - .willThrow(new IllegalArgumentException("존재하지 않는 방입니다")); - - // When - roomChatWebSocketController.clearRoomChat(roomId, request, headerAccessor, testPrincipal); - - // Then - verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); - verify(errorHelper).sendGenericErrorToUser( - eq("test-session-123"), - any(IllegalArgumentException.class), - eq("채팅 삭제 중 오류가 발생했습니다") - ); - } - - @Test - @DisplayName("WebSocket 채팅 전체 삭제 - 메시지 수가 0인 경우") - void t13() { - Long roomId = 3L; - int messageCount = 0; // 메시지가 없는 경우 - - ChatClearRequest request = new ChatClearRequest("모든 채팅을 삭제하겠습니다"); - - ChatClearedNotification.ClearedByDto clearedByInfo = new ChatClearedNotification.ClearedByDto( - 1L, "방장", "https://example.com/host.jpg", "HOST" - ); - - // Mock 설정 - given(roomChatService.getRoomChatCount(roomId)).willReturn(messageCount); - given(roomChatService.clearRoomChat(roomId, 1L)).willReturn(clearedByInfo); - - // When - roomChatWebSocketController.clearRoomChat(roomId, request, headerAccessor, testPrincipal); - - // Then - verify(messagingTemplate).convertAndSend( - eq("/topic/room/" + roomId + "/chat-cleared"), - argThat((ChatClearedNotification notification) -> - notification.deletedCount().equals(0) && - notification.message().contains("방장님이 모든 채팅을 삭제했습니다.") - ) - ); + testAuth = new UsernamePasswordAuthenticationToken(testUserDetails, null, testUserDetails.getAuthorities()); + } + + @Nested + @DisplayName("handleRoomChat 메서드") + class HandleRoomChat { + + @Test + @DisplayName(" - 채팅 메시지를 정상적으로 처리하고 브로드캐스트한다") + void t1() { + // given + Long roomId = 10L; + RoomChatMessageRequest request = new RoomChatMessageRequest("안녕하세요"); + + // Mock User 생성 + User mockUser = User.builder().id(testUserId).build(); + UserProfile mockProfile = UserProfile.builder().nickname("테스터").profileImageUrl("url").build(); + mockUser.setUserProfile(mockProfile); // UserProfile 설정 + + // Mock Room 생성 + Room mockRoom = Room.builder().id(roomId).build(); + + // Mock RoomChatMessage 생성 + RoomChatMessage savedMessage = RoomChatMessage.builder() + .id(100L) + .room(mockRoom) // <-- room 필드 설정 추가! + .user(mockUser) + .content("안녕하세요") + .createdAt(LocalDateTime.now()) + .build(); + + given(roomChatService.saveRoomChatMessage(roomId, testUserId, request)).willReturn(savedMessage); + + // when + roomChatWebSocketController.handleRoomChat(roomId, request, testAuth); + + // then + verify(roomChatService).saveRoomChatMessage(roomId, testUserId, request); + verify(messagingTemplate).convertAndSend(eq("/topic/room/" + roomId), any(RoomChatMessageResponse.class)); + verifyNoInteractions(errorHelper); + } + + @Test + @DisplayName("실패 - 인증 정보가 없을 때 Unauthorized 예외를 던진다") + void t2() { + // given + Long roomId = 10L; + RoomChatMessageRequest request = new RoomChatMessageRequest("안녕하세요"); + + // when & then + CustomException exception = assertThrows(CustomException.class, () -> + roomChatWebSocketController.handleRoomChat(roomId, request, null) + ); + + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.UNAUTHORIZED); + verify(roomChatService, never()).saveRoomChatMessage(any(), any(), any()); + } + + @Test + @DisplayName("실패 - 서비스에서 예외 발생 시 해당 예외를 그대로 던진다") + void t3() { + // given + Long roomId = 10L; + RoomChatMessageRequest request = new RoomChatMessageRequest("안녕하세요"); + + given(roomChatService.saveRoomChatMessage(roomId, testUserId, request)) + .willThrow(new CustomException(ErrorCode.ROOM_NOT_FOUND)); + + // when & then + CustomException exception = assertThrows(CustomException.class, () -> + roomChatWebSocketController.handleRoomChat(roomId, request, testAuth) + ); + + assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.ROOM_NOT_FOUND); + verify(messagingTemplate, never()).convertAndSend(anyString(), any(RoomChatMessageResponse.class)); + } } } \ No newline at end of file diff --git a/src/test/java/com/back/domain/chat/room/service/RoomChatServiceTest.java b/src/test/java/com/back/domain/chat/room/service/RoomChatServiceTest.java index c8bec684..c4290784 100644 --- a/src/test/java/com/back/domain/chat/room/service/RoomChatServiceTest.java +++ b/src/test/java/com/back/domain/chat/room/service/RoomChatServiceTest.java @@ -1,7 +1,8 @@ package com.back.domain.chat.room.service; import com.back.domain.chat.room.dto.ChatClearedNotification; -import com.back.domain.chat.room.dto.RoomChatMessageDto; +import com.back.domain.chat.room.dto.RoomChatMessageRequest; +import com.back.domain.chat.room.dto.RoomChatMessageResponse; import com.back.domain.chat.room.dto.RoomChatPageResponse; import com.back.domain.studyroom.entity.Room; import com.back.domain.studyroom.entity.RoomChatMessage; @@ -17,6 +18,7 @@ import com.back.global.exception.ErrorCode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -39,7 +41,6 @@ import static org.mockito.BDDMockito.*; @ExtendWith(MockitoExtension.class) -@ActiveProfiles("test") @DisplayName("RoomChatService 테스트") class RoomChatServiceTest { @@ -58,531 +59,139 @@ class RoomChatServiceTest { @InjectMocks private RoomChatService roomChatService; - private Room testRoom; private User testUser; - private UserProfile testUserProfile; - private RoomChatMessage testMessage; + private Room testRoom; @BeforeEach - void setUp() throws Exception { - // UserProfile 생성 - testUserProfile = createUserProfile("테스터", "https://example.com/profile.jpg"); - - // User 생성 및 userProfiles 필드 설정 - testUser = User.builder() - .id(1L) - .email("test@example.com") - .username("테스터") - .build(); - - // 리플렉션으로 userProfiles 필드 설정 - setUserProfile(testUser, testUserProfile); - - testRoom = Room.builder() - .id(1L) - .title("테스트 방") - .description("테스트용 스터디룸") - .build(); - - testMessage = RoomChatMessage.builder() - .id(1L) - .room(testRoom) - .user(testUser) - .content("테스트 메시지") - .createdAt(LocalDateTime.now()) - .build(); - } - - // UserProfile 생성 헬퍼 메소드 - private UserProfile createUserProfile(String nickname, String profileImageUrl) throws Exception { - UserProfile userProfile = new UserProfile(); - - // 리플렉션으로 private 필드 설정 - setField(userProfile, "nickname", nickname); - setField(userProfile, "profileImageUrl", profileImageUrl); - setField(userProfile, "user", testUser); - - return userProfile; - } - - // User의 userProfiles 필드 설정 - private void setUserProfile(User user, UserProfile profile) throws Exception { - Field userProfilesField = User.class.getDeclaredField("userProfile"); - userProfilesField.setAccessible(true); - userProfilesField.set(user, profile); - } - - // 리플렉션으로 필드 값 설정하는 헬퍼 메소드 - private void setField(Object target, String fieldName, Object value) throws Exception { - Field field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(target, value); - } - - @Test - @DisplayName("채팅 메시지 저장 성공") - void t1() { - // Given - // createRequest 사용 후 필드 업데이트 - RoomChatMessageDto roomChatMessageDto = RoomChatMessageDto - .createRequest("안녕하세요!", "TEXT") - .withRoomId(1L) - .withUserId(1L); - - given(roomRepository.findById(1L)).willReturn(Optional.of(testRoom)); - given(userRepository.findById(1L)).willReturn(Optional.of(testUser)); - given(roomChatMessageRepository.save(any(RoomChatMessage.class))).willReturn(testMessage); - - // When - RoomChatMessage result = roomChatService.saveRoomChatMessage(roomChatMessageDto); - - // Then - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(1L); - assertThat(result.getContent()).isEqualTo("테스트 메시지"); - - verify(roomRepository).findById(1L); - verify(userRepository).findById(1L); - verify(roomChatMessageRepository).save(any(RoomChatMessage.class)); - } - - @Test - @DisplayName("채팅 메시지 저장 실패 - 존재하지 않는 방") - void t2() { - RoomChatMessageDto roomChatMessageDto = RoomChatMessageDto - .createRequest("메시지", "TEXT") - .withRoomId(999L) - .withUserId(1L); - - given(roomRepository.findById(999L)).willReturn(Optional.empty()); - - assertThatThrownBy(() -> roomChatService.saveRoomChatMessage(roomChatMessageDto)) - .isInstanceOf(CustomException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCode.ROOM_NOT_FOUND); - - verify(roomRepository).findById(999L); - verify(userRepository, never()).findById(any()); - verify(roomChatMessageRepository, never()).save(any()); - } - - @Test - @DisplayName("채팅 메시지 저장 실패 - 존재하지 않는 사용자") - void t3() { - RoomChatMessageDto roomChatMessageDto = RoomChatMessageDto - .createRequest("메시지", "TEXT") - .withRoomId(1L) - .withUserId(999L); - - given(roomRepository.findById(1L)).willReturn(Optional.of(testRoom)); - given(userRepository.findById(999L)).willReturn(Optional.empty()); - - assertThatThrownBy(() -> roomChatService.saveRoomChatMessage(roomChatMessageDto)) - .isInstanceOf(CustomException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCode.USER_NOT_FOUND); - - verify(roomRepository).findById(1L); - verify(userRepository).findById(999L); - verify(roomChatMessageRepository, never()).save(any()); - } - - @Test - @DisplayName("채팅 기록 조회 성공 - before 파라미터 없음") - void t4() { - Long roomId = 1L; - int page = 0; - int size = 10; - LocalDateTime before = null; - - List messages = Arrays.asList(testMessage); - Page messagePage = new PageImpl<>(messages, PageRequest.of(page, size), 1); - - given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); - given(roomChatMessageRepository.findMessagesByRoomId(eq(roomId), any(Pageable.class))) - .willReturn(messagePage); - - RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, page, size, before); - - assertThat(result).isNotNull(); - assertThat(result.content()).hasSize(1); - assertThat(result.totalElements()).isEqualTo(1); - - RoomChatMessageDto messageDto = result.content().get(0); - - assertThat(messageDto.messageId()).isEqualTo(1L); - assertThat(messageDto.roomId()).isEqualTo(1L); - assertThat(messageDto.userId()).isEqualTo(1L); - assertThat(messageDto.nickname()).isEqualTo("테스터"); - assertThat(messageDto.content()).isEqualTo("테스트 메시지"); - assertThat(messageDto.messageType()).isEqualTo("TEXT"); - - verify(roomChatMessageRepository).findMessagesByRoomId(eq(roomId), any(Pageable.class)); - verify(roomChatMessageRepository, never()).findMessagesByRoomIdBefore(any(), any(), any()); - } - - @Test - @DisplayName("채팅 기록 조회 성공 - before 파라미터 있음") - void t5() { - Long roomId = 1L; - int page = 0; - int size = 10; - LocalDateTime before = LocalDateTime.now().minusHours(1); - - List messages = Arrays.asList(testMessage); - Page messagePage = new PageImpl<>(messages, PageRequest.of(page, size), 1); - - given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); - given(roomChatMessageRepository.findMessagesByRoomIdBefore(eq(roomId), eq(before), any(Pageable.class))) - .willReturn(messagePage); - - RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, page, size, before); - - assertThat(result).isNotNull(); - assertThat(result.content()).hasSize(1); - - RoomChatMessageDto messageDto = result.content().get(0); - - assertThat(messageDto.nickname()).isEqualTo("테스터"); - assertThat(messageDto.profileImageUrl()).isEqualTo("https://example.com/profile.jpg"); - - verify(roomChatMessageRepository).findMessagesByRoomIdBefore(eq(roomId), eq(before), any(Pageable.class)); - verify(roomChatMessageRepository, never()).findMessagesByRoomId(any(), any()); - } - - @Test - @DisplayName("채팅 기록 조회 실패 - 존재하지 않는 방") - void t6() { - Long nonExistentRoomId = 999L; - - given(roomRepository.findById(nonExistentRoomId)).willReturn(Optional.empty()); - - assertThatThrownBy(() -> roomChatService.getRoomChatHistory(nonExistentRoomId, 0, 10, null)) - .isInstanceOf(CustomException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCode.ROOM_NOT_FOUND); - - verify(roomRepository).findById(nonExistentRoomId); - verify(roomChatMessageRepository, never()).findMessagesByRoomId(any(), any()); - verify(roomChatMessageRepository, never()).findMessagesByRoomIdBefore(any(), any(), any()); - } - - @Test - @DisplayName("size 최대값 제한 테스트 - 100초과 요청") - void t7() { - Long roomId = 1L; - int page = 0; - int requestedSize = 150; - LocalDateTime before = null; - - List messages = Arrays.asList(testMessage); - Page messagePage = new PageImpl<>(messages, PageRequest.of(page, 100), 1); - - given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); - given(roomChatMessageRepository.findMessagesByRoomId(eq(roomId), any(Pageable.class))) - .willReturn(messagePage); - - RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, page, requestedSize, before); - - assertThat(result).isNotNull(); - - // size가 100으로 제한되었는지 확인 - verify(roomChatMessageRepository).findMessagesByRoomId(eq(roomId), argThat(pageable -> - pageable.getPageSize() == 100 - )); - } - - @Test - @DisplayName("size 기본값 설정 테스트 - 0 요청") - void t8() { - Long roomId = 1L; - int page = 0; - int requestedSize = 0; - LocalDateTime before = null; - - List messages = Arrays.asList(testMessage); - Page messagePage = new PageImpl<>(messages, PageRequest.of(page, 20), 1); - - given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); - given(roomChatMessageRepository.findMessagesByRoomId(eq(roomId), any(Pageable.class))) - .willReturn(messagePage); - - RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, page, requestedSize, before); - - assertThat(result).isNotNull(); - - // size가 기본값 20으로 설정되었는지 확인 - verify(roomChatMessageRepository).findMessagesByRoomId(eq(roomId), argThat(pageable -> - pageable.getPageSize() == 20 - )); - } - - @Test - @DisplayName("size 기본값 설정 테스트 - 음수 요청") - void t9() { - Long roomId = 1L; - int page = 0; - int requestedSize = -5; - LocalDateTime before = null; - - List messages = Arrays.asList(testMessage); - Page messagePage = new PageImpl<>(messages, PageRequest.of(page, 20), 1); - - given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); - given(roomChatMessageRepository.findMessagesByRoomId(eq(roomId), any(Pageable.class))) - .willReturn(messagePage); - - RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, page, requestedSize, before); - - assertThat(result).isNotNull(); - - // size가 기본값 20으로 설정되었는지 확인 - verify(roomChatMessageRepository).findMessagesByRoomId(eq(roomId), argThat(pageable -> - pageable.getPageSize() == 20 - )); - } - - @Test - @DisplayName("size 정상 범위 테스트") - void t10() { - Long roomId = 1L; - int page = 0; - int requestedSize = 50; // 유효한 size - LocalDateTime before = null; - - List messages = Arrays.asList(testMessage); - Page messagePage = new PageImpl<>(messages, PageRequest.of(page, 50), 1); - - given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); - given(roomChatMessageRepository.findMessagesByRoomId(eq(roomId), any(Pageable.class))) - .willReturn(messagePage); - - RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, page, requestedSize, before); - - assertThat(result).isNotNull(); - - // 요청한 size가 그대로 유지되는지 확인 - verify(roomChatMessageRepository).findMessagesByRoomId(eq(roomId), argThat(pageable -> - pageable.getPageSize() == 50 - )); - } - - @Test - @DisplayName("빈 채팅 기록 조회") - void t11() { - Long roomId = 1L; - Page emptyPage = new PageImpl<>(Arrays.asList(), PageRequest.of(0, 10), 0); - - given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); - given(roomChatMessageRepository.findMessagesByRoomId(eq(roomId), any(Pageable.class))) - .willReturn(emptyPage); - - RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, 0, 10, null); - - assertThat(result).isNotNull(); - assertThat(result.content()).isEmpty(); - assertThat(result.totalElements()).isEqualTo(0); - } - - @Test - @DisplayName("convertToDto 메소드 테스트") - void t12() throws Exception { - // ChatService의 private 메소드에 접근하기 위해 리플렉션 사용 - java.lang.reflect.Method convertToDtoMethod = RoomChatService.class.getDeclaredMethod("convertToDto", RoomChatMessage.class); - convertToDtoMethod.setAccessible(true); - - RoomChatMessageDto result = (RoomChatMessageDto) convertToDtoMethod.invoke(roomChatService, testMessage); - - assertThat(result).isNotNull(); - - assertThat(result.messageId()).isEqualTo(1L); - assertThat(result.roomId()).isEqualTo(1L); - assertThat(result.userId()).isEqualTo(1L); - assertThat(result.nickname()).isEqualTo("테스터"); - assertThat(result.profileImageUrl()).isEqualTo("https://example.com/profile.jpg"); - assertThat(result.content()).isEqualTo("테스트 메시지"); - assertThat(result.messageType()).isEqualTo("TEXT"); - assertThat(result.attachment()).isNull(); - assertThat(result.createdAt()).isNotNull(); - } - - // ==================== 채팅 전체 삭제 기능 테스트 ==================== - - @Test - @DisplayName("채팅 전체 삭제 성공 - 방장 권한") - void t13() { - Long roomId = 1L; - Long userId = 1L; - int deletedCount = 15; - - // RoomMember 생성 (방장) - RoomMember hostMember = RoomMember.createHost(testRoom, testUser); - - // Mock 설정 - given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); - given(userRepository.findById(userId)).willReturn(Optional.of(testUser)); - given(roomMemberRepository.findByRoomIdAndUserId(roomId, userId)).willReturn(Optional.of(hostMember)); - given(roomChatMessageRepository.deleteAllByRoomId(roomId)).willReturn(deletedCount); - - // When - ChatClearedNotification.ClearedByDto result = roomChatService.clearRoomChat(roomId, userId); - - // Then - assertThat(result).isNotNull(); - assertThat(result.userId()).isEqualTo(userId); - assertThat(result.nickname()).isEqualTo("테스터"); - assertThat(result.profileImageUrl()).isEqualTo("https://example.com/profile.jpg"); - assertThat(result.role()).isEqualTo("HOST"); - - verify(roomRepository).findById(roomId); - verify(userRepository).findById(userId); - verify(roomMemberRepository).findByRoomIdAndUserId(roomId, userId); - verify(roomChatMessageRepository).deleteAllByRoomId(roomId); - } - - @Test - @DisplayName("채팅 전체 삭제 성공 - 부방장 권한") - void t14() { - Long roomId = 1L; - Long userId = 1L; - int deletedCount = 8; - - // RoomMember 생성 (부방장) - RoomMember subHostMember = RoomMember.create(testRoom, testUser, RoomRole.SUB_HOST); - - // Mock 설정 - given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); - given(userRepository.findById(userId)).willReturn(Optional.of(testUser)); - given(roomMemberRepository.findByRoomIdAndUserId(roomId, userId)).willReturn(Optional.of(subHostMember)); - given(roomChatMessageRepository.deleteAllByRoomId(roomId)).willReturn(deletedCount); - - // When - ChatClearedNotification.ClearedByDto result = roomChatService.clearRoomChat(roomId, userId); - - // Then - assertThat(result.role()).isEqualTo("SUB_HOST"); - verify(roomChatMessageRepository).deleteAllByRoomId(roomId); - } - - @Test - @DisplayName("채팅 전체 삭제 실패 - 존재하지 않는 방") - void t15() { - Long nonExistentRoomId = 999L; - Long userId = 1L; - - given(roomRepository.findById(nonExistentRoomId)).willReturn(Optional.empty()); - - assertThatThrownBy(() -> roomChatService.clearRoomChat(nonExistentRoomId, userId)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("존재하지 않는 방입니다"); - - verify(roomRepository).findById(nonExistentRoomId); - verify(userRepository, never()).findById(any()); - verify(roomMemberRepository, never()).findByRoomIdAndUserId(any(), any()); - verify(roomChatMessageRepository, never()).deleteAllByRoomId(any()); - } - - @Test - @DisplayName("채팅 전체 삭제 실패 - 존재하지 않는 사용자") - void t16() { - Long roomId = 1L; - Long nonExistentUserId = 999L; - - given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); - given(userRepository.findById(nonExistentUserId)).willReturn(Optional.empty()); - - assertThatThrownBy(() -> roomChatService.clearRoomChat(roomId, nonExistentUserId)) - .isInstanceOf(CustomException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCode.USER_NOT_FOUND); - - verify(roomRepository).findById(roomId); - verify(userRepository).findById(nonExistentUserId); - verify(roomMemberRepository, never()).findByRoomIdAndUserId(any(), any()); - verify(roomChatMessageRepository, never()).deleteAllByRoomId(any()); - } - - @Test - @DisplayName("채팅 전체 삭제 실패 - 방 멤버가 아님") - void t17() { - Long roomId = 1L; - Long userId = 1L; - - given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); - given(userRepository.findById(userId)).willReturn(Optional.of(testUser)); - given(roomMemberRepository.findByRoomIdAndUserId(roomId, userId)).willReturn(Optional.empty()); - - assertThatThrownBy(() -> roomChatService.clearRoomChat(roomId, userId)) - .isInstanceOf(CustomException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCode.NOT_ROOM_MEMBER); - - verify(roomMemberRepository).findByRoomIdAndUserId(roomId, userId); - verify(roomChatMessageRepository, never()).deleteAllByRoomId(any()); - } - - @Test - @DisplayName("채팅 전체 삭제 실패 - 권한 없음 (일반 멤버)") - void t18() { - Long roomId = 1L; - Long userId = 1L; - - // RoomMember 생성 (일반 멤버) - RoomMember memberMember = RoomMember.createMember(testRoom, testUser); - - given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); - given(userRepository.findById(userId)).willReturn(Optional.of(testUser)); - given(roomMemberRepository.findByRoomIdAndUserId(roomId, userId)).willReturn(Optional.of(memberMember)); - - assertThatThrownBy(() -> roomChatService.clearRoomChat(roomId, userId)) - .isInstanceOf(SecurityException.class) - .hasMessage("채팅 삭제 권한이 없습니다"); - - verify(roomMemberRepository).findByRoomIdAndUserId(roomId, userId); - verify(roomChatMessageRepository, never()).deleteAllByRoomId(any()); - } - - @Test - @DisplayName("채팅 전체 삭제 실패 - DB 삭제 오류") - void t19() { - Long roomId = 1L; - Long userId = 1L; - - RoomMember hostMember = RoomMember.createHost(testRoom, testUser); - - given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); - given(userRepository.findById(userId)).willReturn(Optional.of(testUser)); - given(roomMemberRepository.findByRoomIdAndUserId(roomId, userId)).willReturn(Optional.of(hostMember)); - given(roomChatMessageRepository.deleteAllByRoomId(roomId)) - .willThrow(new RuntimeException("DB 연결 오류")); - - assertThatThrownBy(() -> roomChatService.clearRoomChat(roomId, userId)) - .isInstanceOf(CustomException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCode.CHAT_DELETE_FAILED); - - verify(roomChatMessageRepository).deleteAllByRoomId(roomId); - } - - @Test - @DisplayName("방의 채팅 메시지 수 조회 성공") - void t20() { - Long roomId = 1L; - int expectedCount = 42; - - given(roomChatMessageRepository.countByRoomId(roomId)).willReturn(expectedCount); - - int result = roomChatService.getRoomChatCount(roomId); - - assertThat(result).isEqualTo(expectedCount); - verify(roomChatMessageRepository).countByRoomId(roomId); - } - - @Test - @DisplayName("방의 채팅 메시지 수 조회 - 메시지가 없는 경우") - void t21() { - Long roomId = 2L; - int expectedCount = 0; - - given(roomChatMessageRepository.countByRoomId(roomId)).willReturn(expectedCount); - - int result = roomChatService.getRoomChatCount(roomId); - - assertThat(result).isEqualTo(0); - verify(roomChatMessageRepository).countByRoomId(roomId); + void setUp() { + testUser = User.builder().id(1L).build(); + UserProfile testProfile = UserProfile.builder().nickname("테스터").profileImageUrl("url").build(); + testUser.setUserProfile(testProfile); + + testRoom = Room.builder().id(1L).build(); + } + + @Nested + @DisplayName("saveRoomChatMessage 메서드") + class SaveRoomChatMessage { + + @Test + @DisplayName("성공 - 채팅 메시지를 저장한다") + void t1() { + // given + Long roomId = 1L; + Long userId = 1L; + RoomChatMessageRequest request = new RoomChatMessageRequest("안녕하세요"); + + RoomChatMessage mockMessage = new RoomChatMessage(testRoom, testUser, request.content()); + + given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); + given(userRepository.findById(userId)).willReturn(Optional.of(testUser)); + given(roomChatMessageRepository.save(any(RoomChatMessage.class))).willReturn(mockMessage); + + // when + RoomChatMessage result = roomChatService.saveRoomChatMessage(roomId, userId, request); + + // then + assertThat(result.getContent()).isEqualTo("안녕하세요"); + assertThat(result.getUser().getId()).isEqualTo(userId); + assertThat(result.getRoom().getId()).isEqualTo(roomId); + } + + @Test + @DisplayName("실패 - 존재하지 않는 방이면 CustomException 발생") + void t2() { + // given + Long nonExistentRoomId = 999L; + given(roomRepository.findById(nonExistentRoomId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> roomChatService.saveRoomChatMessage(nonExistentRoomId, 1L, new RoomChatMessageRequest("..."))) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.ROOM_NOT_FOUND); + } + } + + @Nested + @DisplayName("getRoomChatHistory 메서드") + class GetRoomChatHistory { + + @Test + @DisplayName("성공 - 채팅 기록을 페이지로 조회한다") + void t1() { + // given + Long roomId = 1L; + RoomChatMessage message = new RoomChatMessage(testRoom, testUser, "테스트 메시지"); + Page messagePage = new PageImpl<>(List.of(message)); + + given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); + given(roomChatMessageRepository.findMessagesByRoomId(eq(roomId), any(Pageable.class))).willReturn(messagePage); + + // when + RoomChatPageResponse result = roomChatService.getRoomChatHistory(roomId, 0, 10, null); + + // then + assertThat(result.content()).hasSize(1); + RoomChatMessageResponse responseDto = result.content().get(0); + assertThat(responseDto.content()).isEqualTo("테스트 메시지"); + assertThat(responseDto.nickname()).isEqualTo("테스터"); + } + } + + @Nested + @DisplayName("clearRoomChat 메서드") + class ClearRoomChat { + + @Test + @DisplayName("성공 - 방장 권한으로 채팅을 전체 삭제한다") + void t1() { + // given + Long roomId = 1L; + Long userId = 1L; + RoomMember hostMember = RoomMember.createHost(testRoom, testUser); + + given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); + given(userRepository.findById(userId)).willReturn(Optional.of(testUser)); + given(roomMemberRepository.findByRoomIdAndUserId(roomId, userId)).willReturn(Optional.of(hostMember)); + + // when + ChatClearedNotification.ClearedByDto result = roomChatService.clearRoomChat(roomId, userId); + + // then + assertThat(result.userId()).isEqualTo(userId); + assertThat(result.role()).isEqualTo("HOST"); + verify(roomChatMessageRepository).deleteAllByRoomId(roomId); + } + + @Test + @DisplayName("실패 - 권한이 없는 경우(일반 멤버) CustomException 발생") + void t2() { + // given + Long roomId = 1L; + Long userId = 1L; + RoomMember member = RoomMember.createMember(testRoom, testUser); + + given(roomRepository.findById(roomId)).willReturn(Optional.of(testRoom)); + given(userRepository.findById(userId)).willReturn(Optional.of(testUser)); + given(roomMemberRepository.findByRoomIdAndUserId(roomId, userId)).willReturn(Optional.of(member)); + + // when & then + assertThatThrownBy(() -> roomChatService.clearRoomChat(roomId, userId)) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.CHAT_DELETE_FORBIDDEN); + } + + @Test + @DisplayName("실패 - 존재하지 않는 방이면 CustomException 발생") + void t3() { + // given + Long nonExistentRoomId = 999L; + given(roomRepository.findById(nonExistentRoomId)).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> roomChatService.clearRoomChat(nonExistentRoomId, 1L)) + .isInstanceOf(CustomException.class) + .hasFieldOrPropertyWithValue("errorCode", ErrorCode.ROOM_NOT_FOUND); + } } } \ No newline at end of file