From 722c476c9c6c98edb9c20112e0d047c82d072c93 Mon Sep 17 00:00:00 2001 From: hyeeuncho Date: Fri, 29 May 2026 12:55:05 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20AI=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AiMessageServiceTest.java | 173 +++++++++++++++++ .../msa/ai_service/service/AiServiceTest.java | 180 ++++++++++++++++++ 2 files changed, 353 insertions(+) create mode 100644 ai-service/src/test/java/com/msa/ai_service/service/AiMessageServiceTest.java create mode 100644 ai-service/src/test/java/com/msa/ai_service/service/AiServiceTest.java diff --git a/ai-service/src/test/java/com/msa/ai_service/service/AiMessageServiceTest.java b/ai-service/src/test/java/com/msa/ai_service/service/AiMessageServiceTest.java new file mode 100644 index 00000000..800ece5e --- /dev/null +++ b/ai-service/src/test/java/com/msa/ai_service/service/AiMessageServiceTest.java @@ -0,0 +1,173 @@ +package com.msa.ai_service.service; + +import com.msa.ai_service.dto.AiMessageResponse; +import com.msa.ai_service.entity.AiMessage; +import com.msa.ai_service.entity.AiMessageStatus; +import com.msa.ai_service.entity.AiRequestType; +import com.msa.ai_service.exception.AiErrorCode; +import com.msa.ai_service.repository.AiMessageRepository; +import com.msa.core_common.error.exception.CustomException; +import com.msa.core_common.response.paging.PageRes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AiMessageServiceTest { + @InjectMocks + private AiMessageService aiMessageService; + + @Mock + private AiMessageRepository aiMessageRepository; + + private UUID aiMessageId; + private UUID deliveryId; + private AiMessage aiMessage; + + @BeforeEach + void setUp() { + aiMessageId = UUID.randomUUID(); + deliveryId = UUID.randomUUID(); + + aiMessage = AiMessage.builder() + .aiMessageId(aiMessageId) + .deliveryId(deliveryId) + .requestType(AiRequestType.DELIVERY_DEADLINE) + .prompt("프롬프트") + .requestPayload("{payload}") + .status(AiMessageStatus.PENDING) + .errorMessage("") + .build(); + } + + @Test + @DisplayName("saveMessage - 요청 이력 저장 성공") + void saveMessage_Success() { + when(aiMessageRepository.save(any(AiMessage.class))).thenReturn(aiMessage); + + AiMessage result = aiMessageService.saveMessage( + deliveryId, + AiRequestType.DELIVERY_DEADLINE, + "프롬프트", + "{payload}" + ); + + assertThat(result).isNotNull(); + assertThat(result.getAiMessageId()).isEqualTo(aiMessageId); + verify(aiMessageRepository, times(1)).save(any(AiMessage.class)); + } + + @Test + @DisplayName("markCompleted - 상태를 SUCCESS로 변경") + void markCompleted_Success() { + LocalDateTime deadline = LocalDateTime.of(2026, 5, 30, 15, 0); + String response = "최종 발송 시한 안내"; + + when(aiMessageRepository.findByAiMessageIdAndDeletedAtIsNull(aiMessageId)) + .thenReturn(Optional.of(aiMessage)); + + aiMessageService.markCompleted(aiMessageId, deadline, response); + + assertThat(aiMessage.getStatus()).isEqualTo(AiMessageStatus.SUCCESS); + assertThat(aiMessage.getFinalDepartureDeadline()).isEqualTo(deadline); + assertThat(aiMessage.getResponseContent()).isEqualTo(response); + } + + @Test + @DisplayName("markFailed - 상태를 FAILED로 변경") + void markFailed_Success() { + when(aiMessageRepository.findByAiMessageIdAndDeletedAtIsNull(aiMessageId)) + .thenReturn(Optional.of(aiMessage)); + + aiMessageService.markFailed(aiMessageId, "AI 응답 파싱 실패"); + + assertThat(aiMessage.getStatus()).isEqualTo(AiMessageStatus.FAILED); + assertThat(aiMessage.getErrorMessage()).isEqualTo("AI 응답 파싱 실패"); + } + + @Test + @DisplayName("getAiMessages - MASTER 권한으로 목록 조회 성공") + void getAiMessages_Success_Master() { + Pageable pageable = PageRequest.of(0, 10); + Page page = new PageImpl<>(List.of(aiMessage), pageable, 1); + + when(aiMessageRepository.findAllByCondition( + eq(AiRequestType.DELIVERY_DEADLINE), + eq(AiMessageStatus.PENDING), + eq(pageable) + )).thenReturn(page); + + PageRes result = aiMessageService.getAiMessages( + "MASTER", + AiRequestType.DELIVERY_DEADLINE, + AiMessageStatus.PENDING, + pageable + ); + + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getAiMessageId()).isEqualTo(aiMessageId); + } + + @Test + @DisplayName("getAiMessage - MASTER 권한으로 단건 조회 성공") + void getAiMessage_Success_Master() { + when(aiMessageRepository.findByAiMessageIdAndDeletedAtIsNull(aiMessageId)) + .thenReturn(Optional.of(aiMessage)); + + AiMessageResponse result = aiMessageService.getAiMessage("MASTER", aiMessageId); + + assertThat(result.getAiMessageId()).isEqualTo(aiMessageId); + assertThat(result.getRequestType()).isEqualTo(AiRequestType.DELIVERY_DEADLINE); + } + + @Test + @DisplayName("getAiMessages - MASTER 권한이 아니면 접근 거부") + void getAiMessages_Fail_NotMaster() { + CustomException exception = assertThrows( + CustomException.class, + () -> aiMessageService.getAiMessages( + "HUB_MANAGER", + AiRequestType.DELIVERY_DEADLINE, + AiMessageStatus.PENDING, + PageRequest.of(0, 10) + ) + ); + + assertThat(exception.getErrorCode()).isEqualTo(AiErrorCode.AI_MESSAGE_ACCESS_DENIED); + } + + @Test + @DisplayName("getAiMessageEntity - 존재하지 않는 메시지 조회 시 예외 발생") + void getAiMessageEntity_Fail_NotFound() { + when(aiMessageRepository.findByAiMessageIdAndDeletedAtIsNull(aiMessageId)) + .thenReturn(Optional.empty()); + + CustomException exception = assertThrows( + CustomException.class, + () -> aiMessageService.getAiMessageEntity(aiMessageId) + ); + + assertThat(exception.getErrorCode()).isEqualTo(AiErrorCode.AI_MESSAGE_NOT_FOUND); + } +} diff --git a/ai-service/src/test/java/com/msa/ai_service/service/AiServiceTest.java b/ai-service/src/test/java/com/msa/ai_service/service/AiServiceTest.java new file mode 100644 index 00000000..06c9d421 --- /dev/null +++ b/ai-service/src/test/java/com/msa/ai_service/service/AiServiceTest.java @@ -0,0 +1,180 @@ +package com.msa.ai_service.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.msa.ai_service.client.AiClient; +import com.msa.ai_service.dto.AiDeadlineResult; +import com.msa.ai_service.dto.AiResponse; +import com.msa.ai_service.entity.AiMessage; +import com.msa.ai_service.entity.AiRequestType; +import com.msa.ai_service.exception.AiErrorCode; +import com.msa.ai_service.parser.AiResponseParser; +import com.msa.ai_service.prompt.DeadlinePromptGenerator; +import com.msa.ai_service.stream.event.DeadlineNotificationRequestedEvent; +import com.msa.core_common.error.exception.CustomException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AiServiceTest { + @InjectMocks + private AiService aiService; + + @Mock + private AiClient aiClient; + + @Mock + private DeadlinePromptGenerator deadlinePromptGenerator; + + @Mock + private AiMessageService aiMessageService; + + @Mock + private ObjectMapper objectMapper; + + @Mock + private AiResponseParser aiResponseParser; + + private DeadlineNotificationRequestedEvent event; + private UUID aiMessageId; + private UUID deliveryId; + + @BeforeEach + void setUp() { + aiMessageId = UUID.randomUUID(); + deliveryId = UUID.randomUUID(); + + event = DeadlineNotificationRequestedEvent.builder() + .eventId(UUID.randomUUID()) + .deliveryId(deliveryId) + .orderId(UUID.randomUUID()) + .ordererName("홍길동") + .ordererEmail("hong@test.com") + .orderedAt(LocalDateTime.of(2026, 5, 29, 9, 0)) + .requestMessage("문 앞 배송") + .receiverUserId(UUID.randomUUID()) + .receiverSlackId("U123456") + .products(List.of(DeadlineNotificationRequestedEvent.ProductInfo.builder() + .productName("콜라") + .quantity(2) + .build())) + .requestedArrivalAt(LocalDateTime.of(2026, 5, 30, 18, 0)) + .departureHubName("서울 허브") + .destinationAddress("서울시 강남구") + .deliveryManagerName("김매니저") + .deliveryManagerEmail("manager@test.com") + .routeInfo(List.of(DeadlineNotificationRequestedEvent.RouteInfo.builder() + .departureHubName("서울 허브") + .arrivalHubName("강남 허브") + .estimatedDistanceKm(10.0) + .estimatedDurationMin(30) + .routeType("HUB_TO_HUB") + .build())) + .workStartTime(LocalTime.of(9, 0)) + .workEndTime(LocalTime.of(18, 0)) + .build(); + } + + @Test + @DisplayName("generateDeadline - AI 호출 성공 시 완료 처리 및 결과 반환") + void generateDeadline_Success() throws JsonProcessingException { + String prompt = "프롬프트"; + String payload = "{\"deliveryId\":\"123\"}"; + LocalDateTime deadline = LocalDateTime.of(2026, 5, 30, 15, 0); + String message = "최종 발송 시한은 2026-05-30 15:00 입니다."; + + AiMessage savedMessage = AiMessage.builder() + .aiMessageId(aiMessageId) + .deliveryId(deliveryId) + .requestType(AiRequestType.DELIVERY_DEADLINE) + .prompt(prompt) + .requestPayload(payload) + .build(); + + AiResponse aiResponse = mock(AiResponse.class); + + when(deadlinePromptGenerator.generatePrompt(event)).thenReturn(prompt); + when(objectMapper.writeValueAsString(event)).thenReturn(payload); + when(aiMessageService.saveMessage(deliveryId, AiRequestType.DELIVERY_DEADLINE, prompt, payload)) + .thenReturn(savedMessage); + when(aiClient.generate(prompt)).thenReturn(aiResponse); + when(aiResponse.getText()).thenReturn("{json}"); + when(aiResponseParser.parseDeadlineResponse("{json}")) + .thenReturn(AiDeadlineResult.of(null, deadline, message)); + + AiDeadlineResult result = aiService.generateDeadline(event); + + assertThat(result.getAiMessageId()).isEqualTo(aiMessageId); + assertThat(result.getFinalDepartureDeadline()).isEqualTo(deadline); + assertThat(result.getMessage()).isEqualTo(message); + + verify(aiMessageService, times(1)).markCompleted(aiMessageId, deadline, message); + verify(aiMessageService, never()).markFailed(any(), anyString()); + } + + @Test + @DisplayName("generateDeadline - AI 응답이 비어있으면 실패 처리 후 예외 발생") + void generateDeadline_Fail_EmptyAiResponse() throws JsonProcessingException { + String prompt = "프롬프트"; + String payload = "{\"deliveryId\":\"123\"}"; + + AiMessage savedMessage = AiMessage.builder() + .aiMessageId(aiMessageId) + .deliveryId(deliveryId) + .requestType(AiRequestType.DELIVERY_DEADLINE) + .prompt(prompt) + .requestPayload(payload) + .build(); + + AiResponse aiResponse = mock(AiResponse.class); + + when(deadlinePromptGenerator.generatePrompt(event)).thenReturn(prompt); + when(objectMapper.writeValueAsString(event)).thenReturn(payload); + when(aiMessageService.saveMessage(deliveryId, AiRequestType.DELIVERY_DEADLINE, prompt, payload)) + .thenReturn(savedMessage); + when(aiClient.generate(prompt)).thenReturn(aiResponse); + when(aiResponse.getText()).thenReturn(" "); + + IllegalStateException exception = assertThrows( + IllegalStateException.class, + () -> aiService.generateDeadline(event) + ); + + assertThat(exception.getMessage()).isEqualTo("AI 응답이 비어 있습니다."); + verify(aiMessageService, times(1)).markFailed(eq(aiMessageId), eq("AI 응답이 비어 있습니다.")); + verify(aiMessageService, never()).markCompleted(any(), any(), anyString()); + } + + @Test + @DisplayName("generateDeadline - 요청 payload 변환 실패 시 커스텀 예외 발생") + void generateDeadline_Fail_RequestPayloadConvert() throws JsonProcessingException { + when(deadlinePromptGenerator.generatePrompt(event)).thenReturn("프롬프트"); + when(objectMapper.writeValueAsString(event)) + .thenThrow(new JsonProcessingException("직렬화 실패") { + }); + + CustomException exception = assertThrows( + CustomException.class, + () -> aiService.generateDeadline(event) + ); + + assertThat(exception.getErrorCode()).isEqualTo(AiErrorCode.AI_REQUEST_PAYLOAD_CONVERT_FAILED); + verify(aiMessageService, never()).saveMessage(any(), any(), anyString(), anyString()); + } +} From aedbf99d8afb37d704a33d0eaa4c4ecb124d3327 Mon Sep 17 00:00:00 2001 From: hyeeuncho Date: Fri, 29 May 2026 12:55:12 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20Slack=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/SlackMessageServiceTest.java | 187 ++++++++++++++++++ .../service/SlackServiceTest.java | 142 +++++++++++++ 2 files changed, 329 insertions(+) create mode 100644 slack-service/src/test/java/com/msa/slack_service/service/SlackMessageServiceTest.java create mode 100644 slack-service/src/test/java/com/msa/slack_service/service/SlackServiceTest.java diff --git a/slack-service/src/test/java/com/msa/slack_service/service/SlackMessageServiceTest.java b/slack-service/src/test/java/com/msa/slack_service/service/SlackMessageServiceTest.java new file mode 100644 index 00000000..e0492e1c --- /dev/null +++ b/slack-service/src/test/java/com/msa/slack_service/service/SlackMessageServiceTest.java @@ -0,0 +1,187 @@ +package com.msa.slack_service.service; + +import com.msa.core_common.error.exception.CustomException; +import com.msa.core_common.response.paging.PageRes; +import com.msa.slack_service.dto.SlackMessageResponse; +import com.msa.slack_service.entity.MessageType; +import com.msa.slack_service.entity.SlackMessage; +import com.msa.slack_service.entity.SlackMessageStatus; +import com.msa.slack_service.exception.SlackErrorCode; +import com.msa.slack_service.repository.SlackMessageRepository; +import com.msa.slack_service.stream.event.DeadlineGeneratedEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SlackMessageServiceTest { + @InjectMocks + private SlackMessageService slackMessageService; + + @Mock + private SlackMessageRepository slackMessageRepository; + + private UUID slackMessageId; + private String idempotencyKey; + private DeadlineGeneratedEvent event; + private SlackMessage slackMessage; + + @BeforeEach + void setUp() { + slackMessageId = UUID.randomUUID(); + idempotencyKey = UUID.randomUUID().toString(); + + event = new DeadlineGeneratedEvent( + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID(), + "U123456", + MessageType.DELIVERY_DEADLINE, + "메시지 본문", + LocalDateTime.of(2026, 5, 30, 15, 0) + ); + + slackMessage = SlackMessage.builder() + .slackMessageId(slackMessageId) + .receiverUserId(event.getReceiverUserId()) + .aiMessageId(event.getAiMessageId()) + .receiverSlackId(event.getReceiverSlackId()) + .idempotencyKey(idempotencyKey) + .messageType(event.getMessageType()) + .message(event.getMessage()) + .status(SlackMessageStatus.PENDING) + .build(); + } + + @Test + @DisplayName("findOrCreateMessage - 기존 멱등 키 메시지 반환") + void findOrCreateMessage_ReturnExisting() { + when(slackMessageRepository.findByIdempotencyKey(idempotencyKey)) + .thenReturn(Optional.of(slackMessage)); + + SlackMessage result = slackMessageService.findOrCreateMessage(event, idempotencyKey); + + assertThat(result.getSlackMessageId()).isEqualTo(slackMessageId); + verify(slackMessageRepository, never()).save(any(SlackMessage.class)); + } + + @Test + @DisplayName("findOrCreateMessage - 멱등 키가 없으면 신규 저장") + void findOrCreateMessage_CreateNew() { + when(slackMessageRepository.findByIdempotencyKey(idempotencyKey)) + .thenReturn(Optional.empty()); + when(slackMessageRepository.save(any(SlackMessage.class))).thenReturn(slackMessage); + + SlackMessage result = slackMessageService.findOrCreateMessage(event, idempotencyKey); + + assertThat(result).isNotNull(); + verify(slackMessageRepository, times(1)).save(any(SlackMessage.class)); + } + + @Test + @DisplayName("markSent - 상태를 SENT로 변경") + void markSent_Success() { + when(slackMessageRepository.findById(slackMessageId)).thenReturn(Optional.of(slackMessage)); + + slackMessageService.markSent(slackMessageId); + + assertThat(slackMessage.getStatus()).isEqualTo(SlackMessageStatus.SENT); + assertThat(slackMessage.getSentAt()).isNotNull(); + } + + @Test + @DisplayName("markFailed - 상태를 FAILED로 변경") + void markFailed_Success() { + when(slackMessageRepository.findById(slackMessageId)).thenReturn(Optional.of(slackMessage)); + + slackMessageService.markFailed(slackMessageId, "전송 실패"); + + assertThat(slackMessage.getStatus()).isEqualTo(SlackMessageStatus.FAILED); + assertThat(slackMessage.getErrorMessage()).isEqualTo("전송 실패"); + } + + @Test + @DisplayName("getSlackMessages - MASTER 권한으로 목록 조회 성공") + void getSlackMessages_Success_Master() { + Pageable pageable = PageRequest.of(0, 10); + Page page = new PageImpl<>(List.of(slackMessage), pageable, 1); + + when(slackMessageRepository.findAllByCondition( + eq(SlackMessageStatus.PENDING), + eq(MessageType.DELIVERY_DEADLINE), + eq(pageable) + )).thenReturn(page); + + PageRes result = slackMessageService.getSlackMessages( + "MASTER", + SlackMessageStatus.PENDING, + MessageType.DELIVERY_DEADLINE, + pageable + ); + + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getSlackMessageId()).isEqualTo(slackMessageId); + } + + @Test + @DisplayName("getSlackMessage - MASTER 권한으로 단건 조회 성공") + void getSlackMessage_Success_Master() { + when(slackMessageRepository.findById(slackMessageId)).thenReturn(Optional.of(slackMessage)); + + SlackMessageResponse result = slackMessageService.getSlackMessage("MASTER", slackMessageId); + + assertThat(result.getSlackMessageId()).isEqualTo(slackMessageId); + } + + @Test + @DisplayName("getSlackMessages - MASTER 권한이 아니면 접근 거부") + void getSlackMessages_Fail_NotMaster() { + CustomException exception = assertThrows( + CustomException.class, + () -> slackMessageService.getSlackMessages( + "HUB_MANAGER", + SlackMessageStatus.PENDING, + MessageType.DELIVERY_DEADLINE, + PageRequest.of(0, 10) + ) + ); + + assertThat(exception.getErrorCode()).isEqualTo(SlackErrorCode.SLACK_MESSAGE_ACCESS_DENIED); + } + + @Test + @DisplayName("getEntity - 존재하지 않는 메시지 조회 시 예외 발생") + void getEntity_Fail_NotFound() { + when(slackMessageRepository.findById(slackMessageId)).thenReturn(Optional.empty()); + + CustomException exception = assertThrows( + CustomException.class, + () -> slackMessageService.getEntity(slackMessageId) + ); + + assertThat(exception.getErrorCode()).isEqualTo(SlackErrorCode.SLACK_MESSAGE_NOT_FOUND); + } +} diff --git a/slack-service/src/test/java/com/msa/slack_service/service/SlackServiceTest.java b/slack-service/src/test/java/com/msa/slack_service/service/SlackServiceTest.java new file mode 100644 index 00000000..55cfb813 --- /dev/null +++ b/slack-service/src/test/java/com/msa/slack_service/service/SlackServiceTest.java @@ -0,0 +1,142 @@ +package com.msa.slack_service.service; + +import com.msa.core_common.error.exception.CustomException; +import com.msa.slack_service.client.SlackClient; +import com.msa.slack_service.entity.MessageType; +import com.msa.slack_service.entity.SlackMessage; +import com.msa.slack_service.entity.SlackMessageStatus; +import com.msa.slack_service.exception.SlackErrorCode; +import com.msa.slack_service.stream.event.DeadlineGeneratedEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.LocalDateTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SlackServiceTest { + @InjectMocks + private SlackService slackService; + + @Mock + private SlackMessageService slackMessageService; + + @Mock + private SlackClient slackClient; + + private UUID slackMessageId; + private DeadlineGeneratedEvent event; + private SlackMessage pendingMessage; + + @BeforeEach + void setUp() { + slackMessageId = UUID.randomUUID(); + + event = new DeadlineGeneratedEvent( + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID(), + UUID.randomUUID(), + "U123456", + MessageType.DELIVERY_DEADLINE, + "최종 발송 시한은 2026-05-30 15:00 입니다.", + LocalDateTime.of(2026, 5, 30, 15, 0) + ); + + pendingMessage = SlackMessage.builder() + .slackMessageId(slackMessageId) + .receiverUserId(event.getReceiverUserId()) + .aiMessageId(event.getAiMessageId()) + .receiverSlackId(event.getReceiverSlackId()) + .idempotencyKey(event.getEventId().toString()) + .messageType(event.getMessageType()) + .message(event.getMessage()) + .status(SlackMessageStatus.PENDING) + .build(); + } + + @Test + @DisplayName("processDeadlineGenerated - PENDING 메시지 전송 성공") + void processDeadlineGenerated_Success() { + when(slackMessageService.findOrCreateMessage(event, event.getEventId().toString())) + .thenReturn(pendingMessage); + + slackService.processDeadlineGenerated(event); + + verify(slackClient, times(1)).sendMessage(event.getReceiverSlackId(), event.getMessage()); + verify(slackMessageService, times(1)).markSent(slackMessageId); + verify(slackMessageService, never()).markFailed(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.anyString()); + } + + @Test + @DisplayName("processDeadlineGenerated - 이미 SENT면 전송 생략") + void processDeadlineGenerated_AlreadySent() { + SlackMessage sentMessage = SlackMessage.builder() + .slackMessageId(slackMessageId) + .receiverSlackId(event.getReceiverSlackId()) + .message(event.getMessage()) + .status(SlackMessageStatus.SENT) + .build(); + + when(slackMessageService.findOrCreateMessage(event, event.getEventId().toString())) + .thenReturn(sentMessage); + + slackService.processDeadlineGenerated(event); + + verify(slackClient, never()).sendMessage(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString()); + verify(slackMessageService, never()).markSent(org.mockito.ArgumentMatchers.any()); + } + + @Test + @DisplayName("processDeadlineGenerated - 전송 실패 시 FAILED 처리 후 예외 전파") + void processDeadlineGenerated_Fail_Send() { + when(slackMessageService.findOrCreateMessage(event, event.getEventId().toString())) + .thenReturn(pendingMessage); + doThrow(new RuntimeException("slack api error")) + .when(slackClient).sendMessage(event.getReceiverSlackId(), event.getMessage()); + + RuntimeException exception = assertThrows( + RuntimeException.class, + () -> slackService.processDeadlineGenerated(event) + ); + + assertThat(exception.getMessage()).isEqualTo("slack api error"); + verify(slackMessageService, times(1)).markFailed(slackMessageId, "slack api error"); + } + + @Test + @DisplayName("resendSlackMessage - MASTER 권한으로 재전송 성공") + void resendSlackMessage_Success_Master() { + when(slackMessageService.getEntity(slackMessageId)).thenReturn(pendingMessage); + + slackService.resendSlackMessage("MASTER", slackMessageId); + + verify(slackClient, times(1)).sendMessage(event.getReceiverSlackId(), event.getMessage()); + verify(slackMessageService, times(1)).markSent(slackMessageId); + } + + @Test + @DisplayName("resendSlackMessage - MASTER 권한이 아니면 접근 거부") + void resendSlackMessage_Fail_NotMaster() { + CustomException exception = assertThrows( + CustomException.class, + () -> slackService.resendSlackMessage("HUB_MANAGER", slackMessageId) + ); + + assertThat(exception.getErrorCode()).isEqualTo(SlackErrorCode.SLACK_MESSAGE_ACCESS_DENIED); + verify(slackMessageService, never()).getEntity(org.mockito.ArgumentMatchers.any()); + } +} From 70d7d05688b38f34f2206ade7621d7e92c095734 Mon Sep 17 00:00:00 2001 From: hyeeuncho Date: Fri, 29 May 2026 13:57:13 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20=EC=88=98=EC=A0=95=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai_service/service/AiMessageServiceTest.java | 3 ++- .../service/SlackMessageServiceTest.java | 3 ++- .../slack_service/service/SlackServiceTest.java | 15 +++++++++------ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/ai-service/src/test/java/com/msa/ai_service/service/AiMessageServiceTest.java b/ai-service/src/test/java/com/msa/ai_service/service/AiMessageServiceTest.java index 800ece5e..2b5c5876 100644 --- a/ai-service/src/test/java/com/msa/ai_service/service/AiMessageServiceTest.java +++ b/ai-service/src/test/java/com/msa/ai_service/service/AiMessageServiceTest.java @@ -6,6 +6,7 @@ import com.msa.ai_service.entity.AiRequestType; import com.msa.ai_service.exception.AiErrorCode; import com.msa.ai_service.repository.AiMessageRepository; +import com.msa.core_common.auth.UserRole; import com.msa.core_common.error.exception.CustomException; import com.msa.core_common.response.paging.PageRes; import org.junit.jupiter.api.BeforeEach; @@ -119,7 +120,7 @@ void getAiMessages_Success_Master() { )).thenReturn(page); PageRes result = aiMessageService.getAiMessages( - "MASTER", + UserRole.MASTER.name(), AiRequestType.DELIVERY_DEADLINE, AiMessageStatus.PENDING, pageable diff --git a/slack-service/src/test/java/com/msa/slack_service/service/SlackMessageServiceTest.java b/slack-service/src/test/java/com/msa/slack_service/service/SlackMessageServiceTest.java index e0492e1c..f641a16f 100644 --- a/slack-service/src/test/java/com/msa/slack_service/service/SlackMessageServiceTest.java +++ b/slack-service/src/test/java/com/msa/slack_service/service/SlackMessageServiceTest.java @@ -1,5 +1,6 @@ package com.msa.slack_service.service; +import com.msa.core_common.auth.UserRole; import com.msa.core_common.error.exception.CustomException; import com.msa.core_common.response.paging.PageRes; import com.msa.slack_service.dto.SlackMessageResponse; @@ -136,7 +137,7 @@ void getSlackMessages_Success_Master() { )).thenReturn(page); PageRes result = slackMessageService.getSlackMessages( - "MASTER", + UserRole.MASTER.name(), SlackMessageStatus.PENDING, MessageType.DELIVERY_DEADLINE, pageable diff --git a/slack-service/src/test/java/com/msa/slack_service/service/SlackServiceTest.java b/slack-service/src/test/java/com/msa/slack_service/service/SlackServiceTest.java index 55cfb813..863ecc0a 100644 --- a/slack-service/src/test/java/com/msa/slack_service/service/SlackServiceTest.java +++ b/slack-service/src/test/java/com/msa/slack_service/service/SlackServiceTest.java @@ -1,5 +1,6 @@ package com.msa.slack_service.service; +import com.msa.core_common.auth.UserRole; import com.msa.core_common.error.exception.CustomException; import com.msa.slack_service.client.SlackClient; import com.msa.slack_service.entity.MessageType; @@ -25,6 +26,8 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; @ExtendWith(MockitoExtension.class) class SlackServiceTest { @@ -78,7 +81,7 @@ void processDeadlineGenerated_Success() { verify(slackClient, times(1)).sendMessage(event.getReceiverSlackId(), event.getMessage()); verify(slackMessageService, times(1)).markSent(slackMessageId); - verify(slackMessageService, never()).markFailed(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.anyString()); + verify(slackMessageService, never()).markFailed(any(), anyString()); } @Test @@ -96,8 +99,8 @@ void processDeadlineGenerated_AlreadySent() { slackService.processDeadlineGenerated(event); - verify(slackClient, never()).sendMessage(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString()); - verify(slackMessageService, never()).markSent(org.mockito.ArgumentMatchers.any()); + verify(slackClient, never()).sendMessage(anyString(), anyString()); + verify(slackMessageService, never()).markSent(any()); } @Test @@ -122,7 +125,7 @@ void processDeadlineGenerated_Fail_Send() { void resendSlackMessage_Success_Master() { when(slackMessageService.getEntity(slackMessageId)).thenReturn(pendingMessage); - slackService.resendSlackMessage("MASTER", slackMessageId); + slackService.resendSlackMessage(UserRole.MASTER.name(), slackMessageId); verify(slackClient, times(1)).sendMessage(event.getReceiverSlackId(), event.getMessage()); verify(slackMessageService, times(1)).markSent(slackMessageId); @@ -133,10 +136,10 @@ void resendSlackMessage_Success_Master() { void resendSlackMessage_Fail_NotMaster() { CustomException exception = assertThrows( CustomException.class, - () -> slackService.resendSlackMessage("HUB_MANAGER", slackMessageId) + () -> slackService.resendSlackMessage(UserRole.HUB_MANAGER.name(), slackMessageId) ); assertThat(exception.getErrorCode()).isEqualTo(SlackErrorCode.SLACK_MESSAGE_ACCESS_DENIED); - verify(slackMessageService, never()).getEntity(org.mockito.ArgumentMatchers.any()); + verify(slackMessageService, never()).getEntity(any()); } }