From dea12325a618ab11cd54d4c3ee0ea2dbb29d3bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B5=9C=EC=8A=B9=EC=9B=90=20Merry?= Date: Mon, 25 Mar 2024 23:33:19 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20#767=20=EC=B1=84=ED=8C=85=20=EC=9B=B9?= =?UTF-8?q?=EC=86=8C=EC=BC=93=20=EA=B5=AC=ED=98=84=20(#771)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: 웹소켓 의존성 추가 * feat: 채팅 웹소켓 인터셉터 생성 * feat: 웹소켓 세션 등록 일급컬렉션 추가 * feat: 웹소켓 configuration 추가 * feat: 웹소켓 연결 해제 기능 구현 * refactor: 인터셉터 서비스 구현 및 인가 인터셉터 분리 * feat: 웹소켓 핸들러 생성 * feat: 채팅 웹소켓 url과 인터셉터 지정 * feat: 채팅 웹소켓 url과 인터셉터 지정 * chore: 작업을 위한 임시 세팅 * feat: 채팅 웹소켓 인터셉터 생성 * feat: 웹소켓 세션 등록 일급컬렉션 추가 * feat: 웹소켓 configuration 추가 * feat: 웹소켓 연결 해제 기능 구현 * feat: 웹소켓 핸들러 생성 * feat: 채팅 웹소켓 url과 인터셉터 지정 * chore: 작업을 위한 임시 세팅 * fix: 이미지 절대 url 가져오는 문제 해결 * refactor: 웹소켓 요청 path 변경 * refactor: TextMessage 형식 변경 * refactor: 웹소켓 핸들러 추상화 * fix: 오류가 발생하는 테스트 해결 * test: 테스트 추가 - WebSocketHandleTextMessageProviderCompositeTest * refactor: map 타입에 대한 dto 생성 * refactor: 코드 리팩터링 * refactor: 로직 이동 * refactor: 채팅 알림 전송 로직을 웹 소켓쪽으로 이동 * refactor: 전송자에 대한 변수명 변경 sender -> writer * refactor: attribute 키의 상수명 변경 * refactor: 핸들링하는 메서드에 대한 이름 변경 handle -> handleCreateSendMessage * refactor: dto 변수명 수정 SendMessagesDto -> SendMessageDto SendMessageDto -> MessageDto * refactor: 기존 메시지 생성 로직 제거 * refactor: final 키워드 추가 * test: 테스트 추가 * fix: 전송할 메시지 생성 시 발신자 session으로만 전송되는 문제 수정 * refactor: 메시지 로그 업데이트 이벤트에서 수신자, 채팅방 객체 대신 id 받도록 변경 * style: 메서드 순서 정렬 * feat: 마지막 읽은 메시지 업데이트 이벤트 발행 추가 * test: 실패하는 테스트 수정 * test: 메시지 전송 시 메시지 읽음 처리 이벤트 호출 테스트 추가 * refactor: 중복되는 탈퇴 회원 검증 메서드 삭제 * style: 와일드카드 제거 * style: 불필요한 필드 삭제 * refactor: 메시지 로그 업데이트 시 마지막 메시지 아이디만 받도록 수정 --------- Co-authored-by: JJ503 <63184334+JJ503@users.noreply.github.com> --- backend/ddang/build.gradle | 3 + .../AuthenticationInterceptor.java | 43 +-- .../AuthenticationInterceptorService.java | 61 ++++ .../LastReadMessageLogService.java | 8 +- .../chat/application/MessageService.java | 35 +- .../chat/application/dto/ReadMessageDto.java | 2 +- .../event/UpdateReadMessageLogEvent.java | 6 +- .../chat/domain/WebSocketChatSessions.java | 42 +++ .../ddang/chat/domain/WebSocketSessions.java | 33 ++ ...hatWebSocketHandleTextMessageProvider.java | 141 ++++++++ .../chat/handler/dto/ChatMessageDataDto.java | 4 + .../ddang/chat/handler/dto/MessageDto.java | 27 ++ .../chat/presentation/ChatRoomController.java | 25 +- .../presentation/util/ImageRelativeUrl.java | 6 +- .../configuration/WebSocketConfiguration.java | 26 ++ .../configuration/WebSocketInterceptor.java | 47 +++ .../WebSocketHandleTextMessageProvider.java | 20 ++ ...ketHandleTextMessageProviderComposite.java | 33 ++ .../websocket/handler/WebSocketHandler.java | 46 +++ .../websocket/handler/dto/SendMessageDto.java | 7 + .../handler/dto/SessionAttributeDto.java | 4 + .../websocket/handler/dto/TextMessageDto.java | 6 + .../handler/dto/TextMessageType.java | 7 + .../UnsupportedTextMessageTypeException.java | 8 + .../src/main/resources/application-local.yml | 2 +- .../presentation/AuctionControllerTest.java | 11 +- .../AuctionQnaControllerTest.java | 11 +- .../AuctionReviewControllerTest.java | 11 +- .../bid/presentation/BidControllerTest.java | 11 +- .../chat/application/MessageServiceTest.java | 57 +--- ...astReadMessageLogEventListenerFixture.java | 2 +- .../LastReadMessageLogServiceFixture.java | 4 +- .../fixture/MessageServiceFixture.java | 7 +- .../chat/domain/WebSocketSessionsTest.java | 99 ++++++ .../fixture/WebSocketSessionsTestFixture.java | 12 + ...ebSocketHandleTextMessageProviderTest.java | 189 +++++++++++ ...tHandleTextMessageProviderTestFixture.java | 103 ++++++ .../presentation/ChatRoomControllerTest.java | 308 ++++++++---------- .../DeviceTokenControllerTest.java | 11 +- .../NotificationEventListenerTest.java | 33 +- .../NotificationEventListenerFixture.java | 16 +- .../qna/presentation/QnaControllerTest.java | 11 +- .../presentation/ReportControllerTest.java | 11 +- .../presentation/ReviewControllerTest.java | 11 +- .../UserAuctionControllerTest.java | 11 +- .../user/presentation/UserControllerTest.java | 11 +- ...andleTextMessageProviderCompositeTest.java | 50 +++ .../handler/WebSocketHandlerTest.java | 72 ++++ .../fixture/WebSocketHandlerTestFixture.java | 20 ++ 49 files changed, 1366 insertions(+), 358 deletions(-) create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/authentication/configuration/AuthenticationInterceptorService.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/chat/domain/WebSocketChatSessions.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/chat/domain/WebSocketSessions.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProvider.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/ChatMessageDataDto.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/MessageDto.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/websocket/configuration/WebSocketConfiguration.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/websocket/configuration/WebSocketInterceptor.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/WebSocketHandleTextMessageProvider.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/WebSocketHandleTextMessageProviderComposite.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/WebSocketHandler.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/SendMessageDto.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/SessionAttributeDto.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/TextMessageDto.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/TextMessageType.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/exception/UnsupportedTextMessageTypeException.java create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/chat/domain/WebSocketSessionsTest.java create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/chat/domain/fixture/WebSocketSessionsTestFixture.java create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProviderTest.java create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/chat/handler/fixture/ChatWebSocketHandleTextMessageProviderTestFixture.java create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/WebSocketHandleTextMessageProviderCompositeTest.java create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/WebSocketHandlerTest.java create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/fixture/WebSocketHandlerTestFixture.java diff --git a/backend/ddang/build.gradle b/backend/ddang/build.gradle index 14fd58159..54749b2a9 100644 --- a/backend/ddang/build.gradle +++ b/backend/ddang/build.gradle @@ -88,6 +88,9 @@ dependencies { annotationProcessor 'jakarta.annotation:jakarta.annotation-api' annotationProcessor 'jakarta.persistence:jakarta.persistence-api' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + + // web socket + implementation 'org.springframework.boot:spring-boot-starter-websocket' } tasks.withType(JavaCompile) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/authentication/configuration/AuthenticationInterceptor.java b/backend/ddang/src/main/java/com/ddang/ddang/authentication/configuration/AuthenticationInterceptor.java index e750506a9..fa4cd1529 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/authentication/configuration/AuthenticationInterceptor.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/authentication/configuration/AuthenticationInterceptor.java @@ -1,13 +1,5 @@ package com.ddang.ddang.authentication.configuration; -import com.ddang.ddang.authentication.application.AuthenticationUserService; -import com.ddang.ddang.authentication.application.BlackListTokenService; -import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; -import com.ddang.ddang.authentication.domain.TokenDecoder; -import com.ddang.ddang.authentication.domain.TokenType; -import com.ddang.ddang.authentication.domain.dto.AuthenticationStore; -import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; -import com.ddang.ddang.authentication.domain.exception.InvalidTokenException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -19,10 +11,7 @@ @RequiredArgsConstructor public class AuthenticationInterceptor implements HandlerInterceptor { - private final BlackListTokenService blackListTokenService; - private final AuthenticationUserService authenticationUserService; - private final TokenDecoder tokenDecoder; - private final AuthenticationStore store; + private final AuthenticationInterceptorService authenticationInterceptorService; @Override public boolean preHandle( @@ -31,37 +20,11 @@ public boolean preHandle( final Object handler ) { final String accessToken = request.getHeader(HttpHeaders.AUTHORIZATION); + authenticationInterceptorService.handleAccessToken(accessToken); - if (isNotRequiredAuthenticate(accessToken)) { - store.set(new AuthenticationUserInfo(null)); - return true; - } - - validateLogoutToken(accessToken); - - final PrivateClaims privateClaims = tokenDecoder.decode(TokenType.ACCESS, accessToken) - .orElseThrow(() -> - new InvalidTokenException("유효한 토큰이 아닙니다.") - ); - - if (authenticationUserService.isWithdrawal(privateClaims.userId())) { - throw new InvalidTokenException("유효한 토큰이 아닙니다."); - } - - store.set(new AuthenticationUserInfo(privateClaims.userId())); return true; } - private boolean isNotRequiredAuthenticate(final String token) { - return token == null || token.length() == 0; - } - - private void validateLogoutToken(final String accessToken) { - if (blackListTokenService.existsBlackListToken(TokenType.ACCESS, accessToken)) { - throw new InvalidTokenException("유효한 토큰이 아닙니다."); - } - } - @Override public void afterCompletion( final HttpServletRequest request, @@ -69,6 +32,6 @@ public void afterCompletion( final Object handler, final Exception ex ) { - store.remove(); + authenticationInterceptorService.removeStore(); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/authentication/configuration/AuthenticationInterceptorService.java b/backend/ddang/src/main/java/com/ddang/ddang/authentication/configuration/AuthenticationInterceptorService.java new file mode 100644 index 000000000..a0e18dc1c --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/authentication/configuration/AuthenticationInterceptorService.java @@ -0,0 +1,61 @@ +package com.ddang.ddang.authentication.configuration; + +import com.ddang.ddang.authentication.application.AuthenticationUserService; +import com.ddang.ddang.authentication.application.BlackListTokenService; +import com.ddang.ddang.authentication.domain.TokenDecoder; +import com.ddang.ddang.authentication.domain.TokenType; +import com.ddang.ddang.authentication.domain.dto.AuthenticationStore; +import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; +import com.ddang.ddang.authentication.domain.exception.InvalidTokenException; +import com.ddang.ddang.authentication.infrastructure.jwt.PrivateClaims; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AuthenticationInterceptorService { + + private final BlackListTokenService blackListTokenService; + private final AuthenticationUserService authenticationUserService; + private final TokenDecoder tokenDecoder; + private final AuthenticationStore store; + + public boolean handleAccessToken(final String accessToken) { + if (isNotRequiredAuthenticate(accessToken)) { + store.set(new AuthenticationUserInfo(null)); + return true; + } + + validateLogoutToken(accessToken); + + final PrivateClaims privateClaims = tokenDecoder.decode(TokenType.ACCESS, accessToken) + .orElseThrow(() -> + new InvalidTokenException("유효한 토큰이 아닙니다.") + ); + + if (authenticationUserService.isWithdrawal(privateClaims.userId())) { + throw new InvalidTokenException("유효한 토큰이 아닙니다."); + } + + store.set(new AuthenticationUserInfo(privateClaims.userId())); + return true; + } + + private boolean isNotRequiredAuthenticate(final String token) { + return token == null || token.length() == 0; + } + + private void validateLogoutToken(final String accessToken) { + if (blackListTokenService.existsBlackListToken(TokenType.ACCESS, accessToken)) { + throw new InvalidTokenException("유효한 토큰이 아닙니다."); + } + } + + public void removeStore() { + store.remove(); + } + + public AuthenticationUserInfo getAuthenticationUserInfo() { + return store.get(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogService.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogService.java index abba0d301..cc56cefe5 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogService.java @@ -33,14 +33,14 @@ public void create(final CreateReadMessageLogEvent createReadMessageLogEvent) { @Transactional public void update(final UpdateReadMessageLogEvent updateReadMessageLogEvent) { - final User reader = updateReadMessageLogEvent.reader(); - final ChatRoom chatRoom = updateReadMessageLogEvent.chatRoom(); - final ReadMessageLog messageLog = readMessageLogRepository.findBy(reader.getId(), chatRoom.getId()) + final Long readerId = updateReadMessageLogEvent.readerId(); + final Long chatRoomId = updateReadMessageLogEvent.chatRoomId(); + final ReadMessageLog messageLog = readMessageLogRepository.findBy(readerId, chatRoomId) .orElseThrow(() -> new ReadMessageLogNotFoundException( "메시지 조회 로그가 존재하지 않습니다." )); - messageLog.updateLastReadMessage(updateReadMessageLogEvent.lastReadMessage().getId()); + messageLog.updateLastReadMessage(updateReadMessageLogEvent.lastReadMessageId()); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java index 39e1498db..0206afdc9 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java @@ -2,11 +2,9 @@ import com.ddang.ddang.chat.application.dto.CreateMessageDto; import com.ddang.ddang.chat.application.dto.ReadMessageDto; -import com.ddang.ddang.chat.application.event.MessageNotificationEvent; import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.MessageNotFoundException; -import com.ddang.ddang.chat.application.exception.UnableToChatException; import com.ddang.ddang.chat.domain.ChatRoom; import com.ddang.ddang.chat.domain.Message; import com.ddang.ddang.chat.domain.repository.ChatRoomRepository; @@ -28,34 +26,28 @@ public class MessageService { private final ApplicationEventPublisher messageLogEventPublisher; - private final ApplicationEventPublisher messageNotificationEventPublisher; private final MessageRepository messageRepository; private final ChatRoomRepository chatRoomRepository; private final UserRepository userRepository; @Transactional - public Long create(final CreateMessageDto dto, final String profileImageAbsoluteUrl) { + public Message create(final CreateMessageDto dto) { final ChatRoom chatRoom = chatRoomRepository.findById(dto.chatRoomId()) .orElseThrow(() -> new ChatRoomNotFoundException( - "지정한 아이디에 대한 채팅방을 찾을 수 없습니다.")); + "지정한 아이디에 대한 채팅방을 찾을 수 없습니다." + )); final User writer = userRepository.findByIdWithProfileImage(dto.writerId()) .orElseThrow(() -> new UserNotFoundException( - "지정한 아이디에 대한 발신자를 찾을 수 없습니다.")); + "지정한 아이디에 대한 발신자를 찾을 수 없습니다." + )); final User receiver = userRepository.findById(dto.receiverId()) .orElseThrow(() -> new UserNotFoundException( - "지정한 아이디에 대한 수신자를 찾을 수 없습니다.")); - - if (!chatRoom.isChatAvailablePartner(receiver)) { - throw new UnableToChatException("탈퇴한 사용자에게는 메시지 전송이 불가능합니다."); - } + "지정한 아이디에 대한 수신자를 찾을 수 없습니다." + )); final Message message = dto.toEntity(chatRoom, writer, receiver); - final Message persistMessage = messageRepository.save(message); - - messageNotificationEventPublisher.publishEvent(new MessageNotificationEvent(persistMessage, profileImageAbsoluteUrl)); - - return persistMessage.getId(); + return messageRepository.save(message); } public List readAllByLastMessageId(final ReadMessageRequest request) { @@ -76,13 +68,16 @@ public List readAllByLastMessageId(final ReadMessageRequest requ ); if (!readMessages.isEmpty()) { - final Message lastReadMessage = readMessages.get(readMessages.size() - 1); - - messageLogEventPublisher.publishEvent(new UpdateReadMessageLogEvent(reader, chatRoom, lastReadMessage)); + final UpdateReadMessageLogEvent updateReadMessageLogEvent = new UpdateReadMessageLogEvent( + reader.getId(), + chatRoom.getId(), + readMessages.get(readMessages.size() - 1).getId() + ); + messageLogEventPublisher.publishEvent(updateReadMessageLogEvent); } return readMessages.stream() - .map(message -> ReadMessageDto.from(message, chatRoom)) + .map(message -> ReadMessageDto.of(message, chatRoom)) .toList(); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadMessageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadMessageDto.java index c6050ec9b..a198011e5 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadMessageDto.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/dto/ReadMessageDto.java @@ -14,7 +14,7 @@ public record ReadMessageDto( String contents ) { - public static ReadMessageDto from( + public static ReadMessageDto of( final Message message, final ChatRoom chatRoom ) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/UpdateReadMessageLogEvent.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/UpdateReadMessageLogEvent.java index 8fd1cde98..7a6be4ac2 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/UpdateReadMessageLogEvent.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/event/UpdateReadMessageLogEvent.java @@ -1,8 +1,4 @@ package com.ddang.ddang.chat.application.event; -import com.ddang.ddang.chat.domain.ChatRoom; -import com.ddang.ddang.chat.domain.Message; -import com.ddang.ddang.user.domain.User; - -public record UpdateReadMessageLogEvent(User reader, ChatRoom chatRoom, Message lastReadMessage) { +public record UpdateReadMessageLogEvent(Long readerId, Long chatRoomId, Long lastReadMessageId) { } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/WebSocketChatSessions.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/WebSocketChatSessions.java new file mode 100644 index 000000000..196903d31 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/WebSocketChatSessions.java @@ -0,0 +1,42 @@ +package com.ddang.ddang.chat.domain; + +import lombok.Getter; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import static com.ddang.ddang.chat.domain.WebSocketSessions.CHAT_ROOM_ID_KEY; + +@Getter +@Component +public class WebSocketChatSessions { + + private final Map chatRoomSessions = new ConcurrentHashMap<>(); + + public void add(final WebSocketSession session, final Long chatRoomId) { + chatRoomSessions.putIfAbsent(chatRoomId, new WebSocketSessions()); + final WebSocketSessions webSocketSessions = chatRoomSessions.get(chatRoomId); + webSocketSessions.putIfAbsent(session, chatRoomId); + } + + public Set getSessionsByChatRoomId(final Long chatRoomId) { + final WebSocketSessions webSocketSessions = chatRoomSessions.get(chatRoomId); + + return webSocketSessions.getSessions(); + } + + public boolean containsByUserId(final Long chatRoomId, final Long userId) { + final WebSocketSessions webSocketSessions = chatRoomSessions.get(chatRoomId); + + return webSocketSessions.contains(userId); + } + + public void remove(final WebSocketSession session) { + final long chatRoomId = Long.parseLong(String.valueOf(session.getAttributes().get(CHAT_ROOM_ID_KEY))); + final WebSocketSessions webSocketSessions = chatRoomSessions.get(chatRoomId); + webSocketSessions.remove(session); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/WebSocketSessions.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/WebSocketSessions.java new file mode 100644 index 000000000..7619377a5 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/domain/WebSocketSessions.java @@ -0,0 +1,33 @@ +package com.ddang.ddang.chat.domain; + +import lombok.Getter; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +@Getter +public class WebSocketSessions { + + protected static final String CHAT_ROOM_ID_KEY = "chatRoomId"; + private static final String USER_ID_KEY = "userId"; + + private final Set sessions = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + public void putIfAbsent(final WebSocketSession session, final Long chatRoomId) { + if (!sessions.contains(session)) { + session.getAttributes().put(CHAT_ROOM_ID_KEY, chatRoomId); + sessions.add(session); + } + } + + public boolean contains(final Long userId) { + return sessions.stream() + .anyMatch(session -> session.getAttributes().get(USER_ID_KEY) == userId); + } + + public void remove(final WebSocketSession session) { + sessions.remove(session); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProvider.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProvider.java new file mode 100644 index 000000000..d9f69098d --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProvider.java @@ -0,0 +1,141 @@ +package com.ddang.ddang.chat.handler; + +import com.ddang.ddang.chat.application.MessageService; +import com.ddang.ddang.chat.application.dto.CreateMessageDto; +import com.ddang.ddang.chat.application.event.MessageNotificationEvent; +import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; +import com.ddang.ddang.chat.domain.Message; +import com.ddang.ddang.chat.domain.WebSocketChatSessions; +import com.ddang.ddang.chat.handler.dto.ChatMessageDataDto; +import com.ddang.ddang.chat.handler.dto.MessageDto; +import com.ddang.ddang.chat.presentation.dto.request.CreateMessageRequest; +import com.ddang.ddang.websocket.handler.WebSocketHandleTextMessageProvider; +import com.ddang.ddang.websocket.handler.dto.SendMessageDto; +import com.ddang.ddang.websocket.handler.dto.SessionAttributeDto; +import com.ddang.ddang.websocket.handler.dto.TextMessageType; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Component +@RequiredArgsConstructor +public class ChatWebSocketHandleTextMessageProvider implements WebSocketHandleTextMessageProvider { + + private final WebSocketChatSessions sessions; + private final ObjectMapper objectMapper; + private final MessageService messageService; + private final ApplicationEventPublisher messageNotificationEventPublisher; + private final ApplicationEventPublisher messageLogEventPublisher; + + @Override + public TextMessageType supportTextMessageType() { + return TextMessageType.CHATTINGS; + } + + @Override + public List handleCreateSendMessage( + final WebSocketSession session, + final Map data + ) throws JsonProcessingException { + final SessionAttributeDto sessionAttribute = getSessionAttributes(session); + final ChatMessageDataDto messageData = objectMapper.convertValue(data, ChatMessageDataDto.class); + sessions.add(session, messageData.chatRoomId()); + + final Long writerId = sessionAttribute.userId(); + final CreateMessageDto createMessageDto = createMessageDto(messageData, writerId); + final Message message = messageService.create(createMessageDto); + sendNotificationIfReceiverNotInSession(message, sessionAttribute); + + return createSendMessages(message, writerId, createMessageDto.chatRoomId()); + } + + private SessionAttributeDto getSessionAttributes(final WebSocketSession session) { + final Map attributes = session.getAttributes(); + + return objectMapper.convertValue(attributes, SessionAttributeDto.class); + } + + private CreateMessageDto createMessageDto(final ChatMessageDataDto messageData, final Long userId) { + final CreateMessageRequest request = new CreateMessageRequest( + messageData.receiverId(), + messageData.contents() + ); + + return CreateMessageDto.of(userId, messageData.chatRoomId(), request); + } + + private void sendNotificationIfReceiverNotInSession( + final Message message, + final SessionAttributeDto sessionAttribute + ) { + if (!sessions.containsByUserId(message.getChatRoom().getId(), message.getReceiver().getId())) { + final String profileImageAbsoluteUrl = String.valueOf(sessionAttribute.baseUrl()); + messageNotificationEventPublisher.publishEvent(new MessageNotificationEvent( + message, + profileImageAbsoluteUrl + )); + } + } + + private List createSendMessages( + final Message message, + final Long writerId, + final Long chatRoomId + ) throws JsonProcessingException { + final Set groupSessions = sessions.getSessionsByChatRoomId(message.getChatRoom().getId()); + + final List sendMessageDtos = new ArrayList<>(); + for (final WebSocketSession currentSession : groupSessions) { + final TextMessage textMessage = createTextMessage(message, writerId, currentSession); + sendMessageDtos.add(new SendMessageDto(currentSession, textMessage)); + updateReadMessageLog(currentSession, chatRoomId, message); + } + + return sendMessageDtos; + } + + private TextMessage createTextMessage( + final Message message, + final Long writerId, + final WebSocketSession session + ) throws JsonProcessingException { + final boolean isMyMessage = isMyMessage(session, writerId); + final MessageDto messageDto = MessageDto.of(message, isMyMessage); + + return new TextMessage(objectMapper.writeValueAsString(messageDto)); + } + + private boolean isMyMessage(final WebSocketSession session, final Long writerId) { + final long userId = Long.parseLong(String.valueOf(session.getAttributes().get("userId"))); + + return writerId.equals(userId); + } + + private void updateReadMessageLog( + final WebSocketSession currentSession, + final Long chatRoomId, + final Message message + ) { + final SessionAttributeDto sessionAttributes = getSessionAttributes(currentSession); + final UpdateReadMessageLogEvent updateReadMessageLogEvent = new UpdateReadMessageLogEvent( + sessionAttributes.userId(), + chatRoomId, + message.getId() + ); + messageLogEventPublisher.publishEvent(updateReadMessageLogEvent); + } + + @Override + public void remove(final WebSocketSession session) { + sessions.remove(session); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/ChatMessageDataDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/ChatMessageDataDto.java new file mode 100644 index 000000000..169982651 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/ChatMessageDataDto.java @@ -0,0 +1,4 @@ +package com.ddang.ddang.chat.handler.dto; + +public record ChatMessageDataDto(Long chatRoomId, Long receiverId, String contents) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/MessageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/MessageDto.java new file mode 100644 index 000000000..30e7e5512 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/MessageDto.java @@ -0,0 +1,27 @@ +package com.ddang.ddang.chat.handler.dto; + +import com.ddang.ddang.chat.domain.Message; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.time.LocalDateTime; + +public record MessageDto( + Long id, + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul") + LocalDateTime createdAt, + + boolean isMyMessage, + + String contents +) { + + public static MessageDto of(final Message message, final boolean isMyMessage) { + return new MessageDto( + message.getId(), + message.getCreatedTime(), + isMyMessage, + message.getContents() + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/ChatRoomController.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/ChatRoomController.java index 8067019e9..83b37f227 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/ChatRoomController.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/presentation/ChatRoomController.java @@ -5,19 +5,15 @@ import com.ddang.ddang.chat.application.ChatRoomService; import com.ddang.ddang.chat.application.MessageService; import com.ddang.ddang.chat.application.dto.CreateChatRoomDto; -import com.ddang.ddang.chat.application.dto.CreateMessageDto; import com.ddang.ddang.chat.application.dto.ReadChatRoomWithLastMessageDto; import com.ddang.ddang.chat.application.dto.ReadMessageDto; import com.ddang.ddang.chat.application.dto.ReadParticipatingChatRoomDto; import com.ddang.ddang.chat.presentation.dto.request.CreateChatRoomRequest; -import com.ddang.ddang.chat.presentation.dto.request.CreateMessageRequest; import com.ddang.ddang.chat.presentation.dto.request.ReadMessageRequest; import com.ddang.ddang.chat.presentation.dto.response.CreateChatRoomResponse; -import com.ddang.ddang.chat.presentation.dto.response.CreateMessageResponse; import com.ddang.ddang.chat.presentation.dto.response.ReadChatRoomResponse; import com.ddang.ddang.chat.presentation.dto.response.ReadChatRoomWithLastMessageResponse; import com.ddang.ddang.chat.presentation.dto.response.ReadMessageResponse; -import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -72,28 +68,15 @@ public ResponseEntity readChatRoomById( @AuthenticateUser final AuthenticationUserInfo userInfo, @PathVariable final Long chatRoomId ) { - final ReadParticipatingChatRoomDto chatRoomDto = chatRoomService.readByChatRoomId(chatRoomId, userInfo.userId()); + final ReadParticipatingChatRoomDto chatRoomDto = chatRoomService.readByChatRoomId( + chatRoomId, + userInfo.userId() + ); final ReadChatRoomResponse response = ReadChatRoomResponse.from(chatRoomDto); return ResponseEntity.ok(response); } - @PostMapping("/{chatRoomId}/messages") - public ResponseEntity createMessage( - @AuthenticateUser final AuthenticationUserInfo userInfo, - @PathVariable final Long chatRoomId, - @RequestBody @Valid final CreateMessageRequest request - ) { - - final Long messageId = messageService.create( - CreateMessageDto.of(userInfo.userId(), chatRoomId, request), ImageRelativeUrl.USER.calculateAbsoluteUrl() - ); - final CreateMessageResponse response = new CreateMessageResponse(messageId); - - return ResponseEntity.created(URI.create("/chattings/" + chatRoomId)) - .body(response); - } - @GetMapping("/{chatRoomId}/messages") public ResponseEntity> readAllByLastMessageId( @AuthenticateUser final AuthenticationUserInfo userInfo, diff --git a/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/util/ImageRelativeUrl.java b/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/util/ImageRelativeUrl.java index 854b6f813..f40c18874 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/util/ImageRelativeUrl.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/image/presentation/util/ImageRelativeUrl.java @@ -1,7 +1,9 @@ package com.ddang.ddang.image.presentation.util; +import lombok.Getter; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +@Getter public enum ImageRelativeUrl { AUCTION("/auctions/images/"), @@ -20,8 +22,4 @@ public String calculateAbsoluteUrl() { return imageBaseUrl + value; } - - public String getValue() { - return value; - } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/websocket/configuration/WebSocketConfiguration.java b/backend/ddang/src/main/java/com/ddang/ddang/websocket/configuration/WebSocketConfiguration.java new file mode 100644 index 000000000..534646c15 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/websocket/configuration/WebSocketConfiguration.java @@ -0,0 +1,26 @@ +package com.ddang.ddang.websocket.configuration; + +import com.ddang.ddang.websocket.handler.WebSocketHandler; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +@Configuration +@EnableWebSocket +public class WebSocketConfiguration implements WebSocketConfigurer { + + private final WebSocketHandler handler; + private final WebSocketInterceptor interceptor; + + public WebSocketConfiguration(final WebSocketHandler handler, final WebSocketInterceptor interceptor) { + this.handler = handler; + this.interceptor = interceptor; + } + + @Override + public void registerWebSocketHandlers(final WebSocketHandlerRegistry registry) { + registry.addHandler(handler, "/websocket") + .addInterceptors(interceptor); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/websocket/configuration/WebSocketInterceptor.java b/backend/ddang/src/main/java/com/ddang/ddang/websocket/configuration/WebSocketInterceptor.java new file mode 100644 index 000000000..59326ce22 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/websocket/configuration/WebSocketInterceptor.java @@ -0,0 +1,47 @@ +package com.ddang.ddang.websocket.configuration; + +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptorService; +import com.ddang.ddang.authentication.configuration.exception.UserUnauthorizedException; +import com.ddang.ddang.authentication.domain.dto.AuthenticationUserInfo; +import com.ddang.ddang.image.presentation.util.ImageRelativeUrl; +import lombok.RequiredArgsConstructor; +import org.apache.http.HttpHeaders; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; + +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class WebSocketInterceptor extends HttpSessionHandshakeInterceptor { + + private final AuthenticationInterceptorService authenticationInterceptorService; + + @Override + public boolean beforeHandshake( + final ServerHttpRequest request, + final ServerHttpResponse response, + final WebSocketHandler wsHandler, + final Map attributes + ) throws Exception { + attributes.put("userId", findUserId(request)); + attributes.put("baseUrl", ImageRelativeUrl.USER.calculateAbsoluteUrl()); + + return super.beforeHandshake(request, response, wsHandler, attributes); + } + + private Long findUserId(final ServerHttpRequest request) { + final String accessToken = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + authenticationInterceptorService.handleAccessToken(accessToken); + + final AuthenticationUserInfo authenticationUserInfo = authenticationInterceptorService.getAuthenticationUserInfo(); + if (authenticationUserInfo.userId() == null) { + throw new UserUnauthorizedException("로그인이 필요한 기능입니다."); + } + + return authenticationUserInfo.userId(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/WebSocketHandleTextMessageProvider.java b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/WebSocketHandleTextMessageProvider.java new file mode 100644 index 000000000..54551d108 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/WebSocketHandleTextMessageProvider.java @@ -0,0 +1,20 @@ +package com.ddang.ddang.websocket.handler; + +import com.ddang.ddang.websocket.handler.dto.SendMessageDto; +import com.ddang.ddang.websocket.handler.dto.TextMessageType; +import org.springframework.web.socket.WebSocketSession; + +import java.util.List; +import java.util.Map; + +public interface WebSocketHandleTextMessageProvider { + + TextMessageType supportTextMessageType(); + + List handleCreateSendMessage( + final WebSocketSession session, + final Map data + ) throws Exception; + + void remove(final WebSocketSession session); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/WebSocketHandleTextMessageProviderComposite.java b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/WebSocketHandleTextMessageProviderComposite.java new file mode 100644 index 000000000..0822c4b18 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/WebSocketHandleTextMessageProviderComposite.java @@ -0,0 +1,33 @@ +package com.ddang.ddang.websocket.handler; + +import com.ddang.ddang.websocket.handler.dto.TextMessageType; +import com.ddang.ddang.websocket.handler.exception.UnsupportedTextMessageTypeException; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +public class WebSocketHandleTextMessageProviderComposite { + + private final Map mappings; + + public WebSocketHandleTextMessageProviderComposite(final Set providers) { + this.mappings = providers.stream() + .collect(Collectors.toMap( + WebSocketHandleTextMessageProvider::supportTextMessageType, + provider -> provider + )); + } + + public WebSocketHandleTextMessageProvider findProvider(final TextMessageType textMessageType) { + final WebSocketHandleTextMessageProvider provider = mappings.get(textMessageType); + + if (provider == null) { + throw new UnsupportedTextMessageTypeException("지원하는 웹 소켓 통신 타입이 아닙니다."); + } + + return provider; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/WebSocketHandler.java b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/WebSocketHandler.java new file mode 100644 index 000000000..c72936775 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/WebSocketHandler.java @@ -0,0 +1,46 @@ +package com.ddang.ddang.websocket.handler; + +import com.ddang.ddang.websocket.handler.dto.SendMessageDto; +import com.ddang.ddang.websocket.handler.dto.TextMessageDto; +import com.ddang.ddang.websocket.handler.dto.TextMessageType; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class WebSocketHandler extends TextWebSocketHandler { + + private static final String TYPE_KEY = "type"; + + private final WebSocketHandleTextMessageProviderComposite providerComposite; + private final ObjectMapper objectMapper; + + @Override + protected void handleTextMessage(final WebSocketSession session, final TextMessage message) throws Exception { + final String payload = message.getPayload(); + final TextMessageDto textMessageDto = objectMapper.readValue(payload, TextMessageDto.class); + session.getAttributes().put(TYPE_KEY, textMessageDto.type()); + + final WebSocketHandleTextMessageProvider provider = providerComposite.findProvider(textMessageDto.type()); + final List sendMessageDtos = provider.handleCreateSendMessage(session, textMessageDto.data()); + for (SendMessageDto sendMessageDto : sendMessageDtos) { + sendMessageDto.session() + .sendMessage(sendMessageDto.textMessage()); + } + } + + @Override + public void afterConnectionClosed(final WebSocketSession session, final CloseStatus status) { + final String type = String.valueOf(session.getAttributes().get(TYPE_KEY)); + final TextMessageType textMessageType = TextMessageType.valueOf(type); + final WebSocketHandleTextMessageProvider provider = providerComposite.findProvider(textMessageType); + provider.remove(session); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/SendMessageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/SendMessageDto.java new file mode 100644 index 000000000..3b59ab57a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/SendMessageDto.java @@ -0,0 +1,7 @@ +package com.ddang.ddang.websocket.handler.dto; + +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +public record SendMessageDto(WebSocketSession session, TextMessage textMessage) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/SessionAttributeDto.java b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/SessionAttributeDto.java new file mode 100644 index 000000000..d175b0eb3 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/SessionAttributeDto.java @@ -0,0 +1,4 @@ +package com.ddang.ddang.websocket.handler.dto; + +public record SessionAttributeDto(Long userId, String baseUrl) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/TextMessageDto.java b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/TextMessageDto.java new file mode 100644 index 000000000..b15407802 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/TextMessageDto.java @@ -0,0 +1,6 @@ +package com.ddang.ddang.websocket.handler.dto; + +import java.util.Map; + +public record TextMessageDto(TextMessageType type, Map data) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/TextMessageType.java b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/TextMessageType.java new file mode 100644 index 000000000..70fc40615 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/TextMessageType.java @@ -0,0 +1,7 @@ +package com.ddang.ddang.websocket.handler.dto; + +public enum TextMessageType { + + CHATTINGS, + BIDS +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/exception/UnsupportedTextMessageTypeException.java b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/exception/UnsupportedTextMessageTypeException.java new file mode 100644 index 000000000..b5af6dec0 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/exception/UnsupportedTextMessageTypeException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.websocket.handler.exception; + +public class UnsupportedTextMessageTypeException extends IllegalStateException { + + public UnsupportedTextMessageTypeException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/resources/application-local.yml b/backend/ddang/src/main/resources/application-local.yml index e6c8bb7d1..cc2538562 100644 --- a/backend/ddang/src/main/resources/application-local.yml +++ b/backend/ddang/src/main/resources/application-local.yml @@ -6,7 +6,7 @@ spring: jpa: hibernate: - ddl-auto: none + ddl-auto: create properties: hibernate: format_sql: true diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionControllerTest.java index 56b5b9669..071a3920c 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionControllerTest.java @@ -33,6 +33,7 @@ import com.ddang.ddang.auction.presentation.dto.request.ReadAuctionSearchCondition; import com.ddang.ddang.auction.presentation.fixture.AuctionControllerFixture; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptorService; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; @@ -70,10 +71,12 @@ void setUp() { final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( - blackListTokenService, - authenticationUserService, - mockTokenDecoder, - store + new AuthenticationInterceptorService( + blackListTokenService, + authenticationUserService, + mockTokenDecoder, + store + ) ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionQnaControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionQnaControllerTest.java index 0ed09b1ef..7ef1e8b47 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionQnaControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionQnaControllerTest.java @@ -3,6 +3,7 @@ import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; import com.ddang.ddang.auction.presentation.fixture.AuctionQuestionControllerFixture; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptorService; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; @@ -46,10 +47,12 @@ void setUp() { final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( - blackListTokenService, - authenticationUserService, - tokenDecoder, - store + new AuthenticationInterceptorService( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ) ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionReviewControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionReviewControllerTest.java index d6e558ff3..228303204 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionReviewControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/auction/presentation/AuctionReviewControllerTest.java @@ -2,6 +2,7 @@ import com.ddang.ddang.auction.presentation.fixture.AuctionReviewControllerFixture; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptorService; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; @@ -48,10 +49,12 @@ void setUp() { final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( - blackListTokenService, - authenticationUserService, - tokenDecoder, - store + new AuthenticationInterceptorService( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ) ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/BidControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/BidControllerTest.java index 6ec3319da..a1657b310 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/BidControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/bid/presentation/BidControllerTest.java @@ -2,6 +2,7 @@ import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptorService; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; @@ -62,10 +63,12 @@ void setUp() { final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( - blackListTokenService, - authenticationUserService, - tokenDecoder, - store + new AuthenticationInterceptorService( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ) ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java index 7794679c5..8bfac5ec1 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/MessageServiceTest.java @@ -1,18 +1,15 @@ package com.ddang.ddang.chat.application; import com.ddang.ddang.chat.application.dto.ReadMessageDto; -import com.ddang.ddang.chat.application.event.MessageNotificationEvent; import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.MessageNotFoundException; import com.ddang.ddang.chat.application.fixture.MessageServiceFixture; +import com.ddang.ddang.chat.domain.Message; import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; import com.ddang.ddang.configuration.IsolateDatabase; -import com.ddang.ddang.notification.application.NotificationService; -import com.ddang.ddang.notification.application.dto.CreateNotificationDto; -import com.ddang.ddang.notification.domain.NotificationStatus; import com.ddang.ddang.user.application.exception.UserNotFoundException; -import com.google.firebase.messaging.FirebaseMessagingException; +import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; @@ -26,7 +23,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willDoNothing; @IsolateDatabase @@ -41,9 +37,6 @@ class MessageServiceTest extends MessageServiceFixture { @Autowired ReadMessageLogRepository readMessageLogRepository; - @MockBean - NotificationService notificationService; - @MockBean LastReadMessageLogService lastReadMessageLogService; @@ -51,43 +44,24 @@ class MessageServiceTest extends MessageServiceFixture { ApplicationEvents events; @Test - void 메시지를_생성한다() throws FirebaseMessagingException { - //given - given(notificationService.send(any(CreateNotificationDto.class))).willReturn(NotificationStatus.SUCCESS); - - // when - final Long messageId = messageService.create(메시지_생성_DTO, 이미지_절대_경로); - - // then - assertThat(messageId).isPositive(); - } - - @Test - void 메시지를_생성하고_알림을_보낸다() { - // when - messageService.create(메시지_생성_DTO, 이미지_절대_경로); - final long actual = events.stream(MessageNotificationEvent.class).count(); - - // then - assertThat(actual).isEqualTo(1); - } - - @Test - void 알림전송에_실패한_경우에도_정상적으로_메시지가_저장된다() throws FirebaseMessagingException { - // given - given(notificationService.send(any(CreateNotificationDto.class))).willReturn(NotificationStatus.FAIL); - + void 메시지를_생성한다() { // when - final Long actual = messageService.create(메시지_생성_DTO, 이미지_절대_경로); + final Message actual = messageService.create(메시지_생성_DTO); // then - assertThat(actual).isPositive(); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(actual.getId()).isPositive(); + softAssertions.assertThat(actual.getChatRoom()).isEqualTo(채팅방); + softAssertions.assertThat(actual.getWriter()).isEqualTo(발신자); + softAssertions.assertThat(actual.getReceiver()).isEqualTo(수신자); + softAssertions.assertThat(actual.getContents()).isEqualTo(메시지_생성_DTO.contents()); + }); } @Test void 채팅방이_없는_경우_메시지를_생성하면_예외가_발생한다() { // when & then - assertThatThrownBy(() -> messageService.create(유효하지_않은_채팅방의_메시지_생성_DTO, 이미지_절대_경로)) + assertThatThrownBy(() -> messageService.create(유효하지_않은_채팅방의_메시지_생성_DTO)) .isInstanceOf(ChatRoomNotFoundException.class) .hasMessageContaining("지정한 아이디에 대한 채팅방을 찾을 수 없습니다."); } @@ -95,7 +69,7 @@ class MessageServiceTest extends MessageServiceFixture { @Test void 발신자가_없는_경우_메시지를_생성하면_예외가_발생한다() { // when & then - assertThatThrownBy(() -> messageService.create(유효하지_않은_발신자의_메시지_생성_DTO, 이미지_절대_경로)) + assertThatThrownBy(() -> messageService.create(유효하지_않은_발신자의_메시지_생성_DTO)) .isInstanceOf(UserNotFoundException.class) .hasMessageContaining("지정한 아이디에 대한 발신자를 찾을 수 없습니다."); } @@ -103,7 +77,7 @@ class MessageServiceTest extends MessageServiceFixture { @Test void 수신자가_없는_경우_메시지를_생성하면_예외가_발생한다() { // when & then - assertThatThrownBy(() -> messageService.create(유효하지_않은_수신자의_메시지_생성_DTO, 이미지_절대_경로)) + assertThatThrownBy(() -> messageService.create(유효하지_않은_수신자의_메시지_생성_DTO)) .isInstanceOf(UserNotFoundException.class) .hasMessageContaining("지정한 아이디에 대한 수신자를 찾을 수 없습니다."); } @@ -135,7 +109,8 @@ class MessageServiceTest extends MessageServiceFixture { @Test void 마지막으로_조회된_메시지_이후에_추가된_메시지가_없는_경우_빈_리스트를_반환한다() { // when - final List readMessageDtos = messageService.readAllByLastMessageId(조회할_메시지가_더이상_없는_메시지_조회용_request); + final List readMessageDtos = messageService.readAllByLastMessageId( + 조회할_메시지가_더이상_없는_메시지_조회용_request); // then assertThat(readMessageDtos).isEmpty(); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogEventListenerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogEventListenerFixture.java index dc0bd5c35..001b61bb1 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogEventListenerFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogEventListenerFixture.java @@ -57,7 +57,7 @@ void setUp() { .contents("메시지") .build(); - 업데이트용_메시지_조회_로그 = new UpdateReadMessageLogEvent(메시지_로그_생성용_발신자_겸_판매자, 메시지_로그_생성용_채팅방, 메시지); + 업데이트용_메시지_조회_로그 = new UpdateReadMessageLogEvent(메시지_로그_생성용_발신자_겸_판매자.getId(), 메시지_로그_생성용_채팅방.getId(), 메시지.getId()); 생성용_메시지_조회_로그 = new CreateReadMessageLogEvent(메시지_로그_생성용_채팅방); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogServiceFixture.java index 737969fd0..2ee5e267c 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogServiceFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/LastReadMessageLogServiceFixture.java @@ -177,8 +177,8 @@ void fixtureSetUp( final ReadMessageLog 메시지_로그_업데이트용_로그_구매자 = new ReadMessageLog(메시지_로그_업데이트용_채팅방, 메시지_로그_업데이트용_입찰자); readMessageLogRepository.saveAll(List.of(메시지_로그_업데이트용_로그_판매자, 메시지_로그_업데이트용_로그_구매자)); - 메시지_로그_업데이트용_이벤트 = new UpdateReadMessageLogEvent(메시지_로그_업데이트용_발신자_겸_판매자, 메시지_로그_업데이트용_채팅방, 메시지_로그_업데이트용_마지막_조회_메시지); + 메시지_로그_업데이트용_이벤트 = new UpdateReadMessageLogEvent(메시지_로그_업데이트용_발신자_겸_판매자.getId(), 메시지_로그_업데이트용_채팅방.getId(), 메시지_로그_업데이트용_마지막_조회_메시지.getId()); - 유효하지_않는_메시지_조회_로그 = new UpdateReadMessageLogEvent(저장되지_않은_사용자, 저장되지_않은_채팅방, 저장되지_않은_메시지); + 유효하지_않는_메시지_조회_로그 = new UpdateReadMessageLogEvent(저장되지_않은_사용자.getId(), 저장되지_않은_채팅방.getId(), 저장되지_않은_메시지.getId()); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java index 01765bf70..487bde116 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/application/fixture/MessageServiceFixture.java @@ -53,11 +53,12 @@ public class MessageServiceFixture { protected ReadMessageRequest 유효하지_않은_사용자의_메시지_조회용_request; protected ReadMessageRequest 유효하지_않은_채팅방의_메시지_조회용_request; protected ReadMessageRequest 존재하지_않는_마지막_메시지_아이디의_메시지_조회용_request; + protected ChatRoom 채팅방; protected User 발신자; + protected User 수신자; protected ChatRoom 메시지가_5개인_채팅방; protected Message 메시지가_5개인_채팅방_메시지의_마지막_메시지; - protected String 이미지_절대_경로 = "/imageUrl"; protected int 메시지_총_개수 = 10; @BeforeEach @@ -82,7 +83,7 @@ void setUp() { .reliability(new Reliability(4.7d)) .oauthId("12345") .build(); - final User 수신자 = User.builder() + 수신자 = User.builder() .name("수신자") .profileImage(new ProfileImage("upload.png", "store.png")) .reliability(new Reliability(4.7d)) @@ -99,7 +100,7 @@ void setUp() { userRepository.save(수신자); userRepository.save(탈퇴한_사용자); - final ChatRoom 채팅방 = new ChatRoom(경매, 발신자); + 채팅방 = new ChatRoom(경매, 발신자); final ChatRoom 탈퇴한_사용자와의_채팅방 = new ChatRoom(경매, 탈퇴한_사용자); 메시지가_5개인_채팅방 = new ChatRoom(경매, 발신자); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/domain/WebSocketSessionsTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/domain/WebSocketSessionsTest.java new file mode 100644 index 000000000..15fe5846a --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/domain/WebSocketSessionsTest.java @@ -0,0 +1,99 @@ +package com.ddang.ddang.chat.domain; + +import com.ddang.ddang.chat.domain.fixture.WebSocketSessionsTestFixture; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.web.socket.WebSocketSession; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class WebSocketSessionsTest extends WebSocketSessionsTestFixture { + + WebSocketSessions sessions; + + WebSocketSession session; + + @BeforeEach + void setUp() { + sessions = new WebSocketSessions(); + session = mock(WebSocketSession.class); + } + + @Test + void 저장되지_않은_세션이라면_추가한다() { + // given + given(session.getAttributes()).willReturn(세션_attribute_정보); + + // when + sessions.putIfAbsent(session, 채팅방_아이디); + + // then + final boolean actual = sessions.getSessions().contains(session); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(sessions.getSessions()).hasSize(1); + softAssertions.assertThat(actual).isTrue(); + }); + } + + @Test + void 이미_저장된_세션이라면_추가하지_않는다() { + // given + given(session.getAttributes()).willReturn(세션_attribute_정보); + sessions.getSessions().add(session); + + // when + sessions.putIfAbsent(session, 채팅방_아이디); + + // then + final boolean actual = sessions.getSessions().contains(session); + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(sessions.getSessions()).hasSize(1); + softAssertions.assertThat(actual).isTrue(); + }); + } + + @Test + void 이미_저장된_세선이라면_참을_반환한다() { + // given + given(session.getAttributes()).willReturn(세션_attribute_정보); + sessions.getSessions().add(session); + + // when + final boolean actual = sessions.contains(사용자_아이디); + + // then + assertThat(actual).isTrue(); + } + + @Test + void 저장되지_않은_세선이라면_거짓을_반환한다() { + // given + given(session.getAttributes()).willReturn(세션_attribute_정보); + + // when + final boolean actual = sessions.contains(사용자_아이디); + + // then + assertThat(actual).isFalse(); + } + + @Test + void 세션을_제거한다() { + // given + given(session.getAttributes()).willReturn(세션_attribute_정보); + + // when + sessions.remove(session); + + // then + final boolean actual = sessions.getSessions().contains(session); + assertThat(actual).isFalse(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/domain/fixture/WebSocketSessionsTestFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/domain/fixture/WebSocketSessionsTestFixture.java new file mode 100644 index 000000000..1e96befe9 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/domain/fixture/WebSocketSessionsTestFixture.java @@ -0,0 +1,12 @@ +package com.ddang.ddang.chat.domain.fixture; + +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("NonAsciiCharacters") +public class WebSocketSessionsTestFixture { + + protected Long 사용자_아이디 = 1L; + protected Map 세션_attribute_정보 = new HashMap<>(Map.of("userId", 사용자_아이디, "baseUrl", "/images")); + protected Long 채팅방_아이디 = 1L; +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProviderTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProviderTest.java new file mode 100644 index 000000000..dd7c99cd9 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/ChatWebSocketHandleTextMessageProviderTest.java @@ -0,0 +1,189 @@ +package com.ddang.ddang.chat.handler; + +import com.ddang.ddang.chat.application.event.MessageNotificationEvent; +import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; +import com.ddang.ddang.chat.domain.WebSocketChatSessions; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; +import com.ddang.ddang.chat.handler.fixture.ChatWebSocketHandleTextMessageProviderTestFixture; +import com.ddang.ddang.configuration.IsolateDatabase; +import com.ddang.ddang.notification.application.NotificationService; +import com.ddang.ddang.notification.application.dto.CreateNotificationDto; +import com.ddang.ddang.notification.domain.NotificationStatus; +import com.ddang.ddang.websocket.handler.dto.SendMessageDto; +import com.ddang.ddang.websocket.handler.dto.TextMessageType; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.test.context.event.ApplicationEvents; +import org.springframework.test.context.event.RecordApplicationEvents; +import org.springframework.web.socket.WebSocketSession; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.BDDMockito.willReturn; + +@IsolateDatabase +@RecordApplicationEvents +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class ChatWebSocketHandleTextMessageProviderTest extends ChatWebSocketHandleTextMessageProviderTestFixture { + + @Autowired + ChatWebSocketHandleTextMessageProvider provider; + + @Autowired + ReadMessageLogRepository readMessageLogRepository; + + @SpyBean + WebSocketChatSessions sessions; + + @Mock + WebSocketSession writerSession; + + @Mock + WebSocketSession receiverSession; + + @MockBean + NotificationService notificationService; + + @Autowired + ApplicationEvents events; + + @Autowired + ObjectMapper objectMapper; + + @Test + void 지원하는_웹소켓_핸들링_타입을_반환한다() { + // when + final TextMessageType actual = provider.supportTextMessageType(); + + // then + assertThat(actual).isEqualTo(TextMessageType.CHATTINGS); + } + + @Test + void 메시지_생성시_수신자가_웹소켓_통신_중이라면_발신자_수신자_모두에게_메시지를_보낸다() throws JsonProcessingException { + // given + given(writerSession.getAttributes()).willReturn(발신자_세션_attribute_정보); + given(receiverSession.getAttributes()).willReturn(수신자_세션_attribute_정보); + willDoNothing().given(sessions).add(writerSession, 채팅방.getId()); + willReturn(true).given(sessions).containsByUserId(채팅방.getId(), 수신자.getId()); + willReturn(Set.of(writerSession, receiverSession)).given(sessions).getSessionsByChatRoomId(채팅방.getId()); + + // when + final List actual = provider.handleCreateSendMessage(writerSession, 메시지_전송_데이터); + + // then + assertThat(actual).hasSize(2); + } + + @Test + void 메시지_생성시_수신자가_웹소켓_통신_중이라면_메시지_로그_업데이트_메서드를_호출한다() throws JsonProcessingException { + // given + 메시지_로그를_생성한다(); + + given(writerSession.getAttributes()).willReturn(발신자_세션_attribute_정보); + given(receiverSession.getAttributes()).willReturn(수신자_세션_attribute_정보); + willDoNothing().given(sessions).add(writerSession, 채팅방.getId()); + willReturn(true).given(sessions).containsByUserId(채팅방.getId(), 수신자.getId()); + willReturn(Set.of(writerSession, receiverSession)).given(sessions).getSessionsByChatRoomId(채팅방.getId()); + + // when + provider.handleCreateSendMessage(writerSession, 메시지_전송_데이터); + final long actual = events.stream(UpdateReadMessageLogEvent.class).count(); + + // then + assertThat(actual).isEqualTo(2L); + } + + @Test + void 메시지_생성시_수신자가_웹소켓_통신중이지_않다면_발신자의_메시지_로그_업데이트_메서드만_호출한다() throws JsonProcessingException { + // given + 메시지_로그를_생성한다(); + + given(writerSession.getAttributes()).willReturn(발신자_세션_attribute_정보); + willDoNothing().given(sessions).add(writerSession, 채팅방.getId()); + willReturn(false).given(sessions).containsByUserId(채팅방.getId(), 수신자.getId()); + willReturn(Set.of(writerSession)).given(sessions).getSessionsByChatRoomId(채팅방.getId()); + + // when + provider.handleCreateSendMessage(writerSession, 메시지_전송_데이터); + final long actual = events.stream(UpdateReadMessageLogEvent.class).count(); + + // then + assertThat(actual).isEqualTo(1L); + } + + @Test + void 메시지_생성시_수신자가_웹소켓_통신_중이지_않다면_발신자에게만_메시지를_전달한다() throws JsonProcessingException { + // given + given(writerSession.getAttributes()).willReturn(발신자_세션_attribute_정보); + willDoNothing().given(sessions).add(writerSession, 채팅방.getId()); + willReturn(false).given(sessions).containsByUserId(채팅방.getId(), 수신자.getId()); + willReturn(Set.of(writerSession)).given(sessions).getSessionsByChatRoomId(채팅방.getId()); + + // when + final List actual = provider.handleCreateSendMessage(writerSession, 메시지_전송_데이터); + + // then + assertThat(actual).hasSize(1); + } + + @Test + void 메시지_생성시_수신자가_웹소켓_통신_중이지_않다면_수신자에게_알림을_보낸다() throws Exception { + // given + given(writerSession.getAttributes()).willReturn(발신자_세션_attribute_정보); + willDoNothing().given(sessions).add(writerSession, 채팅방.getId()); + willReturn(false).given(sessions).containsByUserId(채팅방.getId(), 수신자.getId()); + willReturn(Set.of(writerSession)).given(sessions).getSessionsByChatRoomId(채팅방.getId()); + given(notificationService.send(any(CreateNotificationDto.class))).willReturn(NotificationStatus.SUCCESS); + + // when + provider.handleCreateSendMessage(writerSession, 메시지_전송_데이터); + final long actual = events.stream(MessageNotificationEvent.class).count(); + + // then + assertThat(actual).isEqualTo(1); + } + + @Test + void 메시지_생성시_수신자에게_알림을_보내기에_실패하더라도_정상적으로_메시지가_전달된다() throws Exception { + // given + given(writerSession.getAttributes()).willReturn(발신자_세션_attribute_정보); + willDoNothing().given(sessions).add(writerSession, 채팅방.getId()); + willReturn(false).given(sessions).containsByUserId(채팅방.getId(), 수신자.getId()); + willReturn(Set.of(writerSession)).given(sessions).getSessionsByChatRoomId(채팅방.getId()); + given(notificationService.send(any(CreateNotificationDto.class))).willReturn(NotificationStatus.FAIL); + + // when + final List actual = provider.handleCreateSendMessage(writerSession, 메시지_전송_데이터); + + // then + assertThat(actual).hasSize(1); + } + + @Test + void 세션을_삭제한다() { + // given + given(writerSession.getAttributes()).willReturn(발신자_세션_attribute_정보); + sessions.add(writerSession, 채팅방.getId()); + + // when + provider.remove(writerSession); + + // then + final boolean actual = sessions.containsByUserId(채팅방.getId(), 발신자.getId()); + assertThat(actual).isFalse(); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/fixture/ChatWebSocketHandleTextMessageProviderTestFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/fixture/ChatWebSocketHandleTextMessageProviderTestFixture.java new file mode 100644 index 000000000..3d9d288ac --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/fixture/ChatWebSocketHandleTextMessageProviderTestFixture.java @@ -0,0 +1,103 @@ +package com.ddang.ddang.chat.handler.fixture; + +import com.ddang.ddang.auction.domain.Auction; +import com.ddang.ddang.auction.domain.BidUnit; +import com.ddang.ddang.auction.domain.Price; +import com.ddang.ddang.auction.domain.repository.AuctionRepository; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import com.ddang.ddang.chat.application.LastReadMessageLogService; +import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; +import com.ddang.ddang.chat.domain.ChatRoom; +import com.ddang.ddang.chat.domain.repository.ChatRoomRepository; +import com.ddang.ddang.chat.domain.repository.ReadMessageLogRepository; +import com.ddang.ddang.image.domain.ProfileImage; +import com.ddang.ddang.user.domain.Reliability; +import com.ddang.ddang.user.domain.User; +import com.ddang.ddang.user.domain.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("NonAsciiCharacters") +public class ChatWebSocketHandleTextMessageProviderTestFixture { + + @Autowired + private JpaCategoryRepository categoryRepository; + + @Autowired + private AuctionRepository auctionRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ChatRoomRepository chatRoomRepository; + + @Autowired + private LastReadMessageLogService lastReadMessageLogService; + + protected ChatRoom 채팅방; + protected User 발신자; + protected User 수신자; + + protected Map 발신자_세션_attribute_정보; + protected Map 수신자_세션_attribute_정보; + protected Map 메시지_전송_데이터; + + protected CreateReadMessageLogEvent 메시지_로그_생성_이벤트; + + @BeforeEach + void setUpFixture() { + 발신자 = User.builder() + .name("발신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12345") + .build(); + 수신자 = User.builder() + .name("수신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); + userRepository.save(발신자); + userRepository.save(수신자); + + final Category 전자기기 = new Category("전자기기"); + final Category 전자기기_하위_노트북 = new Category("노트북"); + 전자기기.addSubCategory(전자기기_하위_노트북); + categoryRepository.save(전자기기); + + final Auction 경매 = Auction.builder() + .title("경매") + .seller(수신자) + .description("description") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(10_000)) + .closingTime(LocalDateTime.now().plusDays(3L)) + .build(); + auctionRepository.save(경매); + + 채팅방 = new ChatRoom(경매, 발신자); + + chatRoomRepository.save(채팅방); + + 발신자_세션_attribute_정보 = new HashMap<>(Map.of("userId", 발신자.getId(), "baseUrl", "/images")); + 수신자_세션_attribute_정보 = new HashMap<>(Map.of("userId", 수신자.getId(), "baseUrl", "/images")); + 메시지_전송_데이터 = Map.of( + "chatRoomId", String.valueOf(채팅방.getId()), + "receiverId", String.valueOf(수신자.getId()), + "contents", "메시지 내용" + ); + + 메시지_로그_생성_이벤트 = new CreateReadMessageLogEvent(채팅방); + } + + protected void 메시지_로그를_생성한다() { + lastReadMessageLogService.create(메시지_로그_생성_이벤트); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java index 5d9f61dcb..daf0aa4f1 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/presentation/ChatRoomControllerTest.java @@ -3,17 +3,16 @@ import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; import com.ddang.ddang.auction.domain.exception.WinnerNotFoundException; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptorService; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; import com.ddang.ddang.authentication.domain.dto.AuthenticationStore; import com.ddang.ddang.chat.application.dto.CreateChatRoomDto; -import com.ddang.ddang.chat.application.dto.CreateMessageDto; import com.ddang.ddang.chat.application.exception.ChatRoomNotFoundException; import com.ddang.ddang.chat.application.exception.InvalidAuctionToChatException; import com.ddang.ddang.chat.application.exception.InvalidUserToChat; import com.ddang.ddang.chat.application.exception.MessageNotFoundException; -import com.ddang.ddang.chat.application.exception.UnableToChatException; import com.ddang.ddang.chat.presentation.dto.request.ReadMessageRequest; import com.ddang.ddang.chat.presentation.dto.response.ReadMessageResponse; import com.ddang.ddang.chat.presentation.fixture.ChatRoomControllerFixture; @@ -67,10 +66,12 @@ class ChatRoomControllerTest extends ChatRoomControllerFixture { void setUp() { final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( - blackListTokenService, - authenticationUserService, - tokenDecoder, - store + new AuthenticationInterceptorService( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ) ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); @@ -84,76 +85,6 @@ void setUp() { .build(); } - @Test - void 메시지를_생성한다() throws Exception { - // given - given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); - given(messageService.create(any(CreateMessageDto.class), anyString())).willReturn(채팅방_아이디); - - // when & then - final ResultActions resultActions = mockMvc.perform(RestDocumentationRequestBuilders.post("/chattings/{chatRoomId}/messages", 채팅방_아이디) - .contentType(MediaType.APPLICATION_JSON) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .content(objectMapper.writeValueAsString(메시지_생성_요청))) - .andExpectAll( - status().isCreated(), - header().string(HttpHeaders.LOCATION, is("/chattings/1")), - jsonPath("$.id", is(1L), Long.class) - ); - createMessage_문서화(resultActions); - } - - @Test - void 채팅방이_없는_경우_메시지_생성시_404를_반환한다() throws Exception { - // given - given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); - given(messageService.create(any(CreateMessageDto.class), anyString())).willThrow(new ChatRoomNotFoundException("지정한 아이디에 대한 채팅방을 찾을 수 없습니다.")); - - // when & then - mockMvc.perform(post("/chattings/{chatRoomId}/messages", 유효하지_않은_채팅방_아이디) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .content(objectMapper.writeValueAsString(메시지_생성_요청)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpectAll( - status().isNotFound(), - jsonPath("$.message").exists() - ); - } - - @Test - void 발신자가_없는_경우_메시지_생성시_404를_반환한다() throws Exception { - // given - given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); - given(messageService.create(any(CreateMessageDto.class), anyString())).willThrow(new UserNotFoundException("지정한 아이디에 대한 발신자를 찾을 수 없습니다.")); - - // when & then - mockMvc.perform(post("/chattings/{chatRoomId}/messages", 채팅방_아이디) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .content(objectMapper.writeValueAsString(유효하지_않은_발신자의_메시지_생성_요청)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpectAll( - status().isNotFound(), - jsonPath("$.message").exists() - ); - } - - @Test - void 발신자가_탈퇴한_사용자인_경우_메시지_생성시_400를_반환한다() throws Exception { - // given - given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); - given(messageService.create(any(CreateMessageDto.class), anyString())).willThrow(new UnableToChatException("탈퇴한 사용자에게는 메시지 전송이 불가능합니다.")); - - // when & then - mockMvc.perform(post("/chattings/{chatRoomId}/messages", 채팅방_아이디) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .content(objectMapper.writeValueAsString(탈퇴한_사용자와의_메시지_생성_요청)) - .contentType(MediaType.APPLICATION_JSON)) - .andExpectAll( - status().isBadRequest(), - jsonPath("$.message").exists() - ); - } - @Test void 마지막_조회_메시지_이후_메시지를_조회한다() throws Exception { // given @@ -163,10 +94,19 @@ void setUp() { final ReadMessageResponse expected = ReadMessageResponse.of(조회용_메시지, true); // when & then - final ResultActions resultActions = mockMvc.perform(RestDocumentationRequestBuilders.get("/chattings/{chatRoomId}/messages", 채팅방_아이디) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + final ResultActions resultActions = mockMvc.perform(RestDocumentationRequestBuilders.get( + "/chattings/{chatRoomId}/messages", + 채팅방_아이디 + ) + .header( + HttpHeaders.AUTHORIZATION, + "Bearer accessToken" + ) .contentType(MediaType.APPLICATION_JSON) - .queryParam("lastMessageId", 마지막_메시지_아이디.toString()) + .queryParam( + "lastMessageId", + 마지막_메시지_아이디.toString() + ) ) .andExpectAll( status().isOk(), @@ -184,8 +124,8 @@ void setUp() { // when & then mockMvc.perform(get("/chattings/{chatRoomId}/messages", 채팅방_아이디) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON) ) .andExpectAll( status().isOk(), @@ -197,13 +137,14 @@ void setUp() { void 채팅방_아이디가_잘못된_경우_메시지를_조회하면_404를_반환한다() throws Exception { // given given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); - given(messageService.readAllByLastMessageId(any(ReadMessageRequest.class))).willThrow(new ChatRoomNotFoundException("지정한 아이디에 대한 채팅방을 찾을 수 없습니다.")); + given(messageService.readAllByLastMessageId(any(ReadMessageRequest.class))).willThrow(new ChatRoomNotFoundException( + "지정한 아이디에 대한 채팅방을 찾을 수 없습니다.")); // when & then mockMvc.perform(get("/chattings/{chatRoomId}/messages", 유효하지_않은_채팅방_아이디) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) - .queryParam("lastMessageId", 마지막_메시지_아이디.toString())) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON) + .queryParam("lastMessageId", 마지막_메시지_아이디.toString())) .andExpectAll( status().isNotFound(), jsonPath("$.message").exists() @@ -214,13 +155,14 @@ void setUp() { void 마지막_메시지_아이디가_잘못된_경우_메시지를_조회하면_404를_반환한다() throws Exception { // given given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); - given(messageService.readAllByLastMessageId(any(ReadMessageRequest.class))).willThrow(new MessageNotFoundException("조회한 마지막 메시지가 존재하지 않습니다.")); + given(messageService.readAllByLastMessageId(any(ReadMessageRequest.class))).willThrow(new MessageNotFoundException( + "조회한 마지막 메시지가 존재하지 않습니다.")); // when & then mockMvc.perform(get("/chattings/{chatRoomId}/messages", 채팅방_아이디) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) - .queryParam("lastMessageId", 유효하지_않은_마지막_메시지_아이디.toString()) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON) + .queryParam("lastMessageId", 유효하지_않은_마지막_메시지_아이디.toString()) ) .andExpectAll( status().isNotFound(), @@ -236,20 +178,49 @@ void setUp() { // when & then final ResultActions resultActions = mockMvc.perform(get("/chattings") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON)) + .header( + HttpHeaders.AUTHORIZATION, + "Bearer accessToken" + ) + .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isOk(), jsonPath("$.[0].id", is(조회용_채팅방1.id()), Long.class), - jsonPath("$.[0].chatPartner.name", is(조회용_채팅방1.partnerDto().name())), - jsonPath("$.[0].auction.title", is(조회용_채팅방1.auctionDto().title())), - jsonPath("$.[0].lastMessage.contents", is(조회용_채팅방1.lastMessageDto().contents())), - jsonPath("$.[0].unreadMessageCount", is(조회용_채팅방1.unreadMessageCount()), Long.class), + jsonPath( + "$.[0].chatPartner.name", + is(조회용_채팅방1.partnerDto().name()) + ), + jsonPath( + "$.[0].auction.title", + is(조회용_채팅방1.auctionDto().title()) + ), + jsonPath( + "$.[0].lastMessage.contents", + is(조회용_채팅방1.lastMessageDto().contents()) + ), + jsonPath( + "$.[0].unreadMessageCount", + is(조회용_채팅방1.unreadMessageCount()), + Long.class + ), jsonPath("$.[1].id", is(조회용_채팅방2.id()), Long.class), - jsonPath("$.[1].chatPartner.name", is(조회용_채팅방2.partnerDto().name())), - jsonPath("$.[1].auction.title", is(조회용_채팅방2.auctionDto().title())), - jsonPath("$.[1].lastMessage.contents", is(조회용_채팅방2.lastMessageDto().contents())), - jsonPath("$.[1].unreadMessageCount", is(조회용_채팅방1.unreadMessageCount()), Long.class) + jsonPath( + "$.[1].chatPartner.name", + is(조회용_채팅방2.partnerDto().name()) + ), + jsonPath( + "$.[1].auction.title", + is(조회용_채팅방2.auctionDto().title()) + ), + jsonPath( + "$.[1].lastMessage.contents", + is(조회용_채팅방2.lastMessageDto().contents()) + ), + jsonPath( + "$.[1].unreadMessageCount", + is(조회용_채팅방1.unreadMessageCount()), + Long.class + ) ); readAllParticipatingChatRooms_문서화(resultActions); } @@ -257,12 +228,13 @@ void setUp() { @Test void 사용자가_참여한_채팅방_목록_조회시_요청한_사용자_정보가_없다면_404를_반환한다() throws Exception { // given - given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willThrow(new UserNotFoundException("사용자 정보를 찾을 수 없습니다.")); + given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willThrow(new UserNotFoundException( + "사용자 정보를 찾을 수 없습니다.")); // when & then mockMvc.perform(get("/chattings") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON)) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isNotFound(), jsonPath("$.message").exists() @@ -276,14 +248,26 @@ void setUp() { given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willReturn(조회용_참가중인_채팅방); // when & then - final ResultActions resultActions = mockMvc.perform(RestDocumentationRequestBuilders.get("/chattings/{chatRoomId}", 채팅방_아이디) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + final ResultActions resultActions = mockMvc.perform(RestDocumentationRequestBuilders.get( + "/chattings/{chatRoomId}", + 채팅방_아이디 + ) + .header( + HttpHeaders.AUTHORIZATION, + "Bearer accessToken" + ) .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isOk(), jsonPath("$.id", is(조회용_참가중인_채팅방.id()), Long.class), - jsonPath("$.chatPartner.name", is(조회용_참가중인_채팅방.partnerDto().name())), - jsonPath("$.auction.title", is(조회용_참가중인_채팅방.auctionDto().title())) + jsonPath( + "$.chatPartner.name", + is(조회용_참가중인_채팅방.partnerDto().name()) + ), + jsonPath( + "$.auction.title", + is(조회용_참가중인_채팅방.auctionDto().title()) + ) ); readChatRoom_문서화(resultActions); } @@ -292,12 +276,13 @@ void setUp() { void 지정한_아이디에_해당하는_채팅방_조회시_요청한_사용자_정보가_없다면_404를_반환한다() throws Exception { // given given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); - given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willThrow(new UserNotFoundException("사용자 정보를 찾을 수 없습니다.")); + given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willThrow(new UserNotFoundException( + "사용자 정보를 찾을 수 없습니다.")); // when & then mockMvc.perform(get("/chattings/{chatRoomId}", 채팅방_아이디) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON)) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isNotFound(), jsonPath("$.message").exists() @@ -308,12 +293,13 @@ void setUp() { void 지정한_아이디에_해당하는_채팅방_조회시_채팅방을_찾을_수_없다면_404를_반환한다() throws Exception { // given given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); - given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willThrow(new ChatRoomNotFoundException("지정한 아이디에 대한 채팅방을 찾을 수 없습니다.")); + given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willThrow(new ChatRoomNotFoundException( + "지정한 아이디에 대한 채팅방을 찾을 수 없습니다.")); // when & then mockMvc.perform(get("/chattings/{chatRoomId}", 유효하지_않은_채팅방_아이디) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON)) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isNotFound(), jsonPath("$.message").exists() @@ -324,12 +310,13 @@ void setUp() { void 지정한_아이디에_해당하는_채팅방_조회시_요청한_사용자_채팅방의_참여자가_아니라면_404를_반환한다() throws Exception { // given given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); - given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willThrow(new InvalidUserToChat("해당 채팅방에 접근할 권한이 없습니다.")); + given(chatRoomService.readByChatRoomId(anyLong(), anyLong())).willThrow(new InvalidUserToChat( + "해당 채팅방에 접근할 권한이 없습니다.")); // when & then mockMvc.perform(get("/chattings/{chatRoomId}", 채팅방_아이디) - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON)) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON)) .andExpectAll( status().isForbidden(), jsonPath("$.message").exists() @@ -344,12 +331,18 @@ void setUp() { // when & then final ResultActions resultActions = mockMvc.perform(post("/chattings") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(채팅방_생성_요청))) + .header( + HttpHeaders.AUTHORIZATION, + "Bearer accessToken" + ) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(채팅방_생성_요청))) .andExpectAll( status().isCreated(), - header().string(HttpHeaders.LOCATION, is("/chattings/" + 채팅방_아이디)) + header().string( + HttpHeaders.LOCATION, + is("/chattings/" + 채팅방_아이디) + ) ); createChatRoom_문서화(resultActions); } @@ -358,13 +351,14 @@ void setUp() { void 채팅방_생성시_요청한_사용자_정보를_찾을_수_없다면_404를_반환한다() throws Exception { // given given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); - given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(new UserNotFoundException("사용자 정보를 찾을 수 없습니다.")); + given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(new UserNotFoundException( + "사용자 정보를 찾을 수 없습니다.")); // when & then mockMvc.perform(post("/chattings") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(채팅방_생성_요청))) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(채팅방_생성_요청))) .andExpectAll( status().isNotFound(), jsonPath("$.message").exists() @@ -375,13 +369,14 @@ void setUp() { void 채팅방_생성시_관련된_경매_정보를_찾을_수_없다면_404를_반환한다() throws Exception { // given given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); - given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(new AuctionNotFoundException("해당 경매를 찾을 수 없습니다.")); + given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(new AuctionNotFoundException( + "해당 경매를 찾을 수 없습니다.")); // when & then mockMvc.perform(post("/chattings") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(존재하지_않은_경매_아이디_채팅방_생성_요청))) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(존재하지_않은_경매_아이디_채팅방_생성_요청))) .andExpectAll( status().isNotFound(), jsonPath("$.message").exists() @@ -395,9 +390,9 @@ void setUp() { // when & then mockMvc.perform(post("/chattings") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(유효하지_않은_경매_아이디_채팅방_생성_요청))) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(유효하지_않은_경매_아이디_채팅방_생성_요청))) .andExpectAll( status().isBadRequest(), jsonPath("$.message").exists() @@ -408,13 +403,16 @@ void setUp() { void 경매가_종료되지_않은_상태에서_채팅방을_생성하면_400을_반환한다() throws Exception { // given given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); - given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(new InvalidAuctionToChatException("경매가 아직 종료되지 않았습니다.")); + given(chatRoomService.create( + anyLong(), + any(CreateChatRoomDto.class) + )).willThrow(new InvalidAuctionToChatException("경매가 아직 종료되지 않았습니다.")); // when & then mockMvc.perform(post("/chattings") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(채팅방_생성_요청))) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(채팅방_생성_요청))) .andExpectAll( status().isBadRequest(), jsonPath("$.message").exists() @@ -425,13 +423,14 @@ void setUp() { void 채팅방_생성시_낙찰자가_없다면_404를_반환한다() throws Exception { // given given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); - given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(new WinnerNotFoundException("낙찰자가 존재하지 않습니다")); + given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(new WinnerNotFoundException( + "낙찰자가 존재하지 않습니다")); // when & then mockMvc.perform(post("/chattings") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(채팅방_생성_요청))) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(채팅방_생성_요청))) .andExpectAll( status().isNotFound(), jsonPath("$.message").exists() @@ -442,40 +441,20 @@ void setUp() { void 채팅방_생성을_요청한_사용자가_경매의_판매자_또는_최종_낙찰자가_아니라면_403을_반환한다() throws Exception { // given given(tokenDecoder.decode(eq(TokenType.ACCESS), anyString())).willReturn(Optional.of(사용자_ID_클레임)); - given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(new InvalidUserToChat("경매의 판매자 또는 최종 낙찰자만 채팅이 가능합니다.")); + given(chatRoomService.create(anyLong(), any(CreateChatRoomDto.class))).willThrow(new InvalidUserToChat( + "경매의 판매자 또는 최종 낙찰자만 채팅이 가능합니다.")); // when & then mockMvc.perform(post("/chattings") - .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(채팅방_생성_요청))) + .header(HttpHeaders.AUTHORIZATION, "Bearer accessToken") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(채팅방_생성_요청))) .andExpectAll( status().isForbidden(), jsonPath("$.message").exists() ); } - private void createMessage_문서화(final ResultActions resultActions) throws Exception { - resultActions.andDo( - restDocs.document( - requestHeaders( - headerWithName("Authorization").description("회원 Bearer 인증 정보") - ), - pathParameters( - parameterWithName("chatRoomId").description("메시지를 보내고 싶은 채팅방의 ID") - ), - requestFields( - fieldWithPath("receiverId").description("메시지 수신자 ID"), - fieldWithPath("contents").description("메시지 내용") - ), - responseFields( - fieldWithPath("id").type(JsonFieldType.NUMBER) - .description("메시지 보내진 채팅방 ID") - ) - ) - ); - } - private void readAllByLastMessageId_문서화(final ResultActions resultActions) throws Exception { resultActions.andDo( restDocs.document( @@ -490,7 +469,8 @@ void setUp() { ), responseFields( fieldWithPath("[]").type(JsonFieldType.ARRAY) - .description("하나의 채팅방 내의 메시지 목록 (lastMessageId가 포함되어 있다면 lastMessageId 이후의 메시지 목록"), + .description( + "하나의 채팅방 내의 메시지 목록 (lastMessageId가 포함되어 있다면 lastMessageId 이후의 메시지 목록"), fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("메시지 ID"), fieldWithPath("[].createdAt").type(JsonFieldType.STRING) .description("메시지를 보낸 시간"), diff --git a/backend/ddang/src/test/java/com/ddang/ddang/device/presentation/DeviceTokenControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/device/presentation/DeviceTokenControllerTest.java index c1fe19399..5e870b1fb 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/device/presentation/DeviceTokenControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/device/presentation/DeviceTokenControllerTest.java @@ -2,6 +2,7 @@ import com.ddang.ddang.auction.configuration.DescendingSortPageableArgumentResolver; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptorService; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; @@ -50,10 +51,12 @@ void setUp() { final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( - blackListTokenService, - authenticationUserService, - tokenDecoder, - store + new AuthenticationInterceptorService( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ) ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/notification/application/NotificationEventListenerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/NotificationEventListenerTest.java index 661f705a6..9b50f3ea2 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/notification/application/NotificationEventListenerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/NotificationEventListenerTest.java @@ -3,22 +3,27 @@ import com.ddang.ddang.bid.application.BidService; import com.ddang.ddang.bid.application.event.BidNotificationEvent; import com.ddang.ddang.bid.infrastructure.persistence.JpaBidRepository; -import com.ddang.ddang.chat.application.MessageService; import com.ddang.ddang.chat.application.event.MessageNotificationEvent; import com.ddang.ddang.chat.domain.repository.MessageRepository; +import com.ddang.ddang.chat.handler.ChatWebSocketHandleTextMessageProvider; import com.ddang.ddang.configuration.IsolateDatabase; import com.ddang.ddang.notification.application.fixture.NotificationEventListenerFixture; import com.ddang.ddang.notification.domain.NotificationStatus; +import com.ddang.ddang.websocket.handler.dto.SendMessageDto; import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.FirebaseMessagingException; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; +import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.event.ApplicationEvents; import org.springframework.test.context.event.RecordApplicationEvents; +import org.springframework.web.socket.WebSocketSession; + +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -44,7 +49,7 @@ class NotificationEventListenerTest extends NotificationEventListenerFixture { ApplicationEvents events; @Autowired - MessageService messageService; + ChatWebSocketHandleTextMessageProvider chatWebSocketHandleTextMessageProvider; @Autowired MessageRepository messageRepository; @@ -55,6 +60,9 @@ class NotificationEventListenerTest extends NotificationEventListenerFixture { @Autowired BidService bidService; + @Mock + WebSocketSession session; + @Test void 이벤트가_호출되면_메시지_알림을_전송한다() throws FirebaseMessagingException { // given @@ -68,9 +76,12 @@ class NotificationEventListenerTest extends NotificationEventListenerFixture { } @Test - void 메시지를_전송하면_알림을_전송한다() { + void 메시지를_전송하면_알림을_전송한다() throws Exception { + // given + given(session.getAttributes()).willReturn(세션_attribute_정보); + // when - messageService.create(메시지_생성_DTO, 이미지_절대_경로); + chatWebSocketHandleTextMessageProvider.handleCreateSendMessage(session, 메시지_전송_데이터); // then final long actual = events.stream(MessageNotificationEvent.class).count(); @@ -78,14 +89,20 @@ class NotificationEventListenerTest extends NotificationEventListenerFixture { } @Test - void 메시지_알림_전송이_실패해도_메시지는_저장된다() throws FirebaseMessagingException { - // when + void 메시지_알림_전송이_실패해도_메시지는_저장된다() throws Exception { + // given + given(session.getAttributes()).willReturn(세션_attribute_정보); given(firebaseMessaging.send(any())).willThrow(FirebaseMessagingException.class); - final Long actualSavedMessageId = messageService.create(메시지_생성_DTO, 이미지_절대_경로); + + // when + final List actualSendMessageDtos = chatWebSocketHandleTextMessageProvider.handleCreateSendMessage( + session, + 메시지_전송_데이터 + ); // then SoftAssertions.assertSoftly(softAssertions -> { - softAssertions.assertThat(messageRepository.existsById(actualSavedMessageId)).isTrue(); + softAssertions.assertThat(actualSendMessageDtos).hasSize(1); final long actual = events.stream(MessageNotificationEvent.class).count(); softAssertions.assertThat(actual).isEqualTo(1); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/notification/application/fixture/NotificationEventListenerFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/fixture/NotificationEventListenerFixture.java index 3f7f9c0f2..847fbcb40 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/notification/application/fixture/NotificationEventListenerFixture.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/notification/application/fixture/NotificationEventListenerFixture.java @@ -11,7 +11,6 @@ import com.ddang.ddang.bid.domain.Bid; import com.ddang.ddang.bid.domain.BidPrice; import com.ddang.ddang.bid.domain.repository.BidRepository; -import com.ddang.ddang.chat.application.dto.CreateMessageDto; import com.ddang.ddang.chat.application.event.MessageNotificationEvent; import com.ddang.ddang.chat.domain.ChatRoom; import com.ddang.ddang.chat.domain.Message; @@ -31,7 +30,9 @@ import org.springframework.beans.factory.annotation.Autowired; import java.time.LocalDateTime; +import java.util.HashMap; import java.util.List; +import java.util.Map; @SuppressWarnings("NonAsciiCharacters") public class NotificationEventListenerFixture { @@ -54,13 +55,14 @@ public class NotificationEventListenerFixture { @Autowired private JpaMessageRepository messageRepository; - protected CreateMessageDto 메시지_생성_DTO; protected CreateBidDto 입찰_생성_DTO; protected MessageNotificationEvent 메시지_알림_이벤트; protected BidNotificationEvent 입찰_알림_이벤트; protected QuestionNotificationEvent 질문_알림_이벤트; protected AnswerNotificationEvent 답변_알림_이벤트; + protected Map 세션_attribute_정보; + protected Map 메시지_전송_데이터; protected String 이미지_절대_경로 = "/imageUrl"; @BeforeEach @@ -114,7 +116,15 @@ void setUpFixture() { chatRoomRepository.save(채팅방); bidRepository.save(bid); - 메시지_생성_DTO = new CreateMessageDto(채팅방.getId(), 발신자_겸_판매자.getId(), 수신자_겸_기존_입찰자.getId(), "메시지 내용"); + 세션_attribute_정보 = new HashMap<>(Map.of( + "userId", 발신자_겸_판매자.getId(), + "baseUrl", 이미지_절대_경로 + )); + 메시지_전송_데이터 = Map.of( + "chatRoomId", String.valueOf(채팅방.getId()), + "receiverId", String.valueOf(수신자_겸_기존_입찰자.getId()), + "contents", "메시지 내용" + ); 입찰_생성_DTO = new CreateBidDto(경매.getId(), 1000, 새로운_입찰자.getId()); final Message 저장된_메시지 = messageRepository.save( diff --git a/backend/ddang/src/test/java/com/ddang/ddang/qna/presentation/QnaControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/qna/presentation/QnaControllerTest.java index fef77aefc..6194987b8 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/qna/presentation/QnaControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/qna/presentation/QnaControllerTest.java @@ -4,6 +4,7 @@ import com.ddang.ddang.auction.application.exception.UserForbiddenException; import com.ddang.ddang.auction.configuration.DescendingSortPageableArgumentResolver; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptorService; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; @@ -71,10 +72,12 @@ void setUp() { final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( - blackListTokenService, - authenticationUserService, - tokenDecoder, - store + new AuthenticationInterceptorService( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ) ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/ReportControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/ReportControllerTest.java index 75b56549e..14f54897c 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/ReportControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/report/presentation/ReportControllerTest.java @@ -2,6 +2,7 @@ import com.ddang.ddang.auction.application.exception.AuctionNotFoundException; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptorService; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; @@ -74,10 +75,12 @@ void setUp() { final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( - blackListTokenService, - authenticationUserService, - tokenDecoder, - store + new AuthenticationInterceptorService( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ) ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/ReviewControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/ReviewControllerTest.java index c81faf74a..e80b79303 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/ReviewControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/review/presentation/ReviewControllerTest.java @@ -1,6 +1,7 @@ package com.ddang.ddang.review.presentation; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptorService; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; @@ -56,10 +57,12 @@ void setUp() { final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( - blackListTokenService, - authenticationUserService, - tokenDecoder, - store + new AuthenticationInterceptorService( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ) ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserAuctionControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserAuctionControllerTest.java index c6978a350..4af5133de 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserAuctionControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserAuctionControllerTest.java @@ -2,6 +2,7 @@ import com.ddang.ddang.auction.configuration.DescendingSortPageableArgumentResolver; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptorService; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; @@ -52,10 +53,12 @@ void setUp() { final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( - blackListTokenService, - authenticationUserService, - tokenDecoder, - store + new AuthenticationInterceptorService( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ) ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserControllerTest.java index 12f0cdff7..efef3d994 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserControllerTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/user/presentation/UserControllerTest.java @@ -1,6 +1,7 @@ package com.ddang.ddang.user.presentation; import com.ddang.ddang.authentication.configuration.AuthenticationInterceptor; +import com.ddang.ddang.authentication.configuration.AuthenticationInterceptorService; import com.ddang.ddang.authentication.configuration.AuthenticationPrincipalArgumentResolver; import com.ddang.ddang.authentication.domain.TokenDecoder; import com.ddang.ddang.authentication.domain.TokenType; @@ -54,10 +55,12 @@ void setUp() { final AuthenticationStore store = new AuthenticationStore(); final AuthenticationInterceptor interceptor = new AuthenticationInterceptor( - blackListTokenService, - authenticationUserService, - tokenDecoder, - store + new AuthenticationInterceptorService( + blackListTokenService, + authenticationUserService, + tokenDecoder, + store + ) ); final AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(store); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/WebSocketHandleTextMessageProviderCompositeTest.java b/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/WebSocketHandleTextMessageProviderCompositeTest.java new file mode 100644 index 000000000..bde1854a7 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/WebSocketHandleTextMessageProviderCompositeTest.java @@ -0,0 +1,50 @@ +package com.ddang.ddang.websocket.handler; + +import com.ddang.ddang.chat.handler.ChatWebSocketHandleTextMessageProvider; +import com.ddang.ddang.websocket.handler.dto.TextMessageType; +import com.ddang.ddang.websocket.handler.exception.UnsupportedTextMessageTypeException; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class WebSocketHandleTextMessageProviderCompositeTest { + + @Test + void 지원하는_웹소켓_메시지_타입을_전달하면_해당_웹소켓_메시지_핸들러_provider를_반환한다() { + // given + final ChatWebSocketHandleTextMessageProvider provider = new ChatWebSocketHandleTextMessageProvider( + null, + null, + null, + null, + null + ); + final WebSocketHandleTextMessageProviderComposite composite = + new WebSocketHandleTextMessageProviderComposite(Set.of(provider)); + + // when + final WebSocketHandleTextMessageProvider actual = composite.findProvider(TextMessageType.CHATTINGS); + + // then + assertThat(actual).isInstanceOf(ChatWebSocketHandleTextMessageProvider.class); + } + + @Test + void 지원하지_않는_웹소켓_메시지_타입을_전달하면_예외가_발생한다() { + // given + final WebSocketHandleTextMessageProviderComposite composite = + new WebSocketHandleTextMessageProviderComposite(Set.of()); + + // when & then + assertThatThrownBy(() -> composite.findProvider(TextMessageType.CHATTINGS)) + .isInstanceOf(UnsupportedTextMessageTypeException.class) + .hasMessage("지원하는 웹 소켓 통신 타입이 아닙니다."); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/WebSocketHandlerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/WebSocketHandlerTest.java new file mode 100644 index 000000000..40b7cca3e --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/WebSocketHandlerTest.java @@ -0,0 +1,72 @@ +package com.ddang.ddang.websocket.handler; + +import com.ddang.ddang.chat.handler.ChatWebSocketHandleTextMessageProvider; +import com.ddang.ddang.configuration.IsolateDatabase; +import com.ddang.ddang.websocket.handler.dto.SendMessageDto; +import com.ddang.ddang.websocket.handler.dto.TextMessageType; +import com.ddang.ddang.websocket.handler.fixture.WebSocketHandlerTestFixture; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@IsolateDatabase +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class WebSocketHandlerTest extends WebSocketHandlerTestFixture { + + @MockBean + ChatWebSocketHandleTextMessageProvider provider; + + @MockBean + WebSocketHandleTextMessageProviderComposite providerComposite; + + @Autowired + WebSocketHandler webSocketHandler; + + @Mock + WebSocketSession session; + + @Test + void 세션의_타입_속성이_채팅이라면_채팅_provider를_통해_메시지를_생성한다() throws Exception { + // given + given(providerComposite.findProvider(TextMessageType.CHATTINGS)).willReturn(provider); + given(session.getAttributes()).willReturn(세션_attribute_정보); + given(provider.handleCreateSendMessage(any(WebSocketSession.class), anyMap())) + .willReturn(List.of(new SendMessageDto(session, 전송할_메시지))); + + // when + final TextMessage message = new TextMessage(new ObjectMapper().writeValueAsString(세션_attribute_정보)); + webSocketHandler.handleTextMessage(session, message); + + // then + verify(provider, times(1)).handleCreateSendMessage(any(WebSocketSession.class), anyMap()); + verify(session, times(1)).sendMessage(any()); + } + + @Test + void 웹소켓_통신이_종료되면_해당_세션의_타입에_따라_해당하는_provider를_통해_세션을_제거한다() { + // given + given(providerComposite.findProvider(TextMessageType.CHATTINGS)).willReturn(provider); + given(session.getAttributes()).willReturn(세션_attribute_정보); + + // when + webSocketHandler.afterConnectionClosed(session, null); + + // then + verify(provider, times(1)).remove(any(WebSocketSession.class)); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/fixture/WebSocketHandlerTestFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/fixture/WebSocketHandlerTestFixture.java new file mode 100644 index 000000000..59c7d7b3d --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/fixture/WebSocketHandlerTestFixture.java @@ -0,0 +1,20 @@ +package com.ddang.ddang.websocket.handler.fixture; + +import com.ddang.ddang.websocket.handler.dto.TextMessageType; +import org.springframework.web.socket.TextMessage; + +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("NonAsciiCharacters") +public class WebSocketHandlerTestFixture { + + protected Long 사용자_아이디 = 1L; + protected Map 세션_attribute_정보 = new HashMap<>( + Map.of( + "type", TextMessageType.CHATTINGS.name(), + "data", Map.of("userId", 사용자_아이디, "baseUrl", "/images") + ) + ); + protected TextMessage 전송할_메시지 = new TextMessage("메시지"); +}