diff --git a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java index 77120e884..667a02d5e 100644 --- a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java +++ b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java @@ -12,6 +12,8 @@ import com.example.solidconnection.chat.domain.ChatReadStatus; import com.example.solidconnection.chat.domain.ChatRoom; import com.example.solidconnection.chat.dto.ChatMessageResponse; +import com.example.solidconnection.chat.dto.ChatMessageSendRequest; +import com.example.solidconnection.chat.dto.ChatMessageSendResponse; import com.example.solidconnection.chat.dto.ChatRoomListResponse; import com.example.solidconnection.chat.fixture.ChatAttachmentFixture; import com.example.solidconnection.chat.fixture.ChatMessageFixture; @@ -29,10 +31,14 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.BDDMockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.messaging.simp.SimpMessagingTemplate; @TestContainerSpringBootTest @DisplayName("채팅 서비스 테스트") @@ -62,6 +68,9 @@ class ChatServiceTest { @Autowired private ChatAttachmentFixture chatAttachmentFixture; + @MockBean + private SimpMessagingTemplate simpMessagingTemplate; + private SiteUser user; private SiteUser mentor1; private SiteUser mentor2; @@ -363,4 +372,53 @@ void setUp() { .hasMessage(CHAT_PARTICIPANT_NOT_FOUND.getMessage()); } } + + @Nested + class 채팅_메시지를_전송한다 { + + private SiteUser sender; + private ChatParticipant senderParticipant; + private ChatRoom chatRoom; + + @BeforeEach + void setUp() { + sender = siteUserFixture.사용자(111, "sender"); + chatRoom = chatRoomFixture.채팅방(false); + senderParticipant = chatParticipantFixture.참여자(sender.getId(), chatRoom); + } + + @Test + void 채팅방_참여자는_메시지를_전송할_수_있다() { + // given + final String content = "안녕하세요"; + ChatMessageSendRequest request = new ChatMessageSendRequest(content); + + // when + chatService.sendChatMessage(request, sender.getId(), chatRoom.getId()); + + // then + ArgumentCaptor destinationCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor payloadCaptor = ArgumentCaptor.forClass(ChatMessageSendResponse.class); + + BDDMockito.verify(simpMessagingTemplate).convertAndSend(destinationCaptor.capture(), payloadCaptor.capture()); + + assertAll( + () -> assertThat(destinationCaptor.getValue()).isEqualTo("/topic/chat/" + chatRoom.getId()), + () -> assertThat(payloadCaptor.getValue().content()).isEqualTo(content), + () -> assertThat(payloadCaptor.getValue().senderId()).isEqualTo(senderParticipant.getId()) + ); + } + + @Test + void 채팅_참여자가_아니면_메시지를_전송할_수_없다() { + // given + SiteUser nonParticipant = siteUserFixture.사용자(333, "nonParticipant"); + ChatMessageSendRequest request = new ChatMessageSendRequest("안녕하세요"); + + // when & then + assertThatCode(() -> chatService.sendChatMessage(request, nonParticipant.getId(), chatRoom.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(CHAT_PARTICIPANT_NOT_FOUND.getMessage()); + } + } } diff --git a/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java b/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java new file mode 100644 index 000000000..c74d84534 --- /dev/null +++ b/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java @@ -0,0 +1,114 @@ +package com.example.solidconnection.websocket; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.ThrowableAssert.catchThrowable; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.example.solidconnection.auth.service.AccessToken; +import com.example.solidconnection.auth.service.AuthTokenProvider; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.AfterEach; +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.simp.stomp.StompHeaders; +import org.springframework.messaging.simp.stomp.StompSession; +import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.socket.WebSocketHttpHeaders; +import org.springframework.web.socket.client.standard.StandardWebSocketClient; +import org.springframework.web.socket.messaging.WebSocketStompClient; +import org.springframework.web.socket.sockjs.client.SockJsClient; +import org.springframework.web.socket.sockjs.client.Transport; +import org.springframework.web.socket.sockjs.client.WebSocketTransport; + +@TestContainerSpringBootTest +@DisplayName("WebSocket/STOMP 통합 테스트") +class WebSocketStompIntegrationTest { + + @LocalServerPort + private int port; + private String url; + private WebSocketStompClient stompClient; + private StompSession stompSession; + + @Autowired + private AuthTokenProvider authTokenProvider; + + @Autowired + private SiteUserFixture siteUserFixture; + + @BeforeEach + void setUp() { + this.url = String.format("ws://localhost:%d/connect", port); + List transports = List.of(new WebSocketTransport(new StandardWebSocketClient())); + this.stompClient = new WebSocketStompClient(new SockJsClient(transports)); + this.stompClient.setMessageConverter(new MappingJackson2MessageConverter()); + } + + @AfterEach + void tearDown() { + if (this.stompSession != null && this.stompSession.isConnected()) { + this.stompSession.disconnect(); + } + } + + @Nested + class WebSocket_핸드셰이크_및_STOMP_세션_수립_테스트 { + + private final BlockingQueue transportErrorQueue = new ArrayBlockingQueue<>(1); + + private final StompSessionHandlerAdapter sessionHandler = new StompSessionHandlerAdapter() { + @Override + public void handleTransportError(StompSession session, Throwable exception) { + transportErrorQueue.add(exception); + } + }; + + @Test + void 인증된_사용자는_핸드셰이크를_성공한다() throws Exception { + // given + SiteUser user = siteUserFixture.사용자(); + AccessToken accessToken = authTokenProvider.generateAccessToken(authTokenProvider.toSubject(user), user.getRole()); + + WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); + handshakeHeaders.add("Authorization", "Bearer " + accessToken.token()); + + // when + stompSession = stompClient.connectAsync(url, handshakeHeaders, new StompHeaders(), sessionHandler).get(5, SECONDS); + + // then + assertAll( + () -> assertThat(stompSession).isNotNull(), + () -> assertThat(transportErrorQueue).isEmpty() + ); + } + + @Test + void 인증되지_않은_사용자는_핸드셰이크를_실패한다() { + // when + Throwable thrown = catchThrowable(() -> { + stompSession = stompClient.connectAsync(url, new WebSocketHttpHeaders(), new StompHeaders(), sessionHandler).get(5, SECONDS); + }); + + // then + assertAll( + () -> assertThat(thrown) + .isInstanceOf(ExecutionException.class) + .hasCauseInstanceOf(HttpClientErrorException.Unauthorized.class), + () -> assertThat(transportErrorQueue).hasSize(1) + ); + } + } +}