You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

349 lines
18 KiB

package com.mydreamjob.messaging;
import com.mydreamjob.audit.AuditContext;
import com.mydreamjob.audit.SecurityAuditService;
import com.mydreamjob.auth.User;
import com.mydreamjob.auth.UserRepository;
import com.mydreamjob.bidirectionalmatch.ApplicationMatchRecordRepository;
import com.mydreamjob.common.exception.BusinessException;
import com.mydreamjob.common.exception.ErrorCode;
import com.mydreamjob.company.Company;
import com.mydreamjob.company.CompanyRepository;
import com.mydreamjob.messaging.dto.CreateConversationRequest;
import com.mydreamjob.messaging.dto.SendMessageRequest;
import com.mydreamjob.safety.BlockRelationRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class MessagingServiceTest {
@Mock private ConversationRepository conversationRepository;
@Mock private ConversationMemberRepository memberRepository;
@Mock private MessageRepository messageRepository;
@Mock private MessageReadRepository readRepository;
@Mock private UserRepository userRepository;
@Mock private CompanyRepository companyRepository;
@Mock private ApplicationMatchRecordRepository applicationMatchRecordRepository;
@Mock private BlockRelationRepository blockRelationRepository;
@Mock private SecurityAuditService securityAuditService;
@org.mockito.Spy private MessageViewMapper messageViewMapper = new MessageViewMapper();
@org.mockito.Spy private com.fasterxml.jackson.databind.ObjectMapper objectMapper =
new com.fasterxml.jackson.databind.ObjectMapper();
@InjectMocks private MessagingService messagingService;
private static final Long USER_A = 100L;
private static final Long USER_B = 200L;
private static final Long USER_C = 300L;
private static final Long CONV_ID = 1L;
@BeforeEach
void setUp() {
}
private Conversation conv(Long id) {
return Conversation.builder().id(id).createdBy(USER_A).type("DIRECT").build();
}
private void stubConversationExists() {
when(conversationRepository.findById(CONV_ID)).thenReturn(Optional.of(conv(CONV_ID)));
}
// ── 创建会话 ─────────────────────────────────────────────
@Test void createConversation_withNewUser_shouldCreate() {
CreateConversationRequest req = new CreateConversationRequest();
req.setParticipantUserId(USER_B);
when(memberRepository.findByUserId(USER_A)).thenReturn(List.of());
when(userRepository.existsById(USER_B)).thenReturn(true);
when(conversationRepository.save(any())).thenAnswer(inv -> {
Conversation c = inv.getArgument(0);
if (c.getId() == null) c.setId(99L);
return c;
});
when(memberRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
var r = messagingService.createOrGetDirectConversation(USER_A, req);
assertNotNull(r.get("id"));
assertEquals("DIRECT", r.get("type"));
verify(memberRepository, times(2)).save(any());
}
@Test void createConversation_withSelf_shouldReturn400() {
CreateConversationRequest req = new CreateConversationRequest();
req.setParticipantUserId(USER_A);
assertThrows(BusinessException.class,
() -> messagingService.createOrGetDirectConversation(USER_A, req));
}
// ── 防绕过守卫:企业-求职者新建会话需 MATCHED ──────────────
private User user(Long id, User.Role role) {
User u = User.builder().id(id).phone("1").role(role).build();
u.setRoles(new HashSet<>(Set.of(role)));
return u;
}
private void stubNewConversationCreation() {
when(memberRepository.findByUserId(any())).thenReturn(List.of());
when(conversationRepository.save(any())).thenAnswer(inv -> {
Conversation c = inv.getArgument(0);
if (c.getId() == null) c.setId(99L);
return c;
});
when(memberRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
}
@Test void createConversation_companyToStudent_withoutMatched_shouldReturn403() {
when(userRepository.findById(USER_A)).thenReturn(Optional.of(user(USER_A, User.Role.COMPANY)));
when(userRepository.findById(USER_B)).thenReturn(Optional.of(user(USER_B, User.Role.STUDENT)));
when(companyRepository.findByOwnerUserId(USER_A))
.thenReturn(Optional.of(Company.builder().id(1L).ownerUserId(USER_A).verified(true).build()));
when(applicationMatchRecordRepository.existsMatchedApplication(1L, USER_B)).thenReturn(false);
when(memberRepository.findByUserId(USER_A)).thenReturn(List.of());
when(userRepository.existsById(USER_B)).thenReturn(true);
CreateConversationRequest req = new CreateConversationRequest();
req.setParticipantUserId(USER_B);
BusinessException ex = assertThrows(BusinessException.class,
() -> messagingService.createOrGetDirectConversation(USER_A, req));
assertEquals(ErrorCode.MATCH_REQUIRED_FOR_CONVERSATION.getCode(), ex.getErrorCodeInt());
verify(conversationRepository, never()).save(any());
}
@Test void createConversation_companyToStudent_withMatched_shouldCreate() {
when(userRepository.findById(USER_A)).thenReturn(Optional.of(user(USER_A, User.Role.COMPANY)));
when(userRepository.findById(USER_B)).thenReturn(Optional.of(user(USER_B, User.Role.STUDENT)));
when(companyRepository.findByOwnerUserId(USER_A))
.thenReturn(Optional.of(Company.builder().id(1L).ownerUserId(USER_A).verified(true).build()));
when(applicationMatchRecordRepository.existsMatchedApplication(1L, USER_B)).thenReturn(true);
stubNewConversationCreation();
when(userRepository.existsById(USER_B)).thenReturn(true);
CreateConversationRequest req = new CreateConversationRequest();
req.setParticipantUserId(USER_B);
var r = messagingService.createOrGetDirectConversation(USER_A, req);
assertNotNull(r.get("id"));
verify(conversationRepository, times(1)).save(any());
}
@Test void createConversation_studentToStudent_shouldKeepOriginalSemantics() {
when(userRepository.findById(USER_A)).thenReturn(Optional.of(user(USER_A, User.Role.STUDENT)));
when(userRepository.findById(USER_B)).thenReturn(Optional.of(user(USER_B, User.Role.STUDENT)));
stubNewConversationCreation();
when(userRepository.existsById(USER_B)).thenReturn(true);
CreateConversationRequest req = new CreateConversationRequest();
req.setParticipantUserId(USER_B);
var r = messagingService.createOrGetDirectConversation(USER_A, req);
assertNotNull(r.get("id"));
// 学生-学生不受匹配守卫限制
verify(conversationRepository, times(1)).save(any());
}
// ── 发送消息 ─────────────────────────────────────────────
@Test void sendMessage_asMember_shouldSucceed() {
when(memberRepository.existsByConversationIdAndUserId(CONV_ID, USER_A)).thenReturn(true);
when(messageRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(conversationRepository.findById(CONV_ID)).thenReturn(Optional.of(
Conversation.builder().id(CONV_ID).createdBy(USER_A).type("DIRECT").build()));
when(conversationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
SendMessageRequest req = new SendMessageRequest();
req.setConversationId(CONV_ID);
req.setContent("Hello");
var r = messagingService.sendMessage(USER_A, req);
assertEquals("Hello", r.get("content"));
}
@Test void sendMessage_asNonMember_shouldReturn403() {
stubConversationExists();
when(memberRepository.existsByConversationIdAndUserId(CONV_ID, USER_C)).thenReturn(false);
SendMessageRequest req = new SendMessageRequest();
req.setConversationId(CONV_ID); req.setContent("Hack");
BusinessException ex = assertThrows(BusinessException.class,
() -> messagingService.sendMessage(USER_C, req));
assertEquals(ErrorCode.FORBIDDEN.getCode(), ex.getErrorCodeInt());
}
@Test void sendMessage_tooLong_shouldReturn400() {
stubConversationExists();
when(memberRepository.existsByConversationIdAndUserId(CONV_ID, USER_A)).thenReturn(true);
SendMessageRequest req = new SendMessageRequest();
req.setConversationId(CONV_ID);
req.setContent("x".repeat(2001));
BusinessException ex = assertThrows(BusinessException.class,
() -> messagingService.sendMessage(USER_A, req));
assertEquals(ErrorCode.BAD_REQUEST.getCode(), ex.getErrorCodeInt());
}
// ── V33 屏蔽拒发:拒绝型安全审计仍需落库(即使外层返回 403) ────
@Test void sendMessage_blocked_shouldLogSecurityAudit() {
stubConversationExists();
when(memberRepository.existsByConversationIdAndUserId(CONV_ID, USER_A)).thenReturn(true);
// 会话里存在另一成员 B,且 A 屏蔽了 B
when(memberRepository.findByConversationId(CONV_ID))
.thenReturn(List.of(ConversationMember.builder().conversationId(CONV_ID).userId(USER_B).build()));
when(blockRelationRepository.existsByBlockerUserIdAndBlockedUserId(USER_A, USER_B)).thenReturn(true);
SendMessageRequest req = new SendMessageRequest();
req.setConversationId(CONV_ID);
req.setContent("blocked");
BusinessException ex = assertThrows(BusinessException.class,
() -> messagingService.sendMessage(USER_A, req));
assertEquals(ErrorCode.FORBIDDEN.getCode(), ex.getErrorCodeInt());
// 即使请求被拒,拒绝型安全审计也必须被记录
verify(securityAuditService).logRejected(eq(USER_A), eq("BLOCKED_SEND_PROHIBITED"),
eq("USER"), eq(USER_B), anyString(), any(AuditContext.class));
}
@Test void createConversation_blocked_shouldLogSecurityAudit() {
CreateConversationRequest req = new CreateConversationRequest();
req.setParticipantUserId(USER_B);
when(userRepository.existsById(USER_B)).thenReturn(true);
when(memberRepository.findByUserId(USER_A)).thenReturn(List.of());
when(blockRelationRepository.existsByBlockerUserIdAndBlockedUserId(USER_A, USER_B)).thenReturn(true);
BusinessException ex = assertThrows(BusinessException.class,
() -> messagingService.createOrGetDirectConversation(USER_A, req));
assertEquals(ErrorCode.FORBIDDEN.getCode(), ex.getErrorCodeInt());
// 即使请求被拒,拒绝型安全审计也必须被记录
verify(securityAuditService).logRejected(eq(USER_A), eq("BLOCKED_SEND_PROHIBITED"),
eq("USER"), eq(USER_B), anyString(), any(AuditContext.class));
}
@Test void sendMessage_unblocked_shouldNotLogSecurityAudit() {
stubConversationExists();
when(memberRepository.existsByConversationIdAndUserId(CONV_ID, USER_A)).thenReturn(true);
when(memberRepository.findByConversationId(CONV_ID))
.thenReturn(List.of(ConversationMember.builder().conversationId(CONV_ID).userId(USER_B).build()));
when(blockRelationRepository.existsByBlockerUserIdAndBlockedUserId(USER_A, USER_B)).thenReturn(false);
when(blockRelationRepository.existsByBlockerUserIdAndBlockedUserId(USER_B, USER_A)).thenReturn(false);
when(messageRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
when(conversationRepository.findById(CONV_ID)).thenReturn(Optional.of(conv(CONV_ID)));
when(conversationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
SendMessageRequest req = new SendMessageRequest();
req.setConversationId(CONV_ID);
req.setContent("ok");
assertDoesNotThrow(() -> messagingService.sendMessage(USER_A, req));
verify(securityAuditService, never()).logRejected(any(), any(), any(), any(), any(), any());
}
// ── 消息列表 ─────────────────────────────────────────────
@Test void listMessages_asMember_shouldSucceed() {
stubConversationExists();
when(memberRepository.existsByConversationIdAndUserId(CONV_ID, USER_A)).thenReturn(true);
Message msg = Message.builder().id(1L).conversationId(CONV_ID).senderUserId(USER_A).content("Hi").build();
when(messageRepository.findByConversationIdOrderByCreatedAtDesc(eq(CONV_ID), any()))
.thenReturn(new PageImpl<>(List.of(msg)));
when(readRepository.findByMessageIdInAndReaderUserId(List.of(1L), USER_A)).thenReturn(List.of());
var r = messagingService.listMessages(CONV_ID, USER_A, 0, 20);
assertEquals(1, r.getTotal());
assertEquals(false, r.getItems().get(0).getIsRead());
}
@Test void listMessages_asNonMember_shouldReturn403() {
stubConversationExists();
when(memberRepository.existsByConversationIdAndUserId(CONV_ID, USER_C)).thenReturn(false);
BusinessException ex = assertThrows(BusinessException.class,
() -> messagingService.listMessages(CONV_ID, USER_C, 0, 20));
assertEquals(ErrorCode.FORBIDDEN.getCode(), ex.getErrorCodeInt());
}
// ── 标记已读 ─────────────────────────────────────────────
@Test void markRead_shouldCreateReadRecords() {
stubConversationExists();
when(memberRepository.existsByConversationIdAndUserId(CONV_ID, USER_A)).thenReturn(true);
Message msg = Message.builder().id(1L).conversationId(CONV_ID).senderUserId(USER_B).content("Hi").build();
when(messageRepository.findByConversationIdOrderByCreatedAtDesc(eq(CONV_ID), any()))
.thenReturn(new PageImpl<>(List.of(msg)));
when(readRepository.findByMessageIdInAndReaderUserId(List.of(1L), USER_A)).thenReturn(List.of());
when(readRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
assertDoesNotThrow(() -> messagingService.markRead(CONV_ID, USER_A));
verify(readRepository, times(1)).save(any());
}
@Test void markRead_asNonMember_shouldReturn403() {
stubConversationExists();
when(memberRepository.existsByConversationIdAndUserId(CONV_ID, USER_C)).thenReturn(false);
BusinessException ex = assertThrows(BusinessException.class,
() -> messagingService.markRead(CONV_ID, USER_C));
assertEquals(ErrorCode.FORBIDDEN.getCode(), ex.getErrorCodeInt());
}
// ── 会话存在性检查(不存在 → CONVERSATION_NOT_FOUND) ────────
@Test void getConversation_conversationNotFound_shouldReturn404() {
when(conversationRepository.findById(999L)).thenReturn(Optional.empty());
BusinessException ex = assertThrows(BusinessException.class,
() -> messagingService.getConversation(999L, USER_A));
assertEquals(ErrorCode.CONVERSATION_NOT_FOUND.getCode(), ex.getErrorCodeInt());
}
@Test void getConversation_asMember_shouldSucceed() {
stubConversationExists();
when(memberRepository.existsByConversationIdAndUserId(CONV_ID, USER_A)).thenReturn(true);
when(messageRepository.findByConversationIdOrderByCreatedAtDesc(eq(CONV_ID), any()))
.thenReturn(new PageImpl<>(List.of()));
var v = messagingService.getConversation(CONV_ID, USER_A);
assertEquals(CONV_ID, v.get("id"));
assertEquals(0L, v.get("unreadCount"));
}
@Test void getConversation_asNonMember_shouldReturn403() {
stubConversationExists();
when(memberRepository.existsByConversationIdAndUserId(CONV_ID, USER_C)).thenReturn(false);
BusinessException ex = assertThrows(BusinessException.class,
() -> messagingService.getConversation(CONV_ID, USER_C));
assertEquals(ErrorCode.FORBIDDEN.getCode(), ex.getErrorCodeInt());
}
@Test void listMessages_conversationNotFound_shouldReturn404() {
when(conversationRepository.findById(999L)).thenReturn(Optional.empty());
BusinessException ex = assertThrows(BusinessException.class,
() -> messagingService.listMessages(999L, USER_A, 0, 20));
assertEquals(ErrorCode.CONVERSATION_NOT_FOUND.getCode(), ex.getErrorCodeInt());
}
@Test void sendMessage_conversationNotFound_shouldReturn404() {
when(conversationRepository.findById(999L)).thenReturn(Optional.empty());
SendMessageRequest req = new SendMessageRequest();
req.setConversationId(999L); req.setContent("Hi");
BusinessException ex = assertThrows(BusinessException.class,
() -> messagingService.sendMessage(USER_A, req));
assertEquals(ErrorCode.CONVERSATION_NOT_FOUND.getCode(), ex.getErrorCodeInt());
}
@Test void markRead_conversationNotFound_shouldReturn404() {
when(conversationRepository.findById(999L)).thenReturn(Optional.empty());
BusinessException ex = assertThrows(BusinessException.class,
() -> messagingService.markRead(999L, USER_A));
assertEquals(ErrorCode.CONVERSATION_NOT_FOUND.getCode(), ex.getErrorCodeInt());
}
}