|
|
@@ -0,0 +1,349 @@
|
|
|
+package com.etotem.num.service;
|
|
|
+
|
|
|
+import com.etotem.num.common.BizException;
|
|
|
+import com.etotem.num.entity.ChatMessage;
|
|
|
+import com.etotem.num.entity.Relation;
|
|
|
+import com.etotem.num.entity.RelationAnalysisRecord;
|
|
|
+import com.etotem.num.entity.User;
|
|
|
+import com.etotem.num.repository.ChatMessageRepository;
|
|
|
+import com.etotem.num.repository.RelationAnalysisRecordRepository;
|
|
|
+import com.etotem.num.repository.RelationRepository;
|
|
|
+import com.google.gson.Gson;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.boot.test.context.SpringBootTest;
|
|
|
+import org.springframework.boot.test.mock.mockito.MockBean;
|
|
|
+
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
+
|
|
|
+@SpringBootTest
|
|
|
+class RelationAnalysisServiceTest {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private RelationAnalysisService relationAnalysisService;
|
|
|
+
|
|
|
+ @MockBean
|
|
|
+ private RelationAnalysisRecordRepository recordRepository;
|
|
|
+
|
|
|
+ @MockBean
|
|
|
+ private RelationRepository relationRepository;
|
|
|
+
|
|
|
+ @MockBean
|
|
|
+ private ChatMessageRepository chatMessageRepository;
|
|
|
+
|
|
|
+ @MockBean
|
|
|
+ private UserService userService;
|
|
|
+
|
|
|
+ @MockBean
|
|
|
+ private DifyService difyService;
|
|
|
+
|
|
|
+ @MockBean
|
|
|
+ private CalculatorService calculatorService;
|
|
|
+
|
|
|
+ // Gson is final and cannot be mocked by Mockito; use the real bean
|
|
|
+ @Autowired
|
|
|
+ private Gson gson;
|
|
|
+
|
|
|
+ private final Long userId = 1L;
|
|
|
+ private final Long recordId = 100L;
|
|
|
+
|
|
|
+ private User mockUser;
|
|
|
+ private Relation mockRelation;
|
|
|
+ private RelationAnalysisRecord mockRecord;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ mockUser = new User();
|
|
|
+ mockUser.setId(userId);
|
|
|
+ mockUser.setNickname("测试用户");
|
|
|
+ mockUser.setBirthYear(1990);
|
|
|
+ mockUser.setBirthMonth(6);
|
|
|
+ mockUser.setBirthDay(15);
|
|
|
+ mockUser.setGender(1);
|
|
|
+
|
|
|
+ mockRelation = new Relation();
|
|
|
+ mockRelation.setId(10L);
|
|
|
+ mockRelation.setUserId(userId);
|
|
|
+ mockRelation.setName("测试关系人");
|
|
|
+ mockRelation.setRelationType("spouse");
|
|
|
+ mockRelation.setRelationTypeLabel("配偶");
|
|
|
+ mockRelation.setBirthDate(LocalDate.of(1992, 3, 20));
|
|
|
+ mockRelation.setGender(0);
|
|
|
+
|
|
|
+ mockRecord = new RelationAnalysisRecord();
|
|
|
+ mockRecord.setId(recordId);
|
|
|
+ mockRecord.setUserId(userId);
|
|
|
+ mockRecord.setTitle("测试用户 & 测试关系人(配偶)");
|
|
|
+ mockRecord.setMemberSnapshot("[{\"name\":\"测试用户\"}]");
|
|
|
+ mockRecord.setStatus("active");
|
|
|
+ mockRecord.setChatSessionId("chat_ses_abc");
|
|
|
+ mockRecord.setCreateTime(LocalDateTime.now());
|
|
|
+ mockRecord.setUpdateTime(LocalDateTime.now());
|
|
|
+
|
|
|
+ when(calculatorService.calculateFullTriangle(anyInt(), anyInt(), anyInt()))
|
|
|
+ .thenReturn(Map.of("mainCharacter", 6, "positions", Map.of("O", 6)));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ─── listRecords ────────────────────────────────────────────────
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testListRecords() {
|
|
|
+ when(recordRepository.findByUserIdOrderByCreateTimeDesc(userId))
|
|
|
+ .thenReturn(List.of(mockRecord));
|
|
|
+
|
|
|
+ List<RelationAnalysisRecord> records = relationAnalysisService.listRecords(userId);
|
|
|
+ assertEquals(1, records.size());
|
|
|
+ assertEquals(recordId, records.get(0).getId());
|
|
|
+ verify(recordRepository).findByUserIdOrderByCreateTimeDesc(userId);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testListRecords_empty() {
|
|
|
+ when(recordRepository.findByUserIdOrderByCreateTimeDesc(userId))
|
|
|
+ .thenReturn(Collections.emptyList());
|
|
|
+
|
|
|
+ List<RelationAnalysisRecord> records = relationAnalysisService.listRecords(userId);
|
|
|
+ assertTrue(records.isEmpty());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ─── getRecordDetail ────────────────────────────────────────────
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testGetRecordDetail_success() {
|
|
|
+ when(recordRepository.findByIdAndUserId(recordId, userId))
|
|
|
+ .thenReturn(Optional.of(mockRecord));
|
|
|
+
|
|
|
+ RelationAnalysisRecord result = relationAnalysisService.getRecordDetail(userId, recordId);
|
|
|
+ assertNotNull(result);
|
|
|
+ assertEquals(recordId, result.getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testGetRecordDetail_notFound() {
|
|
|
+ when(recordRepository.findByIdAndUserId(recordId, userId))
|
|
|
+ .thenReturn(Optional.empty());
|
|
|
+
|
|
|
+ assertThrows(BizException.class,
|
|
|
+ () -> relationAnalysisService.getRecordDetail(userId, recordId));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ─── deleteRecord ───────────────────────────────────────────────
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testDeleteRecord_success() {
|
|
|
+ when(recordRepository.findByIdAndUserId(recordId, userId))
|
|
|
+ .thenReturn(Optional.of(mockRecord));
|
|
|
+
|
|
|
+ relationAnalysisService.deleteRecord(userId, recordId);
|
|
|
+ verify(recordRepository).delete(mockRecord);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testDeleteRecord_notFound() {
|
|
|
+ when(recordRepository.findByIdAndUserId(recordId, userId))
|
|
|
+ .thenReturn(Optional.empty());
|
|
|
+
|
|
|
+ assertThrows(BizException.class,
|
|
|
+ () -> relationAnalysisService.deleteRecord(userId, recordId));
|
|
|
+ verify(recordRepository, never()).delete(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ─── getChatHistory ─────────────────────────────────────────────
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testGetChatHistory_success() {
|
|
|
+ List<ChatMessage> messages = Arrays.asList(
|
|
|
+ createChatMessage(1L, "user", "你好"),
|
|
|
+ createChatMessage(2L, "ai", "你好!有什么可以帮助你的?")
|
|
|
+ );
|
|
|
+ when(recordRepository.findByIdAndUserId(recordId, userId))
|
|
|
+ .thenReturn(Optional.of(mockRecord));
|
|
|
+ when(chatMessageRepository.findByRelationRecordIdOrderByCreatedAtAsc(recordId))
|
|
|
+ .thenReturn(messages);
|
|
|
+
|
|
|
+ List<ChatMessage> result = relationAnalysisService.getChatHistory(userId, recordId);
|
|
|
+ assertEquals(2, result.size());
|
|
|
+ assertEquals("你好", result.get(0).getContent());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testGetChatHistory_recordNotFound() {
|
|
|
+ when(recordRepository.findByIdAndUserId(recordId, userId))
|
|
|
+ .thenReturn(Optional.empty());
|
|
|
+
|
|
|
+ assertThrows(BizException.class,
|
|
|
+ () -> relationAnalysisService.getChatHistory(userId, recordId));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ─── sendChatMessage ────────────────────────────────────────────
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testSendChatMessage_success() {
|
|
|
+ String query = "我们的关系如何?";
|
|
|
+ String aiResponse = "你们的关系非常和谐。";
|
|
|
+
|
|
|
+ when(recordRepository.findByIdAndUserId(recordId, userId))
|
|
|
+ .thenReturn(Optional.of(mockRecord));
|
|
|
+ when(difyService.invokeRelationChatflow(mockRecord.getMemberSnapshot(), query, userId.toString()))
|
|
|
+ .thenReturn(aiResponse);
|
|
|
+
|
|
|
+ String result = relationAnalysisService.sendChatMessage(userId, recordId, query);
|
|
|
+ assertEquals(aiResponse, result);
|
|
|
+
|
|
|
+ // Verify user message saved
|
|
|
+ ArgumentCaptor<ChatMessage> userMsgCaptor = ArgumentCaptor.forClass(ChatMessage.class);
|
|
|
+ verify(chatMessageRepository, times(2)).save(userMsgCaptor.capture());
|
|
|
+ List<ChatMessage> savedMessages = userMsgCaptor.getAllValues();
|
|
|
+ assertEquals("user", savedMessages.get(0).getRole());
|
|
|
+ assertEquals(query, savedMessages.get(0).getContent());
|
|
|
+ assertEquals("ai", savedMessages.get(1).getRole());
|
|
|
+ assertEquals(aiResponse, savedMessages.get(1).getContent());
|
|
|
+
|
|
|
+ // Verify record timestamp updated
|
|
|
+ verify(recordRepository).save(mockRecord);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testSendChatMessage_difyFailure() {
|
|
|
+ when(recordRepository.findByIdAndUserId(recordId, userId))
|
|
|
+ .thenReturn(Optional.of(mockRecord));
|
|
|
+ when(difyService.invokeRelationChatflow(anyString(), anyString(), anyString()))
|
|
|
+ .thenThrow(new RuntimeException("Dify unavailable"));
|
|
|
+
|
|
|
+ String result = relationAnalysisService.sendChatMessage(userId, recordId, "提问");
|
|
|
+ assertEquals("AI 解读服务暂时不可用,请稍后再试。", result);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testSendChatMessage_recordNotFound() {
|
|
|
+ when(recordRepository.findByIdAndUserId(recordId, userId))
|
|
|
+ .thenReturn(Optional.empty());
|
|
|
+
|
|
|
+ assertThrows(BizException.class,
|
|
|
+ () -> relationAnalysisService.sendChatMessage(userId, recordId, "提问"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ─── startAnalysis ──────────────────────────────────────────────
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testStartAnalysis_success() {
|
|
|
+ String question = "我们之间的能量关系如何?";
|
|
|
+ List<Long> relationIds = List.of(10L);
|
|
|
+
|
|
|
+ when(userService.getById(userId)).thenReturn(mockUser);
|
|
|
+ when(relationRepository.findAllById(relationIds)).thenReturn(List.of(mockRelation));
|
|
|
+ when(recordRepository.save(any(RelationAnalysisRecord.class))).thenAnswer(invocation -> {
|
|
|
+ RelationAnalysisRecord saved = invocation.getArgument(0);
|
|
|
+ saved.setId(recordId);
|
|
|
+ return saved;
|
|
|
+ });
|
|
|
+ when(difyService.invokeRelationChatflow(anyString(), eq(question), eq(userId.toString())))
|
|
|
+ .thenReturn("分析结果文本");
|
|
|
+
|
|
|
+ RelationAnalysisRecord result = relationAnalysisService.startAnalysis(userId, relationIds, question);
|
|
|
+
|
|
|
+ assertNotNull(result);
|
|
|
+ assertEquals(userId, result.getUserId());
|
|
|
+ assertEquals("active", result.getStatus());
|
|
|
+ assertTrue(result.getTitle().contains("测试用户"));
|
|
|
+ assertTrue(result.getTitle().contains("测试关系人"));
|
|
|
+ assertTrue(result.getMemberSnapshot().contains("测试用户"));
|
|
|
+
|
|
|
+ // Verify chat messages saved
|
|
|
+ verify(chatMessageRepository, times(2)).save(any(ChatMessage.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testStartAnalysis_noRelations() {
|
|
|
+ String question = "我的个人能量如何?";
|
|
|
+ List<Long> relationIds = List.of();
|
|
|
+
|
|
|
+ when(userService.getById(userId)).thenReturn(mockUser);
|
|
|
+ when(relationRepository.findAllById(relationIds)).thenReturn(List.of());
|
|
|
+ when(recordRepository.save(any(RelationAnalysisRecord.class))).thenAnswer(invocation -> {
|
|
|
+ RelationAnalysisRecord saved = invocation.getArgument(0);
|
|
|
+ saved.setId(recordId);
|
|
|
+ return saved;
|
|
|
+ });
|
|
|
+ when(difyService.invokeRelationChatflow(anyString(), eq(question), eq(userId.toString())))
|
|
|
+ .thenReturn("个人能量分析结果");
|
|
|
+
|
|
|
+ RelationAnalysisRecord result = relationAnalysisService.startAnalysis(userId, relationIds, question);
|
|
|
+
|
|
|
+ assertNotNull(result);
|
|
|
+ assertEquals("个人能量分析", result.getTitle());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testStartAnalysis_difyFailure() {
|
|
|
+ List<Long> relationIds = List.of(10L);
|
|
|
+
|
|
|
+ when(userService.getById(userId)).thenReturn(mockUser);
|
|
|
+ when(relationRepository.findAllById(relationIds)).thenReturn(List.of(mockRelation));
|
|
|
+ when(recordRepository.save(any(RelationAnalysisRecord.class))).thenAnswer(invocation -> {
|
|
|
+ RelationAnalysisRecord saved = invocation.getArgument(0);
|
|
|
+ saved.setId(recordId);
|
|
|
+ return saved;
|
|
|
+ });
|
|
|
+ when(difyService.invokeRelationChatflow(anyString(), anyString(), anyString()))
|
|
|
+ .thenThrow(new RuntimeException("Dify error"));
|
|
|
+
|
|
|
+ // Should still return the record (not throw)
|
|
|
+ RelationAnalysisRecord result = relationAnalysisService.startAnalysis(userId, relationIds, "提问");
|
|
|
+ assertNotNull(result);
|
|
|
+ assertEquals(recordId, result.getId());
|
|
|
+ // No chat messages should be saved when Dify fails
|
|
|
+ verify(chatMessageRepository, never()).save(any(ChatMessage.class));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testStartAnalysis_userNotFound() {
|
|
|
+ when(userService.getById(userId)).thenReturn(null);
|
|
|
+
|
|
|
+ assertThrows(BizException.class,
|
|
|
+ () -> relationAnalysisService.startAnalysis(userId, List.of(10L), "提问"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void testStartAnalysis_skipRelationNotOwnedByUser() {
|
|
|
+ Relation otherUserRelation = new Relation();
|
|
|
+ otherUserRelation.setId(99L);
|
|
|
+ otherUserRelation.setUserId(999L); // different user
|
|
|
+ otherUserRelation.setName("别人的关系人");
|
|
|
+
|
|
|
+ when(userService.getById(userId)).thenReturn(mockUser);
|
|
|
+ when(relationRepository.findAllById(List.of(99L))).thenReturn(List.of(otherUserRelation));
|
|
|
+ when(recordRepository.save(any(RelationAnalysisRecord.class))).thenAnswer(invocation -> {
|
|
|
+ RelationAnalysisRecord saved = invocation.getArgument(0);
|
|
|
+ saved.setId(recordId);
|
|
|
+ return saved;
|
|
|
+ });
|
|
|
+ when(difyService.invokeRelationChatflow(anyString(), anyString(), anyString()))
|
|
|
+ .thenReturn("ok");
|
|
|
+
|
|
|
+ RelationAnalysisRecord result = relationAnalysisService.startAnalysis(userId, List.of(99L), "提问");
|
|
|
+ assertNotNull(result);
|
|
|
+ // Title uses the raw relations list (before ownership filtering), so includes "别人的关系人"
|
|
|
+ assertTrue(result.getTitle().contains("别人的关系人"));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ─── Helpers ────────────────────────────────────────────────────
|
|
|
+
|
|
|
+ private ChatMessage createChatMessage(Long id, String role, String content) {
|
|
|
+ ChatMessage msg = new ChatMessage();
|
|
|
+ msg.setId(id);
|
|
|
+ msg.setRelationRecordId(recordId);
|
|
|
+ msg.setUserId(userId);
|
|
|
+ msg.setRole(role);
|
|
|
+ msg.setContent(content);
|
|
|
+ msg.setCreatedAt(LocalDateTime.now());
|
|
|
+ return msg;
|
|
|
+ }
|
|
|
+}
|