test(send): integration tests for scene-based routing

This commit is contained in:
2026-07-14 18:08:05 +08:00
parent 128df39113
commit e1c7c582ee

View File

@@ -0,0 +1,327 @@
package com.sino.mci.send.service;
import com.sino.mci.channel.common.model.AccountType;
import com.sino.mci.channel.common.model.ChannelType;
import com.sino.mci.channel.entity.ChannelAccount;
import com.sino.mci.channel.entity.ChannelAccountConversation;
import com.sino.mci.channel.entity.ChannelSubject;
import com.sino.mci.channel.entity.Conversation;
import com.sino.mci.channel.repository.ChannelAccountConversationMapper;
import com.sino.mci.channel.repository.ChannelAccountMapper;
import com.sino.mci.channel.repository.ChannelSubjectMapper;
import com.sino.mci.channel.repository.ConversationMapper;
import com.sino.mci.crm.entity.ChannelIdentity;
import com.sino.mci.crm.entity.Person;
import com.sino.mci.crm.repository.ChannelIdentityMapper;
import com.sino.mci.crm.repository.PersonMapper;
import com.sino.mci.send.entity.SendRecord;
import com.sino.mci.send.repository.SendRecordMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.MediaType;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
import static com.sino.mci.test.AuthTestHelper.createTenantAndLogin;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* 基于会话场景与目标类型的发送路由集成测试。
*
* <p>覆盖自动路由策略:
* <ul>
* <li>内部群conversation_scene = 1→ WECOM / WECOM_BOT</li>
* <li>外部群conversation_scene = 2→ WECHAT_PERSONAL</li>
* <li>内部人员 → WECOM_AGENT</li>
* <li>外部人员 → WECOM_AGENT</li>
* </ul>
*/
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@TestPropertySource(properties = {
"spring.rabbitmq.listener.simple.auto-startup=false"
})
class SendRoutingIntegrationTest {
private static final String CHANNEL_TYPE_WECOM = ChannelType.WECOM.name().toLowerCase();
private static final String CHANNEL_TYPE_WECHAT_PERSONAL = ChannelType.WECHAT_PERSONAL.name().toLowerCase();
private static final int CONVERSATION_SCENE_INTERNAL = 1;
private static final int CONVERSATION_SCENE_EXTERNAL = 2;
private static final int PERSON_TYPE_INTERNAL = 1;
private static final int PERSON_TYPE_EXTERNAL = 3;
private static final int STATUS_ENABLED = 1;
private static final int BINDING_TYPE_SEND_PRIMARY = 1;
@Autowired
private MockMvc mockMvc;
@Autowired
private ChannelSubjectMapper subjectMapper;
@Autowired
private ChannelAccountMapper accountMapper;
@Autowired
private ConversationMapper conversationMapper;
@Autowired
private ChannelAccountConversationMapper accountConversationMapper;
@Autowired
private PersonMapper personMapper;
@Autowired
private ChannelIdentityMapper channelIdentityMapper;
@Autowired
private SendRecordMapper sendRecordMapper;
@Test
void shouldRouteInternalGroupToWeComBot() throws Exception {
String tenantCode = "routing-int-group-" + UUID.randomUUID().toString().substring(0, 8);
String authToken = createTenantAndLogin(mockMvc, tenantCode, "内部群路由测试");
ChannelSubject subject = createWeComSubject(tenantCode);
ChannelAccount botAccount = createWeComBotAccount(tenantCode, subject.getId());
Conversation conversation = createConversation(tenantCode, subject.getId(),
CONVERSATION_SCENE_INTERNAL, "内部群");
bindAccountToConversation(botAccount.getId(), conversation.getId());
String taskId = sendTestMessage(authToken, tenantCode,
buildConversationPayload(conversation.getId()));
List<SendRecord> records = sendRecordMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SendRecord>()
.eq(SendRecord::getTenantCode, tenantCode)
.eq(SendRecord::getTaskId, taskId));
assertThat(records).hasSize(1);
SendRecord record = records.get(0);
assertThat(record.getChannelType()).isEqualTo(CHANNEL_TYPE_WECOM);
assertThat(record.getAccountId()).isEqualTo(botAccount.getId());
assertThat(record.getConversationId()).isEqualTo(conversation.getId());
}
@Test
void shouldRouteExternalGroupToWeChatPersonal() throws Exception {
String tenantCode = "routing-ext-group-" + UUID.randomUUID().toString().substring(0, 8);
String authToken = createTenantAndLogin(mockMvc, tenantCode, "外部群路由测试");
ChannelSubject subject = createWeComSubject(tenantCode);
ChannelAccount pywechatAccount = createWeChatPersonalAccount(tenantCode, subject.getId());
Conversation conversation = createConversation(tenantCode, subject.getId(),
CONVERSATION_SCENE_EXTERNAL, "外部群");
bindAccountToConversation(pywechatAccount.getId(), conversation.getId());
String taskId = sendTestMessage(authToken, tenantCode,
buildConversationPayload(conversation.getId()));
List<SendRecord> records = sendRecordMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SendRecord>()
.eq(SendRecord::getTenantCode, tenantCode)
.eq(SendRecord::getTaskId, taskId));
assertThat(records).hasSize(1);
SendRecord record = records.get(0);
assertThat(record.getChannelType()).isEqualTo(CHANNEL_TYPE_WECHAT_PERSONAL);
assertThat(record.getAccountId()).isEqualTo(pywechatAccount.getId());
assertThat(record.getConversationId()).isEqualTo(conversation.getId());
}
@Test
void shouldRouteInternalPersonToWeComAgent() throws Exception {
String tenantCode = "routing-int-person-" + UUID.randomUUID().toString().substring(0, 8);
String authToken = createTenantAndLogin(mockMvc, tenantCode, "内部人员路由测试");
ChannelSubject subject = createWeComSubject(tenantCode);
ChannelAccount agentAccount = createWeComAgentAccount(tenantCode, subject.getId());
Person person = createPerson(tenantCode, "int-user", "内部人员", PERSON_TYPE_INTERNAL);
createChannelIdentity(tenantCode, person.getId(), subject.getId(), "int-user");
String taskId = sendTestMessage(authToken, tenantCode,
buildPersonPayload(person.getId()));
List<SendRecord> records = sendRecordMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SendRecord>()
.eq(SendRecord::getTenantCode, tenantCode)
.eq(SendRecord::getTaskId, taskId));
assertThat(records).hasSize(1);
SendRecord record = records.get(0);
assertThat(record.getChannelType()).isEqualTo(CHANNEL_TYPE_WECOM);
assertThat(record.getAccountId()).isEqualTo(agentAccount.getId());
assertThat(record.getPersonId()).isEqualTo(person.getId());
}
@Test
void shouldRouteExternalPersonToWeComAgent() throws Exception {
String tenantCode = "routing-ext-person-" + UUID.randomUUID().toString().substring(0, 8);
String authToken = createTenantAndLogin(mockMvc, tenantCode, "外部人员路由测试");
ChannelSubject subject = createWeComSubject(tenantCode);
ChannelAccount agentAccount = createWeComAgentAccount(tenantCode, subject.getId());
Person person = createPerson(tenantCode, "ext-user", "外部人员", PERSON_TYPE_EXTERNAL);
createChannelIdentity(tenantCode, person.getId(), subject.getId(), "ext-user");
String taskId = sendTestMessage(authToken, tenantCode,
buildPersonPayload(person.getId()));
List<SendRecord> records = sendRecordMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<SendRecord>()
.eq(SendRecord::getTenantCode, tenantCode)
.eq(SendRecord::getTaskId, taskId));
assertThat(records).hasSize(1);
SendRecord record = records.get(0);
assertThat(record.getChannelType()).isEqualTo(CHANNEL_TYPE_WECOM);
assertThat(record.getAccountId()).isEqualTo(agentAccount.getId());
assertThat(record.getPersonId()).isEqualTo(person.getId());
}
private String sendTestMessage(String authToken, String tenantCode, String payload) throws Exception {
String response = mockMvc.perform(post("/msg-platform/api/v1/send/test")
.contextPath("/msg-platform")
.header("Authorization", authToken)
.header("X-Tenant-Code", tenantCode)
.contentType(MediaType.APPLICATION_JSON)
.content(payload))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(0))
.andExpect(jsonPath("$.data.totalCount").value(1))
.andExpect(jsonPath("$.data.status").value("simulated"))
.andReturn().getResponse().getContentAsString();
return com.jayway.jsonpath.JsonPath.read(response, "$.data.taskId");
}
private String buildConversationPayload(Long conversationId) {
return String.format(
"{\"targetType\":\"conversation\",\"targetValue\":{\"conversation_id\":%d},\"contentType\":\"text\",\"content\":{\"text\":\"hello\"},\"channelStrategy\":{\"channel_type\":\"wecom\",\"strategy\":\"primary\"}}",
conversationId);
}
private String buildPersonPayload(Long personId) {
return String.format(
"{\"targetType\":\"person\",\"targetValue\":{\"person_id\":%d},\"contentType\":\"text\",\"content\":{\"text\":\"hello\"},\"channelStrategy\":{\"channel_type\":\"wecom\"}}",
personId);
}
private ChannelSubject createWeComSubject(String tenantCode) {
ChannelSubject subject = new ChannelSubject();
subject.setTenantCode(tenantCode);
subject.setChannelType(CHANNEL_TYPE_WECOM);
subject.setSubjectCode("sub-" + UUID.randomUUID().toString().substring(0, 8));
subject.setSubjectName("测试主体");
subject.setCorpId("corp-" + UUID.randomUUID().toString().substring(0, 8));
subject.setAgentId("1000002");
subject.setAgentSecret("secret-" + UUID.randomUUID());
subject.setStatus(STATUS_ENABLED);
subjectMapper.insert(subject);
return subject;
}
private ChannelAccount createWeComBotAccount(String tenantCode, Long subjectId) {
ChannelAccount account = new ChannelAccount();
account.setTenantCode(tenantCode);
account.setAccountType(AccountType.WECOM_BOT.getCode());
account.setChannelType(CHANNEL_TYPE_WECOM);
account.setSubjectId(subjectId);
account.setAccountId("bot-" + UUID.randomUUID().toString().substring(0, 8));
account.setAccountName("企微群机器人");
account.setStatus(STATUS_ENABLED);
accountMapper.insert(account);
return account;
}
private ChannelAccount createWeComAgentAccount(String tenantCode, Long subjectId) {
ChannelAccount account = new ChannelAccount();
account.setTenantCode(tenantCode);
account.setAccountType(AccountType.WECOM_AGENT.getCode());
account.setChannelType(CHANNEL_TYPE_WECOM);
account.setSubjectId(subjectId);
account.setAccountId("agent-" + UUID.randomUUID().toString().substring(0, 8));
account.setAccountName("企微应用");
account.setStatus(STATUS_ENABLED);
accountMapper.insert(account);
return account;
}
private ChannelAccount createWeChatPersonalAccount(String tenantCode, Long subjectId) {
ChannelAccount account = new ChannelAccount();
account.setTenantCode(tenantCode);
account.setAccountType(AccountType.WECHAT_PERSONAL.getCode());
account.setChannelType(CHANNEL_TYPE_WECHAT_PERSONAL);
account.setSubjectId(subjectId);
account.setAccountId("wxid-" + UUID.randomUUID().toString().substring(0, 8));
account.setAccountName("pywechat 个人号");
account.setStatus(STATUS_ENABLED);
account.setOnlineStatus(STATUS_ENABLED);
account.setRiskLevel(1);
account.setLastHeartbeat(LocalDateTime.now());
accountMapper.insert(account);
return account;
}
private Conversation createConversation(String tenantCode, Long subjectId,
Integer conversationScene, String conversationName) {
Conversation conversation = new Conversation();
conversation.setTenantCode(tenantCode);
conversation.setConversationType(2);
conversation.setChannelType(CHANNEL_TYPE_WECOM);
conversation.setChannelConversationId("chat-" + UUID.randomUUID().toString().substring(0, 8));
conversation.setSubjectId(subjectId);
conversation.setConversationName(conversationName);
conversation.setConversationScene(conversationScene);
conversation.setCreateSource(1);
conversation.setStatus(STATUS_ENABLED);
conversationMapper.insert(conversation);
return conversation;
}
private void bindAccountToConversation(Long accountId, Long conversationId) {
ChannelAccountConversation binding = new ChannelAccountConversation();
binding.setAccountId(accountId);
binding.setConversationId(conversationId);
binding.setBindingType(BINDING_TYPE_SEND_PRIMARY);
binding.setSortOrder(1);
binding.setStatus(STATUS_ENABLED);
accountConversationMapper.insert(binding);
}
private Person createPerson(String tenantCode, String personCode, String personName, Integer personType) {
Person person = new Person();
person.setTenantCode(tenantCode);
person.setPersonCode(personCode + "-" + UUID.randomUUID().toString().substring(0, 8));
person.setPersonName(personName);
person.setPhone("13800138000");
person.setPersonType(personType);
person.setStatus(STATUS_ENABLED);
personMapper.insert(person);
return person;
}
private void createChannelIdentity(String tenantCode, Long personId, Long subjectId, String channelUserId) {
ChannelIdentity identity = new ChannelIdentity();
identity.setTenantCode(tenantCode);
identity.setPersonId(personId);
identity.setChannelType(CHANNEL_TYPE_WECOM);
identity.setChannelUserId(channelUserId + "-" + UUID.randomUUID().toString().substring(0, 8));
identity.setChannelUserName("测试用户");
identity.setSubjectId(subjectId);
identity.setStatus(STATUS_ENABLED);
channelIdentityMapper.insert(identity);
}
}