diff --git a/src/main/java/com/example/mate/domain/goodsChat/dto/request/GoodsChatMessageRequest.java b/src/main/java/com/example/mate/domain/goodsChat/dto/request/GoodsChatMessageRequest.java index 84db625f..b018eede 100644 --- a/src/main/java/com/example/mate/domain/goodsChat/dto/request/GoodsChatMessageRequest.java +++ b/src/main/java/com/example/mate/domain/goodsChat/dto/request/GoodsChatMessageRequest.java @@ -3,9 +3,11 @@ import com.example.mate.domain.constant.MessageType; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.Getter; @Getter +@AllArgsConstructor public class GoodsChatMessageRequest { @NotNull(message = "채팅방 ID는 필수 입력 값입니다.") diff --git a/src/main/java/com/example/mate/domain/goodsChat/entity/GoodsChatMessage.java b/src/main/java/com/example/mate/domain/goodsChat/entity/GoodsChatMessage.java index 6036e463..b68ed7ab 100644 --- a/src/main/java/com/example/mate/domain/goodsChat/entity/GoodsChatMessage.java +++ b/src/main/java/com/example/mate/domain/goodsChat/entity/GoodsChatMessage.java @@ -20,6 +20,8 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; @Entity @Table(name = "goods_chat_message") @@ -38,6 +40,7 @@ public class GoodsChatMessage { @JoinColumn(name = "member_id", referencedColumnName = "member_id"), @JoinColumn(name = "chat_room_id", referencedColumnName = "chat_room_id") }) + @OnDelete(action = OnDeleteAction.CASCADE) private GoodsChatPart goodsChatPart; @Column(name = "content", nullable = false, columnDefinition = "TEXT") diff --git a/src/main/java/com/example/mate/domain/goodsChat/service/GoodsChatMessageService.java b/src/main/java/com/example/mate/domain/goodsChat/service/GoodsChatMessageService.java index 988e317f..cabb3dc9 100644 --- a/src/main/java/com/example/mate/domain/goodsChat/service/GoodsChatMessageService.java +++ b/src/main/java/com/example/mate/domain/goodsChat/service/GoodsChatMessageService.java @@ -41,7 +41,7 @@ public void sendMessage(GoodsChatMessageRequest message) { // DB에 메시지 저장 GoodsChatMessage chatMessage - = messageRepository.save(createChatMessage(message.getMessage(), chatPart, MessageType.TALK)); + = messageRepository.save(createChatMessage(message.getMessage(), chatPart, message.getType())); chatRoom.updateLastChat(chatMessage.getContent(), chatMessage.getSentAt()); GoodsChatMessageResponse response = GoodsChatMessageResponse.of(chatMessage); diff --git a/src/test/java/com/example/mate/domain/goodsChat/controller/GoodsChatRoomControllerTest.java b/src/test/java/com/example/mate/domain/goodsChat/controller/GoodsChatRoomControllerTest.java index d99dbc37..876c5c83 100644 --- a/src/test/java/com/example/mate/domain/goodsChat/controller/GoodsChatRoomControllerTest.java +++ b/src/test/java/com/example/mate/domain/goodsChat/controller/GoodsChatRoomControllerTest.java @@ -1,18 +1,24 @@ package com.example.mate.domain.goodsChat.controller; +import static org.mockito.BDDMockito.willDoNothing; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.example.mate.common.error.CustomException; +import com.example.mate.common.error.ErrorCode; import com.example.mate.common.response.PageResponse; +import com.example.mate.common.security.util.JwtUtil; import com.example.mate.config.WithAuthMember; import com.example.mate.domain.goodsChat.dto.response.GoodsChatMessageResponse; -import com.example.mate.common.security.util.JwtUtil; import com.example.mate.domain.goodsChat.dto.response.GoodsChatRoomResponse; +import com.example.mate.domain.goodsChat.dto.response.GoodsChatRoomSummaryResponse; import com.example.mate.domain.goodsChat.service.GoodsChatService; +import com.example.mate.domain.member.dto.response.MemberSummaryResponse; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -23,6 +29,7 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext; import org.springframework.test.web.servlet.MockMvc; @@ -164,4 +171,215 @@ void getMessagesForChatRoom_should_return_messages() throws Exception { verify(goodsChatService).getMessagesForChatRoom(chatRoomId, memberId, pageable); } + @Test + @DisplayName("굿즈거래 채팅방 상세 조회 성공") + void getGoodsChatRoomInfo_should_return_chatroom_info_and_latest_message() throws Exception { + // given + Long chatRoomId = 1L; + Long memberId = 1L; + Long goodsPostId = 1L; + + GoodsChatMessageResponse firstMessage = GoodsChatMessageResponse.builder() + .chatMessageId(1L) + .message("first message") + .senderId(memberId) + .sentAt(LocalDateTime.now().minusMinutes(10)) + .build(); + + GoodsChatMessageResponse secondMessage = GoodsChatMessageResponse.builder() + .chatMessageId(2L) + .message("second message") + .senderId(memberId) + .sentAt(LocalDateTime.now()) + .build(); + + List message = List.of(secondMessage, firstMessage); + + GoodsChatRoomResponse existingChatRoomResponse = GoodsChatRoomResponse.builder() + .chatRoomId(1L) + .goodsPostId(goodsPostId) + .teamName("test team") + .title("test title") + .category("ACCESSORY") + .price(10000) + .postStatus("OPEN") + .imageUrl("/images/test.jpg") + .initialMessages(PageResponse.from(new PageImpl<>(List.of(message)), message)) + .build(); + + when(goodsChatService.getGoodsChatRoomInfo(memberId, chatRoomId)).thenReturn(existingChatRoomResponse); + + mockMvc.perform(get("/api/goods/chat/{chatRoomId}", chatRoomId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("SUCCESS")) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.chatRoomId").value(chatRoomId)) + .andExpect(jsonPath("$.data.goodsPostId").value(goodsPostId)) + .andExpect(jsonPath("$.data.initialMessages.content").isArray()) + .andExpect(jsonPath("$.data.initialMessages.content[0].chatMessageId").value(secondMessage.getChatMessageId())) + .andExpect(jsonPath("$.data.initialMessages.content[0].message").value(secondMessage.getMessage())) + .andExpect(jsonPath("$.data.initialMessages.content[1].chatMessageId").value(firstMessage.getChatMessageId())) + .andExpect(jsonPath("$.data.initialMessages.content[1].message").value(firstMessage.getMessage())); + } + + @Test + @DisplayName("굿즈거래 채팅방 목록 조회 성공") + void getGoodsChatRooms_should_return_chat_room_list() throws Exception { + // given + Long memberId = 1L; + Pageable pageable = PageRequest.of(0, 10); + + GoodsChatRoomSummaryResponse chatRoom = GoodsChatRoomSummaryResponse.builder() + .chatRoomId(1L) + .opponentNickname("Opponent1") + .lastChatContent("First message") + .lastChatSentAt(LocalDateTime.now().minusMinutes(10)) + .placeName("Test Place") + .goodsMainImageUrl("/images/goods1.jpg") + .opponentImageUrl("/images/opponent1.jpg") + .build(); + + PageResponse pageResponse = PageResponse.from( + new PageImpl<>(List.of(chatRoom), pageable, 1), List.of(chatRoom)); + + when(goodsChatService.getGoodsChatRooms(memberId, pageable)).thenReturn(pageResponse); + + // when & then + mockMvc.perform(get("/api/goods/chat") + .param("page", "0") + .param("size", "10")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("SUCCESS")) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content[0].chatRoomId").value(chatRoom.getChatRoomId())) + .andExpect(jsonPath("$.data.content[0].opponentNickname").value(chatRoom.getOpponentNickname())) + .andExpect(jsonPath("$.data.content[0].lastChatContent").value(chatRoom.getLastChatContent())) + .andExpect(jsonPath("$.data.content[0].lastChatSentAt").isNotEmpty()) + .andExpect(jsonPath("$.data.content[0].placeName").value(chatRoom.getPlaceName())) + .andExpect(jsonPath("$.data.content[0].goodsMainImageUrl").value(chatRoom.getGoodsMainImageUrl())) + .andExpect(jsonPath("$.data.content[0].opponentImageUrl").value(chatRoom.getOpponentImageUrl())); + + verify(goodsChatService).getGoodsChatRooms(memberId, pageable); + } + + @Test + @DisplayName("굿즈거래 채팅방 메시지 조회 성공") + void getGoodsChatRoomMessages_should_return_messages() throws Exception { + // given + Long chatRoomId = 1L; + Long memberId = 1L; + PageRequest pageable = PageRequest.of(0, 20); + + GoodsChatMessageResponse firstMessage = GoodsChatMessageResponse.builder() + .chatMessageId(1L) + .message("First message") + .senderId(memberId) + .sentAt(LocalDateTime.now().minusMinutes(10)) + .build(); + + GoodsChatMessageResponse secondMessage = GoodsChatMessageResponse.builder() + .chatMessageId(2L) + .message("Second message") + .senderId(memberId) + .sentAt(LocalDateTime.now()) + .build(); + + PageResponse pageResponse = PageResponse.from( + new PageImpl<>(List.of(secondMessage, firstMessage), pageable, 2), + List.of(secondMessage, firstMessage) + ); + + when(goodsChatService.getMessagesForChatRoom(chatRoomId, memberId, pageable)).thenReturn(pageResponse); + + // when & then + mockMvc.perform(get("/api/goods/chat/{chatRoomId}/message", chatRoomId) + .param("page", "0") + .param("size", "20")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("SUCCESS")) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content[0].chatMessageId").value(secondMessage.getChatMessageId())) + .andExpect(jsonPath("$.data.content[0].message").value(secondMessage.getMessage())) + .andExpect(jsonPath("$.data.content[1].chatMessageId").value(firstMessage.getChatMessageId())) + .andExpect(jsonPath("$.data.content[1].message").value(firstMessage.getMessage())); + + verify(goodsChatService).getMessagesForChatRoom(chatRoomId, memberId, pageable); + } + + @Test + @DisplayName("굿즈거래 채팅방 나가기 성공") + void leaveGoodsChatRoom_should_deactivate_chat_part_and_return_no_content() throws Exception { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + + willDoNothing().given(goodsChatService).deactivateGoodsChatPart(memberId, chatRoomId); + + // when & then + mockMvc.perform(delete("/api/goods/chat/{chatRoomId}", chatRoomId)) + .andExpect(status().isNoContent()); + + verify(goodsChatService).deactivateGoodsChatPart(memberId, chatRoomId); + } + + @Test + @DisplayName("굿즈거래 채팅방 참여자 목록 조회 성공") + void getGoodsChatRoomMembers_should_return_list_of_chat_members() throws Exception { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + + MemberSummaryResponse member = MemberSummaryResponse.builder() + .memberId(memberId) + .nickname("member1") + .imageUrl("/images/member1.jpg") + .build(); + + MemberSummaryResponse anotherMember = MemberSummaryResponse.builder() + .memberId(2L) + .nickname("member2") + .imageUrl("/images/member2.jpg") + .build(); + + List memberList = List.of(member, anotherMember); + + when(goodsChatService.getChatRoomMembers(memberId, chatRoomId)).thenReturn(memberList); + + // when & then + mockMvc.perform(get("/api/goods/chat/{chatRoomId}/members", chatRoomId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("SUCCESS")) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").isArray()) + .andExpect(jsonPath("$.data[0].memberId").value(member.getMemberId())) + .andExpect(jsonPath("$.data[0].nickname").value(member.getNickname())) + .andExpect(jsonPath("$.data[0].imageUrl").value(member.getImageUrl())) + .andExpect(jsonPath("$.data[1].memberId").value(anotherMember.getMemberId())) + .andExpect(jsonPath("$.data[1].nickname").value(anotherMember.getNickname())) + .andExpect(jsonPath("$.data[1].imageUrl").value(anotherMember.getImageUrl())); + + verify(goodsChatService).getChatRoomMembers(memberId, chatRoomId); + } + + @Test + @DisplayName("굿즈거래 채팅방 참여자 목록 조회 실패 - 참여하지 않은 사용자가 조회할 경우 예외 발생") + void getGoodsChatRoomMembers_should_throw_exception_when_user_is_not_a_member() throws Exception { + // given + Long memberId = 1L; + Long chatRoomId = 2L; + + when(goodsChatService.getChatRoomMembers(memberId, chatRoomId)) + .thenThrow(new CustomException(ErrorCode.GOODS_CHAT_NOT_FOUND_CHAT_PART)); + + // when & then + mockMvc.perform(get("/api/goods/chat/{chatRoomId}/members", chatRoomId)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.status").value("ERROR")) + .andExpect(jsonPath("$.code").value(ErrorCode.GOODS_CHAT_NOT_FOUND_CHAT_PART.getStatus().value())) + .andExpect(jsonPath("$.message").value(ErrorCode.GOODS_CHAT_NOT_FOUND_CHAT_PART.getMessage())); + + verify(goodsChatService).getChatRoomMembers(memberId, chatRoomId); + } } \ No newline at end of file diff --git a/src/test/java/com/example/mate/domain/goodsChat/integration/GoodsChatIntegrationTest.java b/src/test/java/com/example/mate/domain/goodsChat/integration/GoodsChatIntegrationTest.java index dcc50f3f..b8b922f8 100644 --- a/src/test/java/com/example/mate/domain/goodsChat/integration/GoodsChatIntegrationTest.java +++ b/src/test/java/com/example/mate/domain/goodsChat/integration/GoodsChatIntegrationTest.java @@ -1,12 +1,15 @@ package com.example.mate.domain.goodsChat.integration; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.example.mate.common.response.ApiResponse; +import com.example.mate.common.response.PageResponse; import com.example.mate.common.security.util.JwtUtil; import com.example.mate.config.WithAuthMember; import com.example.mate.domain.constant.Gender; @@ -18,12 +21,16 @@ import com.example.mate.domain.goods.entity.Role; import com.example.mate.domain.goods.entity.Status; import com.example.mate.domain.goods.repository.GoodsPostRepository; +import com.example.mate.domain.goodsChat.dto.response.GoodsChatMessageResponse; import com.example.mate.domain.goodsChat.dto.response.GoodsChatRoomResponse; import com.example.mate.domain.goodsChat.entity.GoodsChatMessage; import com.example.mate.domain.goodsChat.entity.GoodsChatPart; +import com.example.mate.domain.goodsChat.entity.GoodsChatPartId; import com.example.mate.domain.goodsChat.entity.GoodsChatRoom; import com.example.mate.domain.goodsChat.repository.GoodsChatMessageRepository; +import com.example.mate.domain.goodsChat.repository.GoodsChatPartRepository; import com.example.mate.domain.goodsChat.repository.GoodsChatRoomRepository; +import com.example.mate.domain.member.dto.response.MemberSummaryResponse; import com.example.mate.domain.member.entity.Member; import com.example.mate.domain.member.repository.MemberRepository; import com.fasterxml.jackson.core.type.TypeReference; @@ -31,6 +38,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; 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; @@ -38,6 +46,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.jdbc.core.JdbcTemplate; @@ -54,6 +63,7 @@ public class GoodsChatIntegrationTest { @Autowired private MemberRepository memberRepository; @Autowired private GoodsPostRepository goodsPostRepository; @Autowired private GoodsChatRoomRepository chatRoomRepository; + @Autowired private GoodsChatPartRepository chatPartRepository; @Autowired private GoodsChatMessageRepository messageRepository; @Autowired private ObjectMapper objectMapper; @Autowired private JdbcTemplate jdbcTemplate; @@ -150,6 +160,197 @@ void get_messages_for_chat_room() throws Exception { .getResponse(); } + @Test + @DisplayName("굿즈거래 채팅방 상세 조회 테스트") + @WithAuthMember(memberId = 2L) + void get_goods_chat_room_info() throws Exception { + // given + Long chatRoomId = chatRoom.getId(); + Long memberId = buyer.getId(); + + // when + MockHttpServletResponse result = mockMvc.perform(get("/api/goods/chat/{chatRoomId}", chatRoomId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("SUCCESS")) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.initialMessages.content").isArray()) + .andExpect(jsonPath("$.data.initialMessages.content[0].message").value("test message")) + .andExpect(jsonPath("$.data.initialMessages.content[1].message").value("test message")) + .andReturn() + .getResponse(); + + result.setCharacterEncoding("UTF-8"); + ApiResponse apiResponse = objectMapper.readValue(result.getContentAsString(), new TypeReference<>() {}); + GoodsChatRoomResponse response = apiResponse.getData(); + + // then + GoodsChatRoom actualChatRoom = chatRoomRepository.findById(chatRoomId).orElse(null); + GoodsPost actualPost = actualChatRoom.getGoodsPost(); + + assertThat(response.getChatRoomId()).isEqualTo(chatRoomId); + assertThat(response.getGoodsPostId()).isEqualTo(actualPost.getId()); + assertThat(response.getTitle()).isEqualTo(actualPost.getTitle()); + assertThat(response.getPrice()).isEqualTo(actualPost.getPrice()); + assertThat(response.getPostStatus()).isEqualTo(actualPost.getStatus().getValue()); + + GoodsPostImage image = actualPost.getGoodsPostImages().get(0); + assertThat(image.getImageUrl()).isEqualTo("upload/test_img_url"); + } + + @Test + @DisplayName("굿즈거래 채팅방 목록 조회 성공 통합 테스트") + @WithAuthMember(memberId = 2L) + void getGoodsChatRooms_should_return_chat_room_list_integration_test() throws Exception { + // given + Long memberId = buyer.getId(); + Pageable pageable = PageRequest.of(0, 10); + + // when & then + mockMvc.perform(get("/api/goods/chat") + .param("page", String.valueOf(pageable.getPageNumber())) + .param("size", String.valueOf(pageable.getPageSize()))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("SUCCESS")) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.content").isArray()) + .andExpect(jsonPath("$.data.content[0].chatRoomId").value(chatRoom.getId())) + .andExpect(jsonPath("$.data.content[0].opponentNickname").value(seller.getNickname())) + .andExpect(jsonPath("$.data.content[0].lastChatContent").value(chatRoom.getLastChatContent())) + .andExpect(jsonPath("$.data.content[0].placeName").value(goodsPost.getLocation().getPlaceName())) + .andExpect(jsonPath("$.data.content[0].goodsMainImageUrl").value(goodsPost.getMainImageUrl())) + .andExpect(jsonPath("$.data.content[0].opponentImageUrl").value(seller.getImageUrl())) + .andReturn() + .getResponse(); + } + + @Test + @DisplayName("굿즈거래 채팅방 메시지 조회 성공 통합 테스트") + @WithAuthMember(memberId = 2L) + void getGoodsChatRoomMessages_integration_test() throws Exception { + // given + Long chatRoomId = chatRoom.getId(); + Long buyerId = buyer.getId(); + Pageable pageable = PageRequest.of(0, 20); + + // when & then + MockHttpServletResponse response = mockMvc.perform(get("/api/goods/chat/{chatRoomId}/message", chatRoomId) + .param("page", String.valueOf(pageable.getPageNumber())) + .param("size", String.valueOf(pageable.getPageSize()))) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("SUCCESS")) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data.content").isArray()) + .andReturn() + .getResponse(); + + response.setCharacterEncoding("UTF-8"); + ApiResponse> apiResponse = objectMapper.readValue(response.getContentAsString(), new TypeReference<>() {}); + List expectMessages = apiResponse.getData().getContent(); + + // 실제 데이터 조회 + Page chatMessages = messageRepository.getChatMessages(chatRoomId, pageable); + List actualMessages = chatMessages.getContent(); + + assertThat(expectMessages.size()).isEqualTo(actualMessages.size()); + + for (int i = 0; i < expectMessages.size(); i++) { + GoodsChatMessageResponse expectedMessage = expectMessages.get(i); + GoodsChatMessage actualMessage = actualMessages.get(i); + + assertThat(expectedMessage.getChatMessageId()).isEqualTo(actualMessage.getId()); + assertThat(expectedMessage.getRoomId()).isEqualTo(actualMessage.getGoodsChatPart().getGoodsChatRoom().getId()); + assertThat(expectedMessage.getSenderId()).isEqualTo(actualMessage.getGoodsChatPart().getMember().getId()); + assertThat(expectedMessage.getSenderNickname()).isEqualTo(actualMessage.getGoodsChatPart().getMember().getNickname()); + assertThat(expectedMessage.getMessage()).isEqualTo(actualMessage.getContent()); + assertThat(expectedMessage.getMessageType()).isEqualTo(actualMessage.getMessageType().getValue()); + assertThat(expectedMessage.getSenderImageUrl()).isEqualTo(actualMessage.getGoodsChatPart().getMember().getImageUrl()); + assertThat(expectedMessage.getSentAt()).isEqualToIgnoringNanos(actualMessage.getSentAt()); // 시간 비교, 나노초는 무시 + } + } + + @Test + @DisplayName("굿즈거래 채팅방 나가기 통합 테스트 - 채팅방에 한명이 남을 경우 채팅방과 당사자는 비활성화 된다.") + @WithAuthMember(memberId = 2L) + void leaveGoodsChatRoom_integration_test_single_member_left() throws Exception { + // given + Long chatRoomId = chatRoom.getId(); + Long memberId = buyer.getId(); + + // when + mockMvc.perform(delete("/api/goods/chat/{chatRoomId}", chatRoomId)).andExpect(status().isNoContent()); + + // then + GoodsChatRoom goodsChatRoom = chatRoomRepository.findById(chatRoomId).orElseThrow(); + assertThat(goodsChatRoom.isRoomActive()).isFalse(); + + GoodsChatPart goodsChatPart = chatPartRepository.findById(new GoodsChatPartId(memberId, chatRoomId)).orElseThrow(); + assertThat(goodsChatPart.getIsActive()).isFalse(); + } + + @Test + @DisplayName("굿즈거래 채팅방 나가기 통합 테스트 - 채팅방에 아무도 남지 않을 경우 채팅방과 채팅참여, 채팅은 모두 삭제된다.") + @WithAuthMember(memberId = 2L) + void leaveGoodsChatRoom_integration_test_no_participants_left() throws Exception { + // given + Long chatRoomId = chatRoom.getId(); + Long memberId = buyer.getId(); + + GoodsChatPart goodsChatPart = chatRoom.getChatParts().get(1); + goodsChatPart.leaveAndCheckRoomStatus(); + + chatRoomRepository.saveAndFlush(chatRoom); + + // when + mockMvc.perform(delete("/api/goods/chat/{chatRoomId}", chatRoomId)).andExpect(status().isNoContent()); + + // then + Optional goodsChatRoom = chatRoomRepository.findById(chatRoomId); + assertThat(goodsChatRoom).isEmpty(); + + List existingMember = chatPartRepository.findAllWithMemberByChatRoomId(chatRoomId); + assertThat(existingMember).isEmpty(); + } + + @Test + @DisplayName("굿즈거래 채팅방 참여자 목록 조회 성공 통합 테스트") + @WithAuthMember(memberId = 2L) + void getGoodsChatRoomMembers_integration_test() throws Exception { + // given + Long chatRoomId = chatRoom.getId(); + Long memberId = buyer.getId(); + + // when + MockHttpServletResponse response = mockMvc.perform(get("/api/goods/chat/{chatRoomId}/members", chatRoomId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("SUCCESS")) + .andExpect(jsonPath("$.code").value(200)) + .andExpect(jsonPath("$.data").isArray()) + .andReturn() + .getResponse(); + + response.setCharacterEncoding("UTF-8"); + + // then + ApiResponse> apiResponse = objectMapper.readValue(response.getContentAsString(), new TypeReference<>() {}); + List actualMembers = apiResponse.getData(); + + // 실제 데이터 조회 + List chatParts = chatPartRepository.findAllWithMemberByChatRoomId(chatRoomId); + List expectedMembers = chatParts.stream().map(GoodsChatPart::getMember).toList(); + + assertThat(actualMembers.size()).isEqualTo(expectedMembers.size()); + + for (int i = 0; i < actualMembers.size(); i++) { + MemberSummaryResponse actualMember = actualMembers.get(i); + Member expectedMember = expectedMembers.get(i); + + assertThat(actualMember.getMemberId()).isEqualTo(expectedMember.getId()); + assertThat(actualMember.getNickname()).isEqualTo(expectedMember.getNickname()); + assertThat(actualMember.getImageUrl()).isEqualTo(expectedMember.getImageUrl()); + } + } + private Member createMember(String name, String nickname, String email) { return memberRepository.save(Member.builder() .name(name) diff --git a/src/test/java/com/example/mate/domain/goodsChat/service/GoodsChatMessageServiceTest.java b/src/test/java/com/example/mate/domain/goodsChat/service/GoodsChatMessageServiceTest.java new file mode 100644 index 00000000..eb59a065 --- /dev/null +++ b/src/test/java/com/example/mate/domain/goodsChat/service/GoodsChatMessageServiceTest.java @@ -0,0 +1,311 @@ +package com.example.mate.domain.goodsChat.service; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.example.mate.common.error.CustomException; +import com.example.mate.common.error.ErrorCode; +import com.example.mate.domain.constant.MessageType; +import com.example.mate.domain.goodsChat.dto.request.GoodsChatMessageRequest; +import com.example.mate.domain.goodsChat.dto.response.GoodsChatMessageResponse; +import com.example.mate.domain.goodsChat.entity.GoodsChatMessage; +import com.example.mate.domain.goodsChat.entity.GoodsChatPart; +import com.example.mate.domain.goodsChat.entity.GoodsChatPartId; +import com.example.mate.domain.goodsChat.entity.GoodsChatRoom; +import com.example.mate.domain.goodsChat.event.GoodsChatEvent; +import com.example.mate.domain.goodsChat.repository.GoodsChatMessageRepository; +import com.example.mate.domain.goodsChat.repository.GoodsChatPartRepository; +import com.example.mate.domain.goodsChat.repository.GoodsChatRoomRepository; +import com.example.mate.domain.member.entity.Member; +import com.example.mate.domain.member.repository.MemberRepository; +import java.time.LocalDateTime; +import java.util.Optional; +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.SimpMessagingTemplate; + +@ExtendWith(MockitoExtension.class) +class GoodsChatMessageServiceTest { + + @InjectMocks + private GoodsChatMessageService goodsChatMessageService; + + @Mock + private MemberRepository memberRepository; + + @Mock + private GoodsChatRoomRepository chatRoomRepository; + + @Mock + private GoodsChatPartRepository chatPartRepository; + + @Mock + private GoodsChatMessageRepository messageRepository; + + @Mock + private SimpMessagingTemplate messagingTemplate; + + private Member createMember(Long id, String name, String nickname) { + return Member.builder() + .id(id) + .name(name) + .nickname(nickname) + .build(); + } + + private GoodsChatRoom createGoodsChatRoom(Long id) { + return GoodsChatRoom.builder() + .id(id) + .build(); + } + + private GoodsChatPart createGoodsChatPart(Member member, GoodsChatRoom chatRoom) { + return GoodsChatPart.builder() + .member(member) + .goodsChatRoom(chatRoom) + .build(); + } + + private GoodsChatMessage createGoodsChatMessage(String message, GoodsChatPart chatPart, MessageType type) { + return GoodsChatMessage.builder() + .content(message) + .goodsChatPart(chatPart) + .messageType(type) + .sentAt(LocalDateTime.now()) + .build(); + } + + @Nested + @DisplayName("굿즈거래 채팅 전송 테스트") + class SendChatMessageTest { + + @Test + @DisplayName("메시지 전송 성공") + void sendMessage_should_save_message_and_send_to_subscribers() { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + GoodsChatMessageRequest request = new GoodsChatMessageRequest(chatRoomId, memberId, "Hello World", MessageType.TALK); + + Member member = createMember(memberId, "Test User", "test_user"); + GoodsChatRoom chatRoom = createGoodsChatRoom(chatRoomId); + GoodsChatPart chatPart = createGoodsChatPart(member, chatRoom); + GoodsChatMessage chatMessage = createGoodsChatMessage(request.getMessage(), chatPart, MessageType.TALK); + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + when(chatRoomRepository.findById(chatRoomId)).thenReturn(Optional.of(chatRoom)); + when(chatPartRepository.findById(new GoodsChatPartId(memberId, chatRoomId))).thenReturn(Optional.of(chatPart)); + when(messageRepository.save(any(GoodsChatMessage.class))).thenReturn(chatMessage); + + // when + goodsChatMessageService.sendMessage(request); + + // then + verify(memberRepository).findById(memberId); + verify(chatRoomRepository).findById(chatRoomId); + verify(chatPartRepository).findById(new GoodsChatPartId(memberId, chatRoomId)); + verify(messageRepository).save(any(GoodsChatMessage.class)); + verify(messagingTemplate).convertAndSend(eq("/sub/chat/goods/" + chatRoomId), any(GoodsChatMessageResponse.class)); + } + + @Test + @DisplayName("메시지 전송 실패 - 유효하지 않은 회원") + void sendMessage_should_throw_custom_exception_for_invalid_member() { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + GoodsChatMessageRequest request = new GoodsChatMessageRequest(chatRoomId, memberId, "Hello World", MessageType.TALK); + + when(memberRepository.findById(memberId)).thenReturn(Optional.empty()); + + // when + assertThatThrownBy(() -> goodsChatMessageService.sendMessage(request)) + .isExactlyInstanceOf(CustomException.class) + .hasMessage(ErrorCode.MEMBER_NOT_FOUND_BY_ID.getMessage()); + + // then + verify(memberRepository).findById(memberId); + verify(chatRoomRepository, never()).findById(chatRoomId); + verify(chatPartRepository, never()).findById(new GoodsChatPartId(memberId, chatRoomId)); + verify(messageRepository, never()).save(any(GoodsChatMessage.class)); + verify(messagingTemplate, never()).convertAndSend(eq("/sub/chat/goods/" + chatRoomId), any(GoodsChatMessageResponse.class)); + } + + @Test + @DisplayName("메시지 전송 실패 - 유효하지 않은 채팅방") + void sendMessage_should_throw_custom_exception_for_invalid_chatroom() { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + GoodsChatMessageRequest request = new GoodsChatMessageRequest(chatRoomId, memberId, "Hello World", MessageType.TALK); + + Member member = createMember(memberId, "Test User", "test_user"); + GoodsChatRoom chatRoom = createGoodsChatRoom(chatRoomId); + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + when(chatRoomRepository.findById(chatRoomId)).thenReturn(Optional.of(chatRoom)); + when(chatPartRepository.findById(new GoodsChatPartId(memberId, chatRoomId))).thenReturn(Optional.empty()); + + // when + assertThatThrownBy(() -> goodsChatMessageService.sendMessage(request)) + .isExactlyInstanceOf(CustomException.class) + .hasMessage(ErrorCode.GOODS_CHAT_NOT_FOUND_CHAT_PART.getMessage()); + + // then + verify(memberRepository).findById(memberId); + verify(chatRoomRepository).findById(chatRoomId); + verify(chatPartRepository).findById(new GoodsChatPartId(memberId, chatRoomId)); + verify(messageRepository, never()).save(any(GoodsChatMessage.class)); + verify(messagingTemplate, never()).convertAndSend(eq("/sub/chat/goods/" + chatRoomId), any(GoodsChatMessageResponse.class)); + } + + @Test + @DisplayName("메시지 전송 실패 - 해당 채팅방에 참가한 회원이 아닌 경우") + void sendMessage_should_throw_custom_exception_for_non_participant() { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + GoodsChatMessageRequest request = new GoodsChatMessageRequest(chatRoomId, memberId, "Hello World", MessageType.TALK); + Member member = createMember(memberId, "Test User", "test_user"); + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + when(chatRoomRepository.findById(chatRoomId)).thenReturn(Optional.empty()); + + // when + assertThatThrownBy(() -> goodsChatMessageService.sendMessage(request)) + .isExactlyInstanceOf(CustomException.class) + .hasMessage(ErrorCode.GOODS_CHAT_ROOM_NOT_FOUND.getMessage()); + + // then + verify(memberRepository).findById(memberId); + verify(chatRoomRepository).findById(chatRoomId); + verify(chatPartRepository, never()).findById(new GoodsChatPartId(memberId, chatRoomId)); + verify(messageRepository, never()).save(any(GoodsChatMessage.class)); + verify(messagingTemplate, never()).convertAndSend(eq("/sub/chat/goods/" + chatRoomId), any(GoodsChatMessageResponse.class)); + } + } + + @Nested + @DisplayName("굿즈거래 입장 및 퇴장 메시지 전송 테스트") + class SendChatSystemMessageTest { + + @Test + @DisplayName("채팅방 입장 메시지 전송 성공") + void sendChatEventMessage_should_send_enter_message() { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + + Member member = createMember(memberId, "Test User", "test_user"); + GoodsChatEvent event = new GoodsChatEvent(chatRoomId, member, MessageType.ENTER); + + GoodsChatRoom chatRoom = createGoodsChatRoom(event.chatRoomId()); + GoodsChatPart chatPart = createGoodsChatPart(event.member(), chatRoom); + + GoodsChatMessage chatMessage + = createGoodsChatMessage(member.getNickname() + "님이 대화를 시작했습니다.", chatPart, event.type()); + + when(chatRoomRepository.findById(chatRoomId)).thenReturn(Optional.of(chatRoom)); + when(chatPartRepository.findById(new GoodsChatPartId(memberId, chatRoomId))).thenReturn(Optional.of(chatPart)); + when(messageRepository.save(any(GoodsChatMessage.class))).thenReturn(chatMessage); + + // when + goodsChatMessageService.sendChatEventMessage(event); + + // then + verify(chatRoomRepository).findById(chatRoomId); + verify(chatPartRepository).findById(new GoodsChatPartId(memberId, chatRoomId)); + verify(messageRepository).save(any(GoodsChatMessage.class)); + verify(messagingTemplate).convertAndSend(eq("/sub/chat/goods/" + chatRoomId), any(GoodsChatMessageResponse.class)); + } + + @Test + @DisplayName("채팅방 퇴장 메시지 전송 성공") + void sendChatEventMessage_should_send_leave_message() { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + + Member member = createMember(memberId, "Test User", "test_user"); + GoodsChatEvent event = new GoodsChatEvent(chatRoomId, member, MessageType.LEAVE); + + GoodsChatRoom chatRoom = createGoodsChatRoom(event.chatRoomId()); + GoodsChatPart chatPart = createGoodsChatPart(event.member(), chatRoom); + + GoodsChatMessage chatMessage + = createGoodsChatMessage(member.getNickname() + "님이 대화를 떠났습니다.", chatPart, event.type()); + + when(chatRoomRepository.findById(chatRoomId)).thenReturn(Optional.of(chatRoom)); + when(chatPartRepository.findById(new GoodsChatPartId(memberId, chatRoomId))).thenReturn(Optional.of(chatPart)); + when(messageRepository.save(any(GoodsChatMessage.class))).thenReturn(chatMessage); + + // when + goodsChatMessageService.sendChatEventMessage(event); + + // then + verify(chatRoomRepository).findById(chatRoomId); + verify(chatPartRepository).findById(new GoodsChatPartId(memberId, chatRoomId)); + verify(messageRepository).save(any(GoodsChatMessage.class)); + verify(messagingTemplate).convertAndSend(eq("/sub/chat/goods/" + chatRoomId), any(GoodsChatMessageResponse.class)); + } + + @Test + @DisplayName("시스템 메시지 전송 실패 - 유효하지 않은 채팅방") + void sendChatEventMessage_should_throw_custom_exception_for_invalid_chatroom() { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + + Member member = createMember(memberId, "Test User", "test_user"); + GoodsChatEvent event = new GoodsChatEvent(chatRoomId, member, MessageType.LEAVE); + + when(chatRoomRepository.findById(chatRoomId)).thenReturn(Optional.empty()); + + // when + assertThatThrownBy(() -> goodsChatMessageService.sendChatEventMessage(event)) + .isExactlyInstanceOf(CustomException.class) + .hasMessage(ErrorCode.GOODS_CHAT_ROOM_NOT_FOUND.getMessage()); + + // then + verify(chatRoomRepository).findById(chatRoomId); + verify(chatPartRepository, never()).findById(new GoodsChatPartId(memberId, chatRoomId)); + verify(messageRepository, never()).save(any(GoodsChatMessage.class)); + verify(messagingTemplate, never()).convertAndSend(eq("/sub/chat/goods/" + chatRoomId), any(GoodsChatMessageResponse.class)); + } + + @Test + @DisplayName("시스템 메시지 전송 실패 - 해당 채팅방에 참가한 회원이 아닌 경우") + void sendChatEventMessage_should_throw_custom_exception_for_non_participant() { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + + Member member = createMember(memberId, "Test User", "test_user"); + GoodsChatEvent event = new GoodsChatEvent(chatRoomId, member, MessageType.LEAVE); + GoodsChatRoom chatRoom = createGoodsChatRoom(event.chatRoomId()); + + when(chatRoomRepository.findById(chatRoomId)).thenReturn(Optional.of(chatRoom)); + when(chatPartRepository.findById(new GoodsChatPartId(memberId, chatRoomId))).thenReturn(Optional.empty()); + + // when + assertThatThrownBy(() -> goodsChatMessageService.sendChatEventMessage(event)) + .isExactlyInstanceOf(CustomException.class) + .hasMessage(ErrorCode.GOODS_CHAT_NOT_FOUND_CHAT_PART.getMessage()); + + // then + verify(chatRoomRepository).findById(chatRoomId); + verify(chatPartRepository).findById(new GoodsChatPartId(memberId, chatRoomId)); + verify(messageRepository, never()).save(any(GoodsChatMessage.class)); + verify(messagingTemplate, never()).convertAndSend(eq("/sub/chat/goods/" + chatRoomId), any(GoodsChatMessageResponse.class)); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/example/mate/domain/goodsChat/service/GoodsChatServiceTest.java b/src/test/java/com/example/mate/domain/goodsChat/service/GoodsChatServiceTest.java index 3112762c..aabf53ff 100644 --- a/src/test/java/com/example/mate/domain/goodsChat/service/GoodsChatServiceTest.java +++ b/src/test/java/com/example/mate/domain/goodsChat/service/GoodsChatServiceTest.java @@ -14,18 +14,24 @@ import com.example.mate.domain.constant.MessageType; import com.example.mate.domain.goods.entity.Category; import com.example.mate.domain.goods.entity.GoodsPost; +import com.example.mate.domain.goods.entity.GoodsPostImage; +import com.example.mate.domain.goods.entity.Location; import com.example.mate.domain.goods.entity.Role; import com.example.mate.domain.goods.entity.Status; import com.example.mate.domain.goods.repository.GoodsPostRepository; import com.example.mate.domain.goodsChat.dto.response.GoodsChatMessageResponse; import com.example.mate.domain.goodsChat.dto.response.GoodsChatRoomResponse; +import com.example.mate.domain.goodsChat.dto.response.GoodsChatRoomSummaryResponse; import com.example.mate.domain.goodsChat.entity.GoodsChatMessage; +import com.example.mate.domain.goodsChat.entity.GoodsChatPart; import com.example.mate.domain.goodsChat.entity.GoodsChatPartId; import com.example.mate.domain.goodsChat.entity.GoodsChatRoom; +import com.example.mate.domain.goodsChat.event.GoodsChatEvent; import com.example.mate.domain.goodsChat.event.GoodsChatEventPublisher; import com.example.mate.domain.goodsChat.repository.GoodsChatMessageRepository; import com.example.mate.domain.goodsChat.repository.GoodsChatPartRepository; import com.example.mate.domain.goodsChat.repository.GoodsChatRoomRepository; +import com.example.mate.domain.member.dto.response.MemberSummaryResponse; import com.example.mate.domain.member.entity.Member; import com.example.mate.domain.member.repository.MemberRepository; import java.time.LocalDateTime; @@ -86,6 +92,22 @@ private GoodsPost createGoodsPost(Long id, Member seller, Member buyer, Status s .price(10_000) .status(status) .category(Category.ACCESSORY) + .location(createLocation()) + .build(); + } + + private Location createLocation() { + return Location.builder() + .placeName("test place name") + .longitude("test longitude") + .latitude("test latitude") + .build(); + } + + private GoodsPostImage createGoodsPostImage(GoodsPost goodsPost) { + return GoodsPostImage.builder() + .post(goodsPost) + .imageUrl("main_image_url") .build(); } @@ -181,6 +203,7 @@ void get_Or_Create_GoodsChatRoom_should_create_new_chatRoom() { verify(goodsPostRepository).findById(goodsPostId); verify(chatRoomRepository).findExistingChatRoom(goodsPostId, buyerId, Role.BUYER); verify(chatRoomRepository).save(any(GoodsChatRoom.class)); + verify(eventPublisher).publish(any(GoodsChatEvent.class)); } @Test @@ -298,4 +321,412 @@ void getMessagesForChatRoom_should_throw_exception_for_non_participant() { verify(messageRepository, never()).getChatMessages(chatRoomId, pageable); } } + + @Nested + @DisplayName("채팅방 정보 조회 테스트") + class GoodsChatRoomInfoTest { + + @Test + @DisplayName("채팅방 정보 조회 성공") + void getGoodsChatRoomInfo_should_return_chatroom_info_and_latest_message() { + // given + Member member = createMember(1L, "Test Member", "test_member"); + GoodsPost goodsPost = createGoodsPost(1L, member, null, Status.OPEN); + goodsPost.changeImages(List.of(createGoodsPostImage(goodsPost))); + GoodsChatRoom chatRoom = createGoodsChatRoom(1L, goodsPost); + + Long memberId = member.getId(); + Long chatRoomId = chatRoom.getId(); + GoodsChatPartId goodsChatPartId = new GoodsChatPartId(memberId, chatRoomId); + + chatRoom.addChatParticipant(member, Role.BUYER); + chatRoom.addChatParticipant(member, Role.SELLER); + + GoodsChatMessage firstMessage = createMessage(chatRoom, 1L, 0, "first message", LocalDateTime.now().minusMinutes(10)); + GoodsChatMessage secondMessage = createMessage(chatRoom, 2L, 1, "second message", LocalDateTime.now()); + + Page messages = new PageImpl<>(List.of(secondMessage, firstMessage)); + + when(partRepository.existsById(goodsChatPartId)).thenReturn(true); + when(chatRoomRepository.findByChatRoomId(chatRoomId)).thenReturn(Optional.of(chatRoom)); + when(messageRepository.getChatMessages(chatRoomId, PageRequest.of(0, 20))).thenReturn(messages); + + // when + GoodsChatRoomResponse goodsChatRoomInfo = goodsChatService.getGoodsChatRoomInfo(memberId, chatRoomId); + + // then + assertThat(goodsChatRoomInfo.getChatRoomId()).isEqualTo(chatRoomId); + assertThat(goodsChatRoomInfo.getGoodsPostId()).isEqualTo(goodsPost.getId()); + assertThat(goodsChatRoomInfo.getTeamName()).isEqualTo("KIA"); + assertThat(goodsChatRoomInfo.getTitle()).isEqualTo(goodsPost.getTitle()); + assertThat(goodsChatRoomInfo.getCategory()).isEqualTo(goodsPost.getCategory().getValue()); + assertThat(goodsChatRoomInfo.getPrice()).isEqualTo(goodsPost.getPrice()); + assertThat(goodsChatRoomInfo.getPostStatus()).isEqualTo(goodsPost.getStatus().getValue()); + assertThat(goodsChatRoomInfo.getChatRoomStatus()).isEqualTo(chatRoom.getIsActive().toString()); + assertThat(goodsChatRoomInfo.getImageUrl()).isEqualTo(goodsPost.getMainImageUrl()); + + assertThat(goodsChatRoomInfo.getInitialMessages().getContent()) + .hasSize(2) + .extracting(GoodsChatMessageResponse::getMessage) + .containsExactly("second message", "first message"); + + verify(partRepository).existsById(goodsChatPartId); + verify(chatRoomRepository).findByChatRoomId(chatRoomId); + verify(messageRepository).getChatMessages(chatRoomId, PageRequest.of(0, 20)); + } + + @Test + @DisplayName("채팅방 정보 조회 실패 - 채팅방에 참여하지 않은 회원") + void getGoodsChatRoomInfo_should_throw_CustomException_for_not_participant() { + // given + Member member = createMember(1L, "Test Member", "test_member"); + GoodsChatRoom chatRoom = createGoodsChatRoom(1L, null); + + Long memberId = member.getId(); + Long chatRoomId = chatRoom.getId(); + + GoodsChatPartId goodsChatPartId = new GoodsChatPartId(memberId, chatRoomId); + when(partRepository.existsById(goodsChatPartId)).thenReturn(false); + + // when + assertThatThrownBy(() -> goodsChatService.getGoodsChatRoomInfo(memberId, chatRoomId)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.GOODS_CHAT_NOT_FOUND_CHAT_PART.getMessage()); + + // then + verify(partRepository).existsById(goodsChatPartId); + verify(chatRoomRepository, never()).findByChatRoomId(chatRoomId); + verify(messageRepository, never()).getChatMessages(chatRoomId, PageRequest.of(0, 20)); + } + + @Test + @DisplayName("채팅방 정보 조회 실패 - 존재하지 않는 채팅방") + void getGoodsChatRoomInfo_should_throw_CustomException_for_invalid_chatroom() { + Member member = createMember(1L, "Test Member", "test_member"); + GoodsChatRoom chatRoom = createGoodsChatRoom(1L, null); + + Long memberId = member.getId(); + Long chatRoomId = chatRoom.getId(); + + GoodsChatPartId goodsChatPartId = new GoodsChatPartId(memberId, chatRoomId); + when(partRepository.existsById(goodsChatPartId)).thenReturn(true); + when(chatRoomRepository.findByChatRoomId(chatRoomId)).thenReturn(Optional.empty()); + + // when + assertThatThrownBy(() -> goodsChatService.getGoodsChatRoomInfo(memberId, chatRoomId)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.GOODS_CHAT_ROOM_NOT_FOUND.getMessage()); + + // then + verify(partRepository).existsById(goodsChatPartId); + verify(chatRoomRepository).findByChatRoomId(chatRoomId); + verify(messageRepository, never()).getChatMessages(chatRoomId, PageRequest.of(0, 20)); + } + } + + @Nested + @DisplayName("채팅방 목록 조회 테스트") + class GoodsChatRoomListTest { + + @Test + @DisplayName("채팅방 목록 조회 성공") + void getGoodsChatRooms_should_return_paginated_chatRoom_summaries() { + // given + Long memberId = 1L; + Member member = createMember(memberId, "Test Member", "test_member"); + Member opponentMember = createMember(2L, "Opponent Member", "opponent_member"); + + // 첫번째 채팅방 + GoodsPost goodsPost = createGoodsPost(1L, member, null, Status.OPEN); + GoodsChatRoom chatRoom = createGoodsChatRoom(1L, goodsPost); + chatRoom.addChatParticipant(member, Role.BUYER); + chatRoom.addChatParticipant(opponentMember, Role.SELLER); + + // 두번째 채팅방 + GoodsPost goodsPost2 = createGoodsPost(2L, member, null, Status.OPEN); + GoodsChatRoom chatRoom2 = createGoodsChatRoom(2L, goodsPost2); + chatRoom2.addChatParticipant(member, Role.BUYER); + chatRoom2.addChatParticipant(opponentMember, Role.SELLER); + + Pageable pageable = PageRequest.of(0, 10); + Page chatRoomPage = new PageImpl<>(List.of(chatRoom, chatRoom2), pageable, 2); + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + when(chatRoomRepository.findChatRoomPageByMemberId(memberId, pageable)).thenReturn(chatRoomPage); + + // when + PageResponse result = goodsChatService.getGoodsChatRooms(memberId, pageable); + + // then + assertThat(result.getContent()).hasSize(2); + assertThat(result.getTotalElements()).isEqualTo(2); + assertThat(result.getTotalPages()).isEqualTo(1); + assertThat(result.getPageNumber()).isEqualTo(0); + + List resultContent = result.getContent(); + assertThat(resultContent.get(0).getChatRoomId()).isEqualTo(chatRoom.getId()); + assertThat(resultContent.get(0).getOpponentNickname()).isEqualTo(opponentMember.getNickname()); + assertThat(resultContent.get(1).getChatRoomId()).isEqualTo(chatRoom2.getId()); + assertThat(resultContent.get(1).getOpponentNickname()).isEqualTo(opponentMember.getNickname()); + + verify(memberRepository).findById(memberId); + verify(chatRoomRepository).findChatRoomPageByMemberId(memberId, pageable); + } + + @Test + @DisplayName("채팅방 목록 조회 실패 - 존재하지 않는 회원") + void getGoodsChatRooms_should_throw_CustomException_for_invalid_member() { + // given + Long memberId = 1L; + Pageable pageable = PageRequest.of(0, 10); + + when(memberRepository.findById(memberId)).thenReturn(Optional.empty()); + + // when + assertThatThrownBy(() -> goodsChatService.getGoodsChatRooms(memberId, pageable)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.MEMBER_NOT_FOUND_BY_ID.getMessage()); + + // then + verify(memberRepository).findById(memberId); + verify(chatRoomRepository, never()).findChatRoomPageByMemberId(anyLong(), any(Pageable.class)); + } + } + + @Nested + @DisplayName("채팅방 채팅내역 조회 테스트") + class GoodsChatMessageFetchTest { + + @Test + @DisplayName("채팅방 채팅내역 조회 성공 - 최근 채팅내역을 페이지로 반환") + void getMessagesForChatRoom_should_return_paginated_messages() { + // given + GoodsChatRoom chatRoom = createGoodsChatRoom(1L, null); + Member buyer = createMember(1L, "Test buyer", "test_buyer"); + Member seller = createMember(2L, "Test seller", "test_seller"); + chatRoom.addChatParticipant(buyer, Role.BUYER); + chatRoom.addChatParticipant(seller, Role.SELLER); + + Long chatRoomId = chatRoom.getId(); + Long buyerId = buyer.getId(); + + Pageable pageable = PageRequest.of(0, 20); + GoodsChatPartId goodsChatPartId = new GoodsChatPartId(buyerId, chatRoomId); + + GoodsChatMessage firstMessage = createMessage(chatRoom, 1L, 0, "First message", LocalDateTime.now().minusMinutes(10)); + GoodsChatMessage secondMessage = createMessage(chatRoom, 2L, 1, "Second message", LocalDateTime.now()); + + Page messagePage = new PageImpl<>(List.of(secondMessage, firstMessage), pageable, 2); + + when(partRepository.existsById(goodsChatPartId)).thenReturn(true); + when(messageRepository.getChatMessages(chatRoomId, pageable)).thenReturn(messagePage); + + // when + PageResponse result = goodsChatService.getMessagesForChatRoom(chatRoomId, buyerId, pageable); + + // then + assertThat(result.getContent()).hasSize(2); + assertThat(result.getContent().get(0).getMessage()).isEqualTo(secondMessage.getContent()); + assertThat(result.getContent().get(0).getChatMessageId()).isEqualTo(secondMessage.getId()); + assertThat(result.getContent().get(0).getSentAt()).isEqualTo(secondMessage.getSentAt()); + assertThat(result.getContent().get(1).getMessage()).isEqualTo(firstMessage.getContent()); + assertThat(result.getContent().get(1).getChatMessageId()).isEqualTo(firstMessage.getId()); + assertThat(result.getContent().get(1).getSentAt()).isEqualTo(firstMessage.getSentAt()); + + verify(partRepository).existsById(goodsChatPartId); + verify(messageRepository).getChatMessages(chatRoomId, pageable); + } + + @Test + @DisplayName("채팅 메시지 조회 실패 - 회원이 채팅방에 참여하지 않은 경우 예외를 발생시킨다.") + void getMessagesForChatRoom_should_throw_exception_when_member_not_participant() { + // given + GoodsChatRoom chatRoom = createGoodsChatRoom(1L, null); + Member buyer = createMember(1L, "Test buyer", "test_buyer"); + Member seller = createMember(2L, "Test seller", "test_seller"); + chatRoom.addChatParticipant(buyer, Role.BUYER); + chatRoom.addChatParticipant(seller, Role.SELLER); + + Long chatRoomId = chatRoom.getId(); + Long buyerId = buyer.getId(); + + Pageable pageable = PageRequest.of(0, 20); + GoodsChatPartId goodsChatPartId = new GoodsChatPartId(buyerId, chatRoomId); + + when(partRepository.existsById(goodsChatPartId)).thenReturn(false); + + // when + assertThatThrownBy(() -> goodsChatService.getMessagesForChatRoom(chatRoomId, buyerId, pageable)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.GOODS_CHAT_NOT_FOUND_CHAT_PART.getMessage()); + + // then + verify(partRepository).existsById(goodsChatPartId); + verify(messageRepository, never()).getChatMessages(anyLong(), any(Pageable.class)); + } + + @Nested + @DisplayName("굿즈 채팅방 퇴장 테스트") + class GoodsChatroomLeaveTest { + + @Test + @DisplayName("채팅방 퇴장 성공 - 남아있는 참여자가 있는 경우 퇴장 메시지 전송") + void deactivateGoodsChatPart_should_publish_leave_event_when_other_members_remain() { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + GoodsChatRoom chatRoom = createGoodsChatRoom(chatRoomId, null); + + Member member = createMember(memberId, "Test Member", "test_member"); + Member anotherMember = createMember(2L, "Another Member", "another_member"); + + chatRoom.addChatParticipant(member, Role.BUYER); + chatRoom.addChatParticipant(anotherMember, Role.SELLER); + + GoodsChatPart goodsChatPart = chatRoom.getChatParts().get(0); + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + when(partRepository.findById(new GoodsChatPartId(memberId, chatRoomId))).thenReturn(Optional.of(goodsChatPart)); + + // when + goodsChatService.deactivateGoodsChatPart(memberId, chatRoomId); + + // then + assertThat(goodsChatPart.getIsActive()).isFalse(); + assertThat(chatRoom.getIsActive()).isFalse(); + verify(memberRepository).findById(memberId); + verify(partRepository).findById(new GoodsChatPartId(memberId, chatRoomId)); + verify(chatRoomRepository, never()).deleteById(chatRoomId); + verify(eventPublisher).publish(any(GoodsChatEvent.class)); + } + + @Test + @DisplayName("채팅방 퇴장 성공 - 채팅방에 남아있는 참여자가 없는 경우 채팅방 삭제") + void deactivateGoodsChatPart_should_delete_chat_room_when_no_members_remain() { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + GoodsChatRoom chatRoom = createGoodsChatRoom(chatRoomId, null); + + Member member = createMember(memberId, "Test Member", "test_member"); + Member anotherMember = createMember(2L, "Another Member", "another_member"); + + chatRoom.addChatParticipant(member, Role.BUYER); + chatRoom.addChatParticipant(anotherMember, Role.SELLER); + + GoodsChatPart goodsChatPart = chatRoom.getChatParts().get(0); + + // 미리 anotherMember 는 채팅방을 나가도록 설정 (채팅방 비활성화) + chatRoom.getChatParts().get(1).leaveAndCheckRoomStatus(); + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + when(partRepository.findById(new GoodsChatPartId(memberId, chatRoomId))).thenReturn(Optional.of(goodsChatPart)); + + // when + goodsChatService.deactivateGoodsChatPart(memberId, chatRoomId); + + // then + verify(memberRepository).findById(memberId); + verify(partRepository).findById(new GoodsChatPartId(memberId, chatRoomId)); + verify(chatRoomRepository).deleteById(chatRoomId); + verify(eventPublisher, never()).publish(any(GoodsChatEvent.class)); + } + + @Test + @DisplayName("채팅방 퇴장 실패 - 존재하지 않는 회원일 경우 예외 발생") + void deactivateGoodsChatPart_should_throw_exception_when_member_not_found() { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + + when(memberRepository.findById(memberId)).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> goodsChatService.deactivateGoodsChatPart(memberId, chatRoomId)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.MEMBER_NOT_FOUND_BY_ID.getMessage()); + + verify(memberRepository).findById(memberId); + verify(partRepository, never()).findById(any(GoodsChatPartId.class)); + verify(chatRoomRepository, never()).deleteById(anyLong()); + verify(eventPublisher, never()).publish(any(GoodsChatEvent.class)); + } + + @Test + @DisplayName("채팅방 퇴장 실패 - 참여하지 않은 회원이 퇴장을 시도할 경우 예외 발생") + void deactivateGoodsChatPart_should_throw_exception_when_member_not_in_chatroom() { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + Member member = createMember(memberId, "Test Member", "test_member"); + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + when(partRepository.findById(new GoodsChatPartId(memberId, chatRoomId))).thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> goodsChatService.deactivateGoodsChatPart(memberId, chatRoomId)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.GOODS_CHAT_NOT_FOUND_CHAT_PART.getMessage()); + + verify(memberRepository).findById(memberId); + verify(partRepository).findById(new GoodsChatPartId(memberId, chatRoomId)); + verify(chatRoomRepository, never()).deleteById(chatRoomId); + verify(eventPublisher, never()).publish(any(GoodsChatEvent.class)); + } + } + } + + @Nested + @DisplayName("채팅방 참여자 조회 테스트") + class GoodsChatRoomMemberTest { + + @Test + @DisplayName("채팅방 참여자 조회 성공 - 참여자가 존재하는 경우 참여자 목록을 반환") + void getChatRoomMembers_should_return_list_of_participants() { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + GoodsChatRoom goodsChatRoom = createGoodsChatRoom(chatRoomId, null); + + Member member = createMember(memberId, "Test Member", "test_member"); + Member anotherMember = createMember(2L, "Test Another Member", "test_another_member"); + + goodsChatRoom.addChatParticipant(member, Role.BUYER); + goodsChatRoom.addChatParticipant(anotherMember, Role.SELLER); + + when(partRepository.existsById(new GoodsChatPartId(memberId, chatRoomId))).thenReturn(true); + when(partRepository.findAllWithMemberByChatRoomId(chatRoomId)).thenReturn(goodsChatRoom.getChatParts()); + + // when + List result = goodsChatService.getChatRoomMembers(memberId, chatRoomId); + + // then + assertThat(result).hasSize(2); + assertThat(result.get(0).getMemberId()).isEqualTo(member.getId()); + assertThat(result.get(0).getNickname()).isEqualTo(member.getNickname()); + assertThat(result.get(1).getMemberId()).isEqualTo(anotherMember.getId()); + assertThat(result.get(1).getNickname()).isEqualTo(anotherMember.getNickname()); + + verify(partRepository).existsById(new GoodsChatPartId(memberId, chatRoomId)); + verify(partRepository).findAllWithMemberByChatRoomId(chatRoomId); + } + + @Test + @DisplayName("채팅방 참여자 조회 실패 - 참여하지 않은 회원이 참여자 목록을 요청한 경우 예외 발생") + void getChatRoomMembers_should_throw_exception_for_non_participant() { + // given + Long memberId = 1L; + Long chatRoomId = 1L; + + when(partRepository.existsById(new GoodsChatPartId(memberId, chatRoomId))).thenReturn(false); + + // when & then + assertThatThrownBy(() -> goodsChatService.getChatRoomMembers(memberId, chatRoomId)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.GOODS_CHAT_NOT_FOUND_CHAT_PART.getMessage()); + + verify(partRepository).existsById(new GoodsChatPartId(memberId, chatRoomId)); + verify(partRepository, never()).findAllWithMemberByChatRoomId(chatRoomId); + } + } } \ No newline at end of file