From d39bef055e91160773cdff4a1818178c4c46afd4 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Thu, 7 Aug 2025 11:36:35 +0900 Subject: [PATCH 1/4] =?UTF-8?q?test:=20WebSocket=20-=20STOMP=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WebSocketStompIntegrationTest.java | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java 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..d2e64d6e2 --- /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) + ); + } + } +} From eca25f29f4f501514045349349df1bb97243d4e3 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Thu, 7 Aug 2025 12:50:30 +0900 Subject: [PATCH 2/4] =?UTF-8?q?test:=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=86=A1=EC=8B=A0=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/service/ChatServiceTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) 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..8f887bd23 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.Mockito; 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); + + Mockito.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()); + } + } } From 3a9e91807d52c55a746f7fa5676ff5c3bdd27efe Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Thu, 7 Aug 2025 12:52:19 +0900 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8F=AC=EB=A7=A4=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../websocket/WebSocketStompIntegrationTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java b/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java index d2e64d6e2..c74d84534 100644 --- a/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java @@ -90,8 +90,8 @@ public void handleTransportError(StompSession session, Throwable exception) { // then assertAll( - () -> assertThat(stompSession).isNotNull(), - () -> assertThat(transportErrorQueue).isEmpty() + () -> assertThat(stompSession).isNotNull(), + () -> assertThat(transportErrorQueue).isEmpty() ); } From d05cc9f94a2fa49b78114b77790069495351ffb0 Mon Sep 17 00:00:00 2001 From: seonghyeok Date: Sun, 10 Aug 2025 14:55:29 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20Mockito=20->=20BDDMockito=EB=A1=9C?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/solidconnection/chat/service/ChatServiceTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 8f887bd23..667a02d5e 100644 --- a/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java +++ b/src/test/java/com/example/solidconnection/chat/service/ChatServiceTest.java @@ -32,7 +32,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; +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; @@ -400,7 +400,7 @@ void setUp() { ArgumentCaptor destinationCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor payloadCaptor = ArgumentCaptor.forClass(ChatMessageSendResponse.class); - Mockito.verify(simpMessagingTemplate).convertAndSend(destinationCaptor.capture(), payloadCaptor.capture()); + BDDMockito.verify(simpMessagingTemplate).convertAndSend(destinationCaptor.capture(), payloadCaptor.capture()); assertAll( () -> assertThat(destinationCaptor.getValue()).isEqualTo("/topic/chat/" + chatRoom.getId()),