diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListener.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListener.java index 5c67a1089..e87f61c4a 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListener.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/application/LastReadMessageLogEventListener.java @@ -2,7 +2,6 @@ import com.ddang.ddang.chat.application.event.CreateReadMessageLogEvent; import com.ddang.ddang.chat.application.event.UpdateReadMessageLogEvent; -import com.ddang.ddang.chat.application.exception.ReadMessageLogNotFoundException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -31,8 +30,12 @@ public void create(final CreateReadMessageLogEvent createReadMessageLogEvent) { @Transactional(propagation = Propagation.REQUIRES_NEW) public void update(final UpdateReadMessageLogEvent updateReadMessageLogEvent) { try { + log.info( + "LastReadMessageLogEventListener.update updateReadMessageLogEvent : {}", + updateReadMessageLogEvent.toString() + ); lastReadMessageLogService.update(updateReadMessageLogEvent); - } catch (final ReadMessageLogNotFoundException ex) { + } catch (final Exception ex) { log.error("exception type : {}, ", ex.getClass().getSimpleName(), ex); } } 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 index 196903d31..05d6bc2bf 100644 --- 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 @@ -5,7 +5,6 @@ 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; @@ -22,10 +21,8 @@ public void add(final WebSocketSession session, final Long chatRoomId) { webSocketSessions.putIfAbsent(session, chatRoomId); } - public Set getSessionsByChatRoomId(final Long chatRoomId) { - final WebSocketSessions webSocketSessions = chatRoomSessions.get(chatRoomId); - - return webSocketSessions.getSessions(); + public WebSocketSessions findSessionsByChatRoomId(final Long chatRoomId) { + return chatRoomSessions.get(chatRoomId); } public boolean containsByUserId(final Long chatRoomId, final Long userId) { 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 index 7619377a5..0d5c1c53c 100644 --- 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 @@ -1,12 +1,17 @@ package com.ddang.ddang.chat.domain; import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.socket.WebSocketSession; import java.util.Collections; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +@ToString +@Slf4j @Getter public class WebSocketSessions { @@ -23,10 +28,17 @@ public void putIfAbsent(final WebSocketSession session, final Long chatRoomId) { } public boolean contains(final Long userId) { + log.info("WebSocketSessions.contains userId : {}, currentSessions : {}", userId, sessions); return sessions.stream() .anyMatch(session -> session.getAttributes().get(USER_ID_KEY) == userId); } + public Optional findByUserId(final long userId) { + return sessions.stream() + .filter(session -> session.getAttributes().get(USER_ID_KEY).equals(userId)) + .findFirst(); + } + public void remove(final WebSocketSession session) { sessions.remove(session); } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/ChatHandleProvider.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/ChatHandleProvider.java new file mode 100644 index 000000000..3cff1686a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/ChatHandleProvider.java @@ -0,0 +1,19 @@ +package com.ddang.ddang.chat.handler; + +import com.ddang.ddang.websocket.handler.dto.ChatMessageType; +import com.ddang.ddang.websocket.handler.dto.SendMessageDto; +import com.ddang.ddang.websocket.handler.dto.SessionAttributeDto; +import com.fasterxml.jackson.core.JsonProcessingException; + +import java.util.List; +import java.util.Map; + +public interface ChatHandleProvider { + + List createResponse( + final SessionAttributeDto sessionAttributeDto, + final Map data + ) throws JsonProcessingException; + + ChatMessageType supportsChatType(); +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/ChatHandleTypeProviderComposite.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/ChatHandleTypeProviderComposite.java new file mode 100644 index 000000000..443a9c4d0 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/ChatHandleTypeProviderComposite.java @@ -0,0 +1,23 @@ +package com.ddang.ddang.chat.handler; + +import com.ddang.ddang.websocket.handler.dto.ChatMessageType; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +public class ChatHandleTypeProviderComposite { + + private final Map mappings; + + public ChatHandleTypeProviderComposite(final Set providers) { + this.mappings = providers.stream() + .collect(Collectors.toMap(ChatHandleProvider::supportsChatType, provider -> provider)); + } + + public ChatHandleProvider findProvider(final ChatMessageType chatMessageType) { + return mappings.get(chatMessageType); + } +} 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 index d9f69098d..922896220 100644 --- 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 @@ -1,40 +1,30 @@ 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.ChatMessageType; 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 static final String CHATROOM_ID_KEY = "chatRoomId"; + private static final String TYPE_KEY = "type"; + private final ObjectMapper objectMapper; - private final MessageService messageService; - private final ApplicationEventPublisher messageNotificationEventPublisher; - private final ApplicationEventPublisher messageLogEventPublisher; + private final WebSocketChatSessions sessions; + private final ChatHandleTypeProviderComposite chatHandleTypeProviderComposite; @Override public TextMessageType supportTextMessageType() { @@ -44,94 +34,25 @@ public TextMessageType supportTextMessageType() { @Override public List handleCreateSendMessage( final WebSocketSession session, - final Map data + final Map chatMessageData ) throws JsonProcessingException { - final SessionAttributeDto sessionAttribute = getSessionAttributes(session); - final ChatMessageDataDto messageData = objectMapper.convertValue(data, ChatMessageDataDto.class); - sessions.add(session, messageData.chatRoomId()); + final long chatRoomId = getChatRoomId(chatMessageData); + sessions.add(session, chatRoomId); - final Long writerId = sessionAttribute.userId(); - final CreateMessageDto createMessageDto = createMessageDto(messageData, writerId); - final Message message = messageService.create(createMessageDto); - sendNotificationIfReceiverNotInSession(message, sessionAttribute); + final ChatMessageType type = ChatMessageType.findMessageType(chatMessageData.get(TYPE_KEY)); + final ChatHandleProvider provider = chatHandleTypeProviderComposite.findProvider(type); - return createSendMessages(message, writerId, createMessageDto.chatRoomId()); + return provider.createResponse(convertToSessionAttributeDto(session), chatMessageData); } - private SessionAttributeDto getSessionAttributes(final WebSocketSession session) { + private SessionAttributeDto convertToSessionAttributeDto(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); + private long getChatRoomId(final Map data) { + return Long.parseLong(data.get(CHATROOM_ID_KEY)); } @Override diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/MessageTypeHandler.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/MessageTypeHandler.java new file mode 100644 index 000000000..5d21ac71a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/MessageTypeHandler.java @@ -0,0 +1,148 @@ +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.domain.WebSocketSessions; +import com.ddang.ddang.chat.handler.dto.ChatMessageDataDto; +import com.ddang.ddang.chat.handler.dto.MessageDataDto; +import com.ddang.ddang.chat.handler.dto.MessageDto; +import com.ddang.ddang.chat.handler.dto.SendChatResponse; +import com.ddang.ddang.chat.handler.dto.SendMessageStatus; +import com.ddang.ddang.chat.presentation.dto.request.CreateMessageRequest; +import com.ddang.ddang.websocket.handler.dto.ChatMessageType; +import com.ddang.ddang.websocket.handler.dto.SendMessageDto; +import com.ddang.ddang.websocket.handler.dto.SessionAttributeDto; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MessageTypeHandler implements ChatHandleProvider { + + private final WebSocketChatSessions sessions; + private final ObjectMapper objectMapper; + private final MessageService messageService; + private final ApplicationEventPublisher messageLogEventPublisher; + private final ApplicationEventPublisher messageNotificationEventPublisher; + + @Override + public ChatMessageType supportsChatType() { + return ChatMessageType.MESSAGE; + } + + @Override + public List createResponse( + final SessionAttributeDto sessionAttributeDto, + final Map data + ) throws JsonProcessingException { + final Long writerId = sessionAttributeDto.userId(); + final MessageDataDto messageDataDto = MessageDataDto.from(data); + + final Message message = createMessage(data, writerId); + + sendNotificationIfReceiverNotInSession(message, sessionAttributeDto); + + return createSendMessages(message, writerId, messageDataDto.chatRoomId()); + } + + private Message createMessage(final Map data, final long writerId) { + final ChatMessageDataDto messageData = objectMapper.convertValue(data, ChatMessageDataDto.class); + final CreateMessageDto createMessageDto = createMessageDto(messageData, writerId); + + return messageService.create(createMessageDto); + } + + private void sendNotificationIfReceiverNotInSession( + final Message message, + final SessionAttributeDto sessionAttribute + ) { + log.info("MessageTypeHandler.sendNotificationIfRecevierNotInSession receiverId : {}, chatRoomId : {}", message.getReceiver().getId(), message.getChatRoom().getId()); + log.info( + "MessageTypeHandler.sendNotificationIfReceiverNotInSession sessions : {}", + sessions.getChatRoomSessions() + ); + if (!sessions.containsByUserId(message.getChatRoom().getId(), message.getReceiver().getId())) { + log.info("MessageTypeHandler.sendNotificationIfReceiverNotInSession if문 내부"); + 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 WebSocketSessions groupSessions = sessions.findSessionsByChatRoomId(message.getChatRoom().getId()); + log.info("MessageTypeHandler.createSendMessages groupSessions : {}", groupSessions.toString()); + + final List sendMessageDtos = new ArrayList<>(); + for (final WebSocketSession currentSession : groupSessions.getSessions()) { + final MessageDto messageDto = MessageDto.of(message, isMyMessage(currentSession, writerId)); + final TextMessage textMessage = createTextMessage(messageDto); + sendMessageDtos.add(new SendMessageDto(currentSession, textMessage)); + updateReadMessageLog(currentSession, chatRoomId, message); + } + + return sendMessageDtos; + } + + private void updateReadMessageLog( + final WebSocketSession currentSession, + final Long chatRoomId, + final Message message + ) { + final SessionAttributeDto sessionAttributes = convertToSessionAttributeDto(currentSession); + final UpdateReadMessageLogEvent updateReadMessageLogEvent = new UpdateReadMessageLogEvent( + sessionAttributes.userId(), + chatRoomId, + message.getId() + ); + messageLogEventPublisher.publishEvent(updateReadMessageLogEvent); + } + + private SessionAttributeDto convertToSessionAttributeDto(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 boolean isMyMessage( + final WebSocketSession session, + final Long writerId + ) { + final long userId = Long.parseLong(String.valueOf(session.getAttributes().get("userId"))); + + return writerId.equals(userId); + } + + private TextMessage createTextMessage(final MessageDto messageDto) throws JsonProcessingException { + final SendChatResponse sendChatResponse = new SendChatResponse(SendMessageStatus.SUCCESS, List.of(messageDto)); + + return new TextMessage(objectMapper.writeValueAsString(sendChatResponse)); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/PingTypeHandler.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/PingTypeHandler.java new file mode 100644 index 000000000..fd91e1d4e --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/PingTypeHandler.java @@ -0,0 +1,101 @@ +package com.ddang.ddang.chat.handler; + +import com.ddang.ddang.chat.application.MessageService; +import com.ddang.ddang.chat.application.dto.ReadMessageDto; +import com.ddang.ddang.chat.domain.WebSocketChatSessions; +import com.ddang.ddang.chat.domain.WebSocketSessions; +import com.ddang.ddang.chat.handler.dto.ChatPingDto; +import com.ddang.ddang.chat.handler.dto.MessageDto; +import com.ddang.ddang.chat.handler.dto.PingDataDto; +import com.ddang.ddang.chat.handler.dto.SendChatResponse; +import com.ddang.ddang.chat.handler.dto.SendMessageStatus; +import com.ddang.ddang.chat.presentation.dto.request.ReadMessageRequest; +import com.ddang.ddang.websocket.handler.dto.ChatMessageType; +import com.ddang.ddang.websocket.handler.dto.SendMessageDto; +import com.ddang.ddang.websocket.handler.dto.SessionAttributeDto; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; + +@Component +@RequiredArgsConstructor +public class PingTypeHandler implements ChatHandleProvider { + + private final WebSocketChatSessions sessions; + private final ObjectMapper objectMapper; + private final MessageService messageService; + + @Override + public ChatMessageType supportsChatType() { + return ChatMessageType.PING; + } + + @Override + public List createResponse( + final SessionAttributeDto sessionAttributeDto, + final Map chatPingData + ) throws JsonProcessingException { + final PingDataDto pingDataDto = PingDataDto.from(chatPingData); + final ReadMessageRequest readMessageRequest = createReadMessageRequest(sessionAttributeDto, pingDataDto); + final List readMessageDtos = messageService.readAllByLastMessageId(readMessageRequest); + final SendChatResponse sendChatResponse = createSendChatResponse(readMessageDtos, sessionAttributeDto); + + return List.of(createSendMessageDto(sessionAttributeDto, pingDataDto, sendChatResponse)); + } + + private ReadMessageRequest createReadMessageRequest( + final SessionAttributeDto sessionAttributeDto, + final PingDataDto chatPingData + ) { + final ChatPingDto pingDto = objectMapper.convertValue(chatPingData, ChatPingDto.class); + + return new ReadMessageRequest(sessionAttributeDto.userId(), pingDto.chatRoomId(), pingDto.lastMessageId()); + } + + private SendChatResponse createSendChatResponse( + final List readMessageDtos, + final SessionAttributeDto session + ) { + final List messageDtos = convertToMessageDto(readMessageDtos, session); + + return new SendChatResponse(SendMessageStatus.SUCCESS, messageDtos); + } + + private List convertToMessageDto( + final List readMessageDtos, + final SessionAttributeDto sessionAttributeDto + ) { + return readMessageDtos.stream() + .map(readMessageDto -> MessageDto.of(readMessageDto, isMyMessage(sessionAttributeDto, + readMessageDto.writerId() + ))).toList(); + } + + private boolean isMyMessage(final SessionAttributeDto session, final Long writerId) { + final long userId = session.userId(); + + return writerId.equals(userId); + } + + private SendMessageDto createSendMessageDto( + final SessionAttributeDto sessionAttributeDto, + final PingDataDto chatPingData, + final SendChatResponse sendChatResponse + ) throws JsonProcessingException { + final TextMessage textMessage = new TextMessage(objectMapper.writeValueAsString(sendChatResponse)); + final WebSocketSessions sessions = this.sessions.findSessionsByChatRoomId(chatPingData.chatRoomId()); + final WebSocketSession userSession = sessions.findByUserId(sessionAttributeDto.userId()) + .orElseThrow(() -> new NoSuchElementException( + "웹소켓에 연결된 사용자가 존재하지 않습니다." + )); + + return new SendMessageDto(userSession, textMessage); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/ChatPingDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/ChatPingDto.java new file mode 100644 index 000000000..8778550b3 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/ChatPingDto.java @@ -0,0 +1,4 @@ +package com.ddang.ddang.chat.handler.dto; + +public record ChatPingDto(Long chatRoomId, Long lastMessageId) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/MessageDataDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/MessageDataDto.java new file mode 100644 index 000000000..c4846904a --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/MessageDataDto.java @@ -0,0 +1,18 @@ +package com.ddang.ddang.chat.handler.dto; + +import java.util.Map; + +public record MessageDataDto(long chatRoomId, long receiverId, String contents) { + + private static final String CHAT_ROOM_ID_KEY = "chatRoomId"; + private static final String RECEIVER_ID_KEY = "receiverId"; + private static final String CONTENTS_ID_KEY = "contents"; + + public static MessageDataDto from(final Map data) { + return new MessageDataDto( + Long.parseLong(data.get(CHAT_ROOM_ID_KEY)), + Long.parseLong(data.get(RECEIVER_ID_KEY)), + data.get(CONTENTS_ID_KEY) + ); + } +} 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 index 30e7e5512..4cb12590c 100644 --- 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 @@ -1,5 +1,6 @@ package com.ddang.ddang.chat.handler.dto; +import com.ddang.ddang.chat.application.dto.ReadMessageDto; import com.ddang.ddang.chat.domain.Message; import com.fasterxml.jackson.annotation.JsonFormat; @@ -24,4 +25,13 @@ public static MessageDto of(final Message message, final boolean isMyMessage) { message.getContents() ); } + + public static MessageDto of(final ReadMessageDto readMessageDto, final boolean isMyMessage) { + return new MessageDto( + readMessageDto.id(), + readMessageDto.createdTime(), + isMyMessage, + readMessageDto.contents() + ); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/PingDataDto.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/PingDataDto.java new file mode 100644 index 000000000..315cd199e --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/PingDataDto.java @@ -0,0 +1,15 @@ +package com.ddang.ddang.chat.handler.dto; + +import java.util.Map; + +public record PingDataDto(long chatRoomId, long lastMessageId) { + + private static final String CHAT_ROOM_ID_KEY = "chatRoomId"; + private static final String LAST_MESSAGE_ID = "lastMessageId"; + + public static PingDataDto from(final Map chatPingData) { + return new PingDataDto(Long.parseLong(chatPingData.get(CHAT_ROOM_ID_KEY)), + Long.parseLong(chatPingData.get(LAST_MESSAGE_ID)) + ); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/SendChatResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/SendChatResponse.java new file mode 100644 index 000000000..a963247f9 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/SendChatResponse.java @@ -0,0 +1,6 @@ +package com.ddang.ddang.chat.handler.dto; + +import java.util.List; + +public record SendChatResponse(SendMessageStatus status, List messages) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/SendMessageStatus.java b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/SendMessageStatus.java new file mode 100644 index 000000000..d030f4801 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/chat/handler/dto/SendMessageStatus.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.chat.handler.dto; + +public enum SendMessageStatus { + + SUCCESS, + DISCONNECTED, + FORBIDDEN +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java b/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java index 5831faf10..835daeb4b 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java @@ -42,6 +42,8 @@ import com.ddang.ddang.review.application.exception.ReviewNotFoundException; import com.ddang.ddang.user.application.exception.AlreadyExistsNameException; import com.ddang.ddang.user.application.exception.UserNotFoundException; +import com.ddang.ddang.websocket.handler.exception.UnsupportedChattingTypeException; +import com.ddang.ddang.websocket.handler.exception.UnsupportedTextMessageTypeException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; @@ -436,6 +438,26 @@ public ResponseEntity handleWithdrawalNotAllowedException(fin .body(new ExceptionResponse(ex.getMessage())); } + @ExceptionHandler(UnsupportedTextMessageTypeException.class) + public ResponseEntity handleUnsupportedTextMessageTypeException( + final UnsupportedTextMessageTypeException ex + ) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ExceptionResponse(ex.getMessage())); + } + + @ExceptionHandler(UnsupportedChattingTypeException.class) + public ResponseEntity handleUnsupportedChattingTypeException( + final UnsupportedChattingTypeException ex + ) { + logger.warn(String.format(LOG_MESSAGE_FORMAT, ex.getClass().getSimpleName(), ex.getMessage())); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ExceptionResponse(ex.getMessage())); + } + @Override protected ResponseEntity handleMethodArgumentNotValid( final MethodArgumentNotValidException ex, diff --git a/backend/ddang/src/main/java/com/ddang/ddang/notification/application/NotificationEventListener.java b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/NotificationEventListener.java index 2839f60cd..c6227c573 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/notification/application/NotificationEventListener.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/notification/application/NotificationEventListener.java @@ -37,6 +37,7 @@ public class NotificationEventListener { @TransactionalEventListener public void sendMessageNotification(final MessageNotificationEvent messageNotificationEvent) { try { + log.info("NotificationEventListener.sendMessageNotification : {}", messageNotificationEvent.toString()); final Message message = messageNotificationEvent.message(); final ProfileImage profileImage = message.getWriter().getProfileImage(); final CreateNotificationDto createNotificationDto = new CreateNotificationDto( @@ -48,7 +49,7 @@ public void sendMessageNotification(final MessageNotificationEvent messageNotifi ImageUrlCalculator.calculateBy(messageNotificationEvent.profileImageAbsoluteUrl(), ImageStoreNameProcessor.process(profileImage)) ); notificationService.send(createNotificationDto); - } catch (final FirebaseMessagingException ex) { + } catch (final Exception ex) { log.error("exception type : {}, ", ex.getClass().getSimpleName(), ex); } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/ChatMessageType.java b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/ChatMessageType.java new file mode 100644 index 000000000..15161a2a6 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/dto/ChatMessageType.java @@ -0,0 +1,18 @@ +package com.ddang.ddang.websocket.handler.dto; + +import com.ddang.ddang.websocket.handler.exception.UnsupportedChattingTypeException; + +import java.util.Arrays; + +public enum ChatMessageType { + + MESSAGE, + PING; + + public static ChatMessageType findMessageType(final String value) { + return Arrays.stream(ChatMessageType.values()) + .filter(chattingType -> chattingType.name().equalsIgnoreCase(value)) + .findFirst() + .orElseThrow(() -> new UnsupportedChattingTypeException("잘못된 채팅 타입입니다.")); + } +} 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 index 70fc40615..69405820e 100644 --- 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 @@ -1,7 +1,20 @@ package com.ddang.ddang.websocket.handler.dto; +import com.ddang.ddang.websocket.handler.exception.UnsupportedTextMessageTypeException; +import com.fasterxml.jackson.annotation.JsonCreator; + +import java.util.Arrays; + public enum TextMessageType { CHATTINGS, - BIDS + BIDS; + + @JsonCreator + public static TextMessageType fromString(String value) { + return Arrays.stream(TextMessageType.values()) + .filter(messageType -> messageType.name().equalsIgnoreCase(value)) + .findFirst() + .orElseThrow(() -> new UnsupportedTextMessageTypeException("잘못된 메시지 타입입니다.")); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/exception/UnsupportedChattingTypeException.java b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/exception/UnsupportedChattingTypeException.java new file mode 100644 index 000000000..d7b842b8e --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/exception/UnsupportedChattingTypeException.java @@ -0,0 +1,7 @@ +package com.ddang.ddang.websocket.handler.exception; + +public class UnsupportedChattingTypeException extends IllegalStateException { + public UnsupportedChattingTypeException(final String message) { + super(message); + } +} 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 index 15fe5846a..b3dc381c5 100644 --- 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 @@ -8,6 +8,8 @@ import org.junit.jupiter.api.Test; import org.springframework.web.socket.WebSocketSession; +import java.util.Optional; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; @@ -84,6 +86,19 @@ void setUp() { assertThat(actual).isFalse(); } + @Test + void 유저아이디에_해당하는_웹소켓세션을_반환한다() { + // given + given(session.getAttributes()).willReturn(세션_attribute_정보); + sessions.putIfAbsent(session, 채팅방_아이디); + + // when + final Optional actual = sessions.findByUserId(사용자_아이디); + + // then + assertThat(actual.get()).isEqualTo(session); + } + @Test void 세션을_제거한다() { // given 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 index dd7c99cd9..c9e10150d 100644 --- 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 @@ -25,9 +25,9 @@ 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.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willDoNothing; @@ -79,7 +79,9 @@ class ChatWebSocketHandleTextMessageProviderTest extends ChatWebSocketHandleText 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()); + 채팅방에_해당하는_세션.putIfAbsent(writerSession, 채팅방.getId()); + 채팅방에_해당하는_세션.putIfAbsent(receiverSession, 채팅방.getId()); + willReturn(채팅방에_해당하는_세션).given(sessions).findSessionsByChatRoomId(채팅방.getId()); // when final List actual = provider.handleCreateSendMessage(writerSession, 메시지_전송_데이터); @@ -97,7 +99,9 @@ class ChatWebSocketHandleTextMessageProviderTest extends ChatWebSocketHandleText 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()); + 채팅방에_해당하는_세션.putIfAbsent(writerSession, 채팅방.getId()); + 채팅방에_해당하는_세션.putIfAbsent(receiverSession, 채팅방.getId()); + willReturn(채팅방에_해당하는_세션).given(sessions).findSessionsByChatRoomId(채팅방.getId()); // when provider.handleCreateSendMessage(writerSession, 메시지_전송_데이터); @@ -115,7 +119,8 @@ class ChatWebSocketHandleTextMessageProviderTest extends ChatWebSocketHandleText 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()); + 채팅방에_해당하는_세션.putIfAbsent(writerSession, 채팅방.getId()); + willReturn(채팅방에_해당하는_세션).given(sessions).findSessionsByChatRoomId(채팅방.getId()); // when provider.handleCreateSendMessage(writerSession, 메시지_전송_데이터); @@ -131,7 +136,8 @@ class ChatWebSocketHandleTextMessageProviderTest extends ChatWebSocketHandleText 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()); + 채팅방에_해당하는_세션.putIfAbsent(writerSession, 채팅방.getId()); + willReturn(채팅방에_해당하는_세션).given(sessions).findSessionsByChatRoomId(채팅방.getId()); // when final List actual = provider.handleCreateSendMessage(writerSession, 메시지_전송_데이터); @@ -146,7 +152,8 @@ class ChatWebSocketHandleTextMessageProviderTest extends ChatWebSocketHandleText 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()); + 채팅방에_해당하는_세션.putIfAbsent(writerSession, 채팅방.getId()); + willReturn(채팅방에_해당하는_세션).given(sessions).findSessionsByChatRoomId(채팅방.getId()); given(notificationService.send(any(CreateNotificationDto.class))).willReturn(NotificationStatus.SUCCESS); // when @@ -163,7 +170,8 @@ class ChatWebSocketHandleTextMessageProviderTest extends ChatWebSocketHandleText 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()); + 채팅방에_해당하는_세션.putIfAbsent(writerSession, 채팅방.getId()); + willReturn(채팅방에_해당하는_세션).given(sessions).findSessionsByChatRoomId(채팅방.getId()); given(notificationService.send(any(CreateNotificationDto.class))).willReturn(NotificationStatus.FAIL); // when @@ -173,6 +181,20 @@ class ChatWebSocketHandleTextMessageProviderTest extends ChatWebSocketHandleText assertThat(actual).hasSize(1); } + @Test + void 잘못된_데이터_타입_전달시_예외가_발생한다() { + // given + given(writerSession.getAttributes()).willReturn(발신자_세션_attribute_정보); + willDoNothing().given(sessions).add(writerSession, 채팅방.getId()); + willReturn(false).given(sessions).containsByUserId(채팅방.getId(), 수신자.getId()); + 채팅방에_해당하는_세션.putIfAbsent(writerSession, 채팅방.getId()); + willReturn(채팅방에_해당하는_세션).given(sessions).findSessionsByChatRoomId(채팅방.getId()); + + // when + assertThatThrownBy(() -> provider.handleCreateSendMessage(writerSession, 잘못된_메시지_전송_데이터)).isInstanceOf( + IllegalArgumentException.class); + } + @Test void 세션을_삭제한다() { // given diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/MessageTypeHandlerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/MessageTypeHandlerTest.java new file mode 100644 index 000000000..3619d1bff --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/MessageTypeHandlerTest.java @@ -0,0 +1,74 @@ +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.handler.fixture.MessageTypeHandlerTestFixture; +import com.ddang.ddang.configuration.IsolateDatabase; +import com.fasterxml.jackson.core.JsonProcessingException; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.willReturn; + +@IsolateDatabase +@RecordApplicationEvents +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class MessageTypeHandlerTest extends MessageTypeHandlerTestFixture { + + @Autowired + MessageTypeHandler messageTypeHandler; + + @MockBean + WebSocketChatSessions webSocketChatSessions; + + @Mock + WebSocketSession writerSession; + + @Mock + WebSocketSession receiverSession; + + @Autowired + ApplicationEvents events; + + @Test + void 웹소켓으로_메시지_전송시_사용자가_웹소켓에_접속하지_않은_경우_알림을_전송한다() throws JsonProcessingException { + // given + willReturn(false).given(webSocketChatSessions).containsByUserId(채팅방.getId(), 수신자.getId()); + willReturn(발신자만_존재하는_웹소켓_세션들).given(webSocketChatSessions).findSessionsByChatRoomId(채팅방.getId()); + + // when + messageTypeHandler.createResponse(발신자_세션_속성_dto, 메시지_데이터); + final long actual = events.stream(MessageNotificationEvent.class).count(); + + // then + assertThat(actual).isEqualTo(1); + } + + @Test + void 웹소켓으로_메시지_전송시_메시지_수신자_모두의_메시지_로그_업데이트_이벤트를_호출한다() throws JsonProcessingException { + // given + willReturn(true).given(webSocketChatSessions).containsByUserId(채팅방.getId(), 발신자.getId()); + 발신자와_수신자가_존재하는_웹소켓_세션들.putIfAbsent(writerSession, 채팅방.getId()); + 발신자와_수신자가_존재하는_웹소켓_세션들.putIfAbsent(receiverSession, 채팅방.getId()); + willReturn(발신자와_수신자가_존재하는_웹소켓_세션들).given(webSocketChatSessions).findSessionsByChatRoomId(채팅방.getId()); + willReturn(발신자_세션_attribute_정보).given(writerSession).getAttributes(); + willReturn(수신자_세션_attribute_정보).given(receiverSession).getAttributes(); + + // when + messageTypeHandler.createResponse(발신자_세션_속성_dto, 메시지_데이터); + final long actual = events.stream(UpdateReadMessageLogEvent.class).count(); + + // then + assertThat(actual).isEqualTo(2); + } +} 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 index 3d9d288ac..fc24c782c 100644 --- 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 @@ -9,8 +9,8 @@ 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.WebSocketSessions; 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; @@ -47,23 +47,25 @@ public class ChatWebSocketHandleTextMessageProviderTestFixture { protected Map 발신자_세션_attribute_정보; protected Map 수신자_세션_attribute_정보; protected Map 메시지_전송_데이터; + protected Map 잘못된_메시지_전송_데이터; + protected WebSocketSessions 채팅방에_해당하는_세션; protected CreateReadMessageLogEvent 메시지_로그_생성_이벤트; @BeforeEach void setUpFixture() { 발신자 = User.builder() - .name("발신자") - .profileImage(new ProfileImage("upload.png", "store.png")) - .reliability(new Reliability(4.7d)) - .oauthId("12345") - .build(); + .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(); + .name("수신자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12346") + .build(); userRepository.save(발신자); userRepository.save(수신자); @@ -73,13 +75,13 @@ void setUpFixture() { 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(); + .title("경매") + .seller(수신자) + .description("description") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(10_000)) + .closingTime(LocalDateTime.now().plusDays(3L)) + .build(); auctionRepository.save(경매); 채팅방 = new ChatRoom(경매, 발신자); @@ -89,12 +91,20 @@ void setUpFixture() { 발신자_세션_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", "메시지 내용" + "type", + "message", + "chatRoomId", + String.valueOf(채팅방.getId()), + "receiverId", + String.valueOf(수신자.getId()), + "contents", + "메시지 내용" ); + 잘못된_메시지_전송_데이터 = Map.of("type", "wrong message type"); 메시지_로그_생성_이벤트 = new CreateReadMessageLogEvent(채팅방); + + 채팅방에_해당하는_세션 = new WebSocketSessions(); } protected void 메시지_로그를_생성한다() { diff --git a/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/fixture/MessageTypeHandlerTestFixture.java b/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/fixture/MessageTypeHandlerTestFixture.java new file mode 100644 index 000000000..a464c5464 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/chat/handler/fixture/MessageTypeHandlerTestFixture.java @@ -0,0 +1,99 @@ +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.domain.ChatRoom; +import com.ddang.ddang.chat.domain.WebSocketSessions; +import com.ddang.ddang.chat.domain.repository.ChatRoomRepository; +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 com.ddang.ddang.websocket.handler.dto.SessionAttributeDto; +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 MessageTypeHandlerTestFixture { + + @Autowired + private AuctionRepository auctionRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private ChatRoomRepository chatRoomRepository; + + @Autowired + private JpaCategoryRepository categoryRepository; + + protected User 발신자; + protected User 수신자; + protected SessionAttributeDto 발신자_세션_속성_dto; + protected Map 메시지_데이터; + protected ChatRoom 채팅방; + protected WebSocketSessions 발신자만_존재하는_웹소켓_세션들; + protected WebSocketSessions 발신자와_수신자가_존재하는_웹소켓_세션들; + protected Map 발신자_세션_attribute_정보; + protected Map 수신자_세션_attribute_정보; + + @BeforeEach + void setUpFixture() { + final Category 전자기기 = new Category("전자기기"); + final Category 전자기기_하위_노트북 = new Category("노트북"); + 전자기기.addSubCategory(전자기기_하위_노트북); + categoryRepository.save(전자기기); + + final Auction 경매 = Auction.builder() + .title("경매") + .description("description") + .bidUnit(new BidUnit(1_000)) + .startPrice(new Price(10_000)) + .closingTime(LocalDateTime.now().plusDays(3L)) + .build(); + auctionRepository.save(경매); + + 발신자 = 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(); + final User 탈퇴한_사용자 = User.builder() + .name("탈퇴한 사용자") + .profileImage(new ProfileImage("upload.png", "store.png")) + .reliability(new Reliability(4.7d)) + .oauthId("12347") + .build(); + 탈퇴한_사용자.withdrawal(); + userRepository.save(발신자); + userRepository.save(수신자); + + 채팅방 = new ChatRoom(경매, 발신자); + chatRoomRepository.save(채팅방); + + 발신자_세션_속성_dto = new SessionAttributeDto(발신자.getId(), "/image.png"); + 메시지_데이터 = new HashMap<>(Map.of("chatRoomId", "1", "receiverId", "1", "contents", "메시지 내용")); + + 발신자만_존재하는_웹소켓_세션들 = new WebSocketSessions(); + 발신자와_수신자가_존재하는_웹소켓_세션들 = new WebSocketSessions(); + + 발신자_세션_attribute_정보 = new HashMap<>(Map.of("userId", 발신자.getId(), "baseUrl", "/images")); + 수신자_세션_attribute_정보 = new HashMap<>(Map.of("userId", 수신자.getId(), "baseUrl", "/images")); + } +} 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 847fbcb40..4ff226ba4 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 @@ -121,6 +121,7 @@ void setUpFixture() { "baseUrl", 이미지_절대_경로 )); 메시지_전송_데이터 = Map.of( + "type", "message", "chatRoomId", String.valueOf(채팅방.getId()), "receiverId", String.valueOf(수신자_겸_기존_입찰자.getId()), "contents", "메시지 내용" 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 index bde1854a7..1a524a3e0 100644 --- 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 @@ -20,8 +20,6 @@ class WebSocketHandleTextMessageProviderCompositeTest { void 지원하는_웹소켓_메시지_타입을_전달하면_해당_웹소켓_메시지_핸들러_provider를_반환한다() { // given final ChatWebSocketHandleTextMessageProvider provider = new ChatWebSocketHandleTextMessageProvider( - null, - null, null, null, null 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 index 40b7cca3e..2a9f3f522 100644 --- 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 @@ -9,9 +9,12 @@ import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; @@ -23,6 +26,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +@ExtendWith({MockitoExtension.class}) @IsolateDatabase @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) @SuppressWarnings("NonAsciiCharacters") @@ -64,7 +68,7 @@ class WebSocketHandlerTest extends WebSocketHandlerTestFixture { given(session.getAttributes()).willReturn(세션_attribute_정보); // when - webSocketHandler.afterConnectionClosed(session, null); + webSocketHandler.afterConnectionClosed(session, CloseStatus.NORMAL); // then verify(provider, times(1)).remove(any(WebSocketSession.class)); diff --git a/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/dto/ChatMessageTypeTest.java b/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/dto/ChatMessageTypeTest.java new file mode 100644 index 000000000..9a34259fc --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/websocket/handler/dto/ChatMessageTypeTest.java @@ -0,0 +1,36 @@ +package com.ddang.ddang.websocket.handler.dto; + +import com.ddang.ddang.websocket.handler.exception.UnsupportedChattingTypeException; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SuppressWarnings("NonAsciiCharacters") +class ChatMessageTypeTest { + + @Test + void 타입에_해당하는_enum을_반환한다() { + // given + final Map data = Map.of("type", "message"); + + // when + final ChatMessageType actual = ChatMessageType.findMessageType(data.get("type")); + + // then + assertThat(actual).isEqualTo(ChatMessageType.MESSAGE); + } + + + @Test + void 해당하는_타입이_없는_경우_예외를_던진다() { + // given + final Map data = Map.of("type", "wrong type"); + + // when & then + assertThatThrownBy(() -> ChatMessageType.findMessageType(data.get("type"))) + .isInstanceOf(UnsupportedChattingTypeException.class); + } +}