2026-06-24 f4bd1f3c89d906131495a0aca5aaf82966378510
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
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<IotDeviceMessageDO> 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<IotDeviceMessageDO> page = new Page<>(1, 10, 1L);
        page.setRecords(Collections.singletonList(record));
        when(deviceMessageMapper.selectPage(any(), eq(reqVO))).thenReturn(page);
 
        // 调用
        PageResult<IotDeviceMessageDO> 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<IotDeviceMessageDO> 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<IotDeviceMessageDO> result = service.getDeviceMessageListByRequestIdsAndReply(
                1L, Collections.emptyList(), true);
 
        assertTrue(result.isEmpty());
        verify(deviceMessageMapper, never()).selectListByRequestIdsAndReply(any(), any(), any());
    }
 
    @Test
    public void testGetDeviceMessageListByRequestIdsAndReply_normal_delegateToMapper() {
        // 准备
        List<String> requestIds = Collections.singletonList("req-1");
        IotDeviceMessageDO record = new IotDeviceMessageDO().setId("msg-1");
        when(deviceMessageMapper.selectListByRequestIdsAndReply(1L, requestIds, true))
                .thenReturn(Collections.singletonList(record));
 
        // 调用
        List<IotDeviceMessageDO> 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();
    }
 
}