package cn.iocoder.yudao.module.iot.service.device.message; import cn.hutool.core.date.LocalDateTimeUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO; import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMessageMapper; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskRecordService; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import java.time.LocalDateTime; import java.util.Collections; import java.util.HashMap; import java.util.List; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.*; /** * {@link IotDeviceMessageServiceImpl} 的单元测试 * * 注:TDengine 数据源没有 embedded 替代,mapper 与依赖 service 走 mock; * handleUpstreamDeviceMessage 与 sendDeviceMessage 下行成功路径依赖 SpringUtil.getBean 的自调用 * createDeviceLogAsync,更适合放到集成测试,本类不展开。 * * @author 芋道源码 */ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest { @InjectMocks private IotDeviceMessageServiceImpl service; @Mock private IotDeviceService deviceService; @Mock private IotDevicePropertyService devicePropertyService; @Mock private IotOtaTaskRecordService otaTaskRecordService; @Mock private IotDeviceMessageMapper deviceMessageMapper; @Mock private IotDeviceMessageProducer deviceMessageProducer; // ========== defineDeviceMessageStable ========== @Test public void testDefineDeviceMessageStable_whenTableExists_skipCreate() { // 准备:showSTable 返回非空 → 表已存在 when(deviceMessageMapper.showSTable()).thenReturn("device_message"); // 调用 service.defineDeviceMessageStable(); // 断言:跳过 createSTable verify(deviceMessageMapper, never()).createSTable(); } @Test public void testDefineDeviceMessageStable_whenTableMissing_create() { // 准备:showSTable 返回空 → 表不存在 when(deviceMessageMapper.showSTable()).thenReturn(""); // 调用 service.defineDeviceMessageStable(); // 断言:触发 createSTable verify(deviceMessageMapper, times(1)).createSTable(); } // ========== createDeviceLogAsync ========== @Test public void testCreateDeviceLogAsync_tsFallback_whenNull() { // 准备:构造一条 ts 为 null 的消息 IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); long before = System.currentTimeMillis(); // 调用 service.createDeviceLogAsync(message); long after = System.currentTimeMillis(); // 断言:mapper.insert 接收到的 messageDO 已被填上 ts,值在 [before, after] 区间 ArgumentCaptor captor = ArgumentCaptor.forClass(IotDeviceMessageDO.class); verify(deviceMessageMapper).insert(captor.capture()); Long actualTs = captor.getValue().getTs(); assertNotNull(actualTs, "ts 不应为空"); assertTrue(actualTs >= before && actualTs <= after, "ts 应在调用前后区间内; 实际 = " + actualTs); } @Test public void testCreateDeviceLogAsync_swallowMapperException() { // 准备:mapper.insert 抛异常,验证 @Async 方法内部 try/catch 兜底,不向上抛 IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); doThrow(new RuntimeException("DB unavailable")).when(deviceMessageMapper).insert(any()); // 调用 & 断言 assertDoesNotThrow(() -> service.createDeviceLogAsync(message)); verify(deviceMessageMapper).insert(any(IotDeviceMessageDO.class)); } // ========== sendDeviceMessage ========== @Test public void testSendDeviceMessage_upstream_publishToProducer() { // 准备:上行消息(PROPERTY_POST) IotDeviceDO device = buildDevice(); IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); // 调用 IotDeviceMessage result = service.sendDeviceMessage(message, device); // 断言:走 producer.sendDeviceMessage,不进入下行链路 assertSame(message, result); verify(deviceMessageProducer, times(1)).sendDeviceMessage(message); verify(deviceMessageProducer, never()).sendDeviceMessageToGateway(any(), any()); verify(devicePropertyService, never()).getDeviceServerId(any()); } @Test public void testSendDeviceMessage_downstream_serverIdMissing_throwException() { // 准备:下行消息(SERVICE_INVOKE);devicePropertyService 也查不到 serverId IotDeviceDO device = buildDevice(); IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod()); when(devicePropertyService.getDeviceServerId(device.getId())).thenReturn(null); // 调用 & 断言:抛 DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL ServiceException ex = assertThrows(ServiceException.class, () -> service.sendDeviceMessage(message, device)); assertEquals(1_050_003_007, ex.getCode().intValue()); verify(deviceMessageProducer, never()).sendDeviceMessageToGateway(any(), any()); } // ========== getDeviceMessagePage ========== @Test public void testGetDeviceMessagePage_normal() { // 准备 IotDeviceMessagePageReqVO reqVO = new IotDeviceMessagePageReqVO(); reqVO.setPageNo(1); reqVO.setPageSize(10); IotDeviceMessageDO record = new IotDeviceMessageDO().setId("msg-1"); Page page = new Page<>(1, 10, 1L); page.setRecords(Collections.singletonList(record)); when(deviceMessageMapper.selectPage(any(), eq(reqVO))).thenReturn(page); // 调用 PageResult result = service.getDeviceMessagePage(reqVO); // 断言 assertEquals(1L, result.getTotal()); assertEquals(1, result.getList().size()); assertEquals("msg-1", result.getList().get(0).getId()); } @Test public void testGetDeviceMessagePage_whenTableMissing_returnEmpty() { // 准备:mapper 抛 "Table does not exist" → 视为表未创建,返回空结果 IotDeviceMessagePageReqVO reqVO = new IotDeviceMessagePageReqVO(); reqVO.setPageNo(1); reqVO.setPageSize(10); when(deviceMessageMapper.selectPage(any(), any())) .thenThrow(new RuntimeException("Table does not exist")); // 调用 PageResult result = service.getDeviceMessagePage(reqVO); // 断言 assertEquals(0L, result.getTotal()); assertTrue(result.getList().isEmpty()); } @Test public void testGetDeviceMessagePage_otherException_rethrow() { // 准备:mapper 抛非「表不存在」的异常 → 应向上抛 IotDeviceMessagePageReqVO reqVO = new IotDeviceMessagePageReqVO(); reqVO.setPageNo(1); reqVO.setPageSize(10); when(deviceMessageMapper.selectPage(any(), any())) .thenThrow(new RuntimeException("Connection refused")); // 调用 & 断言 assertThrows(RuntimeException.class, () -> service.getDeviceMessagePage(reqVO)); } // ========== getDeviceMessageListByRequestIdsAndReply ========== @Test public void testGetDeviceMessageListByRequestIdsAndReply_emptyIds_returnEmpty() { // 调用 & 断言:requestIds 为空直接返回空列表,不查 DB List result = service.getDeviceMessageListByRequestIdsAndReply( 1L, Collections.emptyList(), true); assertTrue(result.isEmpty()); verify(deviceMessageMapper, never()).selectListByRequestIdsAndReply(any(), any(), any()); } @Test public void testGetDeviceMessageListByRequestIdsAndReply_normal_delegateToMapper() { // 准备 List requestIds = Collections.singletonList("req-1"); IotDeviceMessageDO record = new IotDeviceMessageDO().setId("msg-1"); when(deviceMessageMapper.selectListByRequestIdsAndReply(1L, requestIds, true)) .thenReturn(Collections.singletonList(record)); // 调用 List result = service.getDeviceMessageListByRequestIdsAndReply( 1L, requestIds, true); // 断言 assertEquals(1, result.size()); assertEquals("msg-1", result.get(0).getId()); } // ========== getDeviceMessageCount ========== @Test public void testGetDeviceMessageCount_whenCreateTimeNull_passNullToMapper() { // 准备 when(deviceMessageMapper.selectCountByCreateTime(isNull())).thenReturn(123L); // 调用 Long count = service.getDeviceMessageCount(null); // 断言 assertEquals(123L, count); verify(deviceMessageMapper).selectCountByCreateTime(isNull()); } @Test public void testGetDeviceMessageCount_withCreateTime_passEpochMilli() { // 准备:service 内部用 LocalDateTimeUtil.toEpochMilli 做转换,断言时也用同函数得到期望值 LocalDateTime createTime = LocalDateTime.of(2026, 1, 1, 0, 0); long expectedMs = LocalDateTimeUtil.toEpochMilli(createTime); when(deviceMessageMapper.selectCountByCreateTime(expectedMs)).thenReturn(456L); // 调用 Long count = service.getDeviceMessageCount(createTime); // 断言 assertEquals(456L, count); } // ========== 辅助方法 ========== /** 构造一条最简消息(指定 method 决定上下行分支) */ private IotDeviceMessage buildMessage(String method) { IotDeviceMessage message = new IotDeviceMessage(); message.setId("msg-1"); message.setDeviceId(2L); message.setMethod(method); message.setParams(new HashMap<>()); return message; } /** 构造最简设备 */ private IotDeviceDO buildDevice() { return IotDeviceDO.builder().id(2L).build(); } }