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
package cn.iocoder.yudao.module.iot.service.device.property;
 
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
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.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty;
import cn.iocoder.yudao.module.iot.dal.redis.device.DevicePropertyRedisDAO;
import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
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.HashMap;
import java.util.Map;
 
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
 
/**
 * {@link IotDevicePropertyServiceImpl} 的单元测试
 *
 * @author 芋道源码
 */
public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
 
    @InjectMocks
    private IotDevicePropertyServiceImpl service;
 
    @Mock
    private IotThingModelService thingModelService;
    @Mock
    private IotDevicePropertyMapper devicePropertyMapper;
    @Mock
    private DevicePropertyRedisDAO deviceDataRedisDAO;
 
    @Test
    public void testSaveDeviceProperty_identifierCaseInsensitive() {
        // 准备参数:物模型 identifier 是 "LightStatus",设备上报的 key 是 "LIGHTSTATUS"(全大写)
        IotDeviceDO device = buildDevice();
        IotThingModelDO thingModel = buildThingModel("LightStatus", IotDataSpecsDataTypeEnum.INT.getDataType());
        Map<String, Object> params = new HashMap<>();
        params.put("LIGHTSTATUS", 100);
        IotDeviceMessage message = buildMessage(params);
 
        // mock 行为
        when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
                .thenReturn(singletonList(thingModel));
 
        // 调用
        service.saveDeviceProperty(device, message);
 
        // 断言:properties 落库 / 入缓存时 key 应为物模型 identifier "LightStatus",而不是上报的 "LIGHTSTATUS"
        Map<String, Object> dbProperties = captureMapperInsertProperties();
        assertTrue(dbProperties.containsKey("LightStatus"));
        assertFalse(dbProperties.containsKey("LIGHTSTATUS"));
        assertEquals(100, dbProperties.get("LightStatus"));
 
        Map<String, IotDevicePropertyDO> redisProperties = captureRedisPutAllProperties(device.getId());
        assertTrue(redisProperties.containsKey("LightStatus"));
        assertFalse(redisProperties.containsKey("LIGHTSTATUS"));
    }
 
    @Test
    public void testSaveDeviceProperty_identifierNotInThingModel() {
        // 准备参数:上报的 key 在物模型里完全不存在(连忽略大小写都匹配不到)
        IotDeviceDO device = buildDevice();
        IotThingModelDO thingModel = buildThingModel("LightStatus", IotDataSpecsDataTypeEnum.INT.getDataType());
        Map<String, Object> params = new HashMap<>();
        params.put("UnknownProperty", 1);
        IotDeviceMessage message = buildMessage(params);
 
        // mock 行为
        when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
                .thenReturn(singletonList(thingModel));
 
        // 调用
        service.saveDeviceProperty(device, message);
 
        // 断言:没有合法属性,不会写入 TDengine 与 Redis
        verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong());
        verify(deviceDataRedisDAO, never()).putAll(anyLong(), any());
    }
 
    @Test
    public void testSaveDeviceProperty_boolFromBooleanTrue() {
        // 准备参数:物模型为 BOOL,设备上报原生 boolean true
        assertBoolValueConvertedToByte(true, (byte) 1);
    }
 
    @Test
    public void testSaveDeviceProperty_boolFromBooleanFalse() {
        // 准备参数:物模型为 BOOL,设备上报原生 boolean false
        assertBoolValueConvertedToByte(false, (byte) 0);
    }
 
    @Test
    public void testSaveDeviceProperty_boolFromStringTrue() {
        // 准备参数:物模型为 BOOL,设备上报字符串 "true"
        assertBoolValueConvertedToByte("true", (byte) 1);
    }
 
    @Test
    public void testSaveDeviceProperty_boolFromStringFalse() {
        // 准备参数:物模型为 BOOL,设备上报字符串 "false"
        assertBoolValueConvertedToByte("false", (byte) 0);
    }
 
    @Test
    public void testSaveDeviceProperty_boolFromNumberOne() {
        // 准备参数:物模型为 BOOL,设备上报数字 1
        assertBoolValueConvertedToByte(1, (byte) 1);
    }
 
    @Test
    public void testSaveDeviceProperty_boolFromNumberZero() {
        // 准备参数:物模型为 BOOL,设备上报数字 0
        assertBoolValueConvertedToByte(0, (byte) 0);
    }
 
    /**
     * 校验 BOOL 类型属性上报后,最终落到 properties Map 的值类型与数值
     */
    private void assertBoolValueConvertedToByte(Object reportedValue, byte expected) {
        // 准备参数
        IotDeviceDO device = buildDevice();
        IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType());
        Map<String, Object> params = new HashMap<>();
        params.put("PowerSwitch", reportedValue);
        IotDeviceMessage message = buildMessage(params);
 
        // mock 行为
        when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
                .thenReturn(singletonList(thingModel));
 
        // 调用:不能抛异常
        assertDoesNotThrow(() -> service.saveDeviceProperty(device, message));
 
        // 断言:写入的 value 是 byte 类型,且值匹配
        Map<String, Object> dbProperties = captureMapperInsertProperties();
        Object actual = dbProperties.get("PowerSwitch");
        assertTrue(actual instanceof Byte, "BOOL 属性应被转为 Byte 类型,实际为 " + (actual == null ? "null" : actual.getClass()));
        assertEquals(expected, actual);
    }
 
    // ========== 辅助方法 ==========
 
    /**
     * 构造一个最简 IotDeviceDO,只设置测试需要的 id 与 productId
     */
    private IotDeviceDO buildDevice() {
        return IotDeviceDO.builder().id(1L).productId(2L).build();
    }
 
    /**
     * 构造物模型;只填 saveDeviceProperty 链路用到的 identifier + property.dataType
     */
    private IotThingModelDO buildThingModel(String identifier, String dataType) {
        ThingModelProperty property = new ThingModelProperty();
        property.setIdentifier(identifier);
        property.setDataType(dataType);
        return IotThingModelDO.builder().identifier(identifier).property(property).build();
    }
 
    /**
     * 构造一条属性上报消息
     */
    private IotDeviceMessage buildMessage(Map<String, Object> params) {
        IotDeviceMessage message = new IotDeviceMessage();
        message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
        message.setParams(params);
        message.setReportTime(LocalDateTime.now());
        return message;
    }
 
    /**
     * 抓取 mapper.insert 的 properties 入参
     */
    @SuppressWarnings("unchecked")
    private Map<String, Object> captureMapperInsertProperties() {
        ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
        verify(devicePropertyMapper).insert(any(IotDeviceDO.class), captor.capture(), anyLong(), anyLong());
        return captor.getValue();
    }
 
    /**
     * 抓取 redisDAO.putAll 的 properties 入参
     */
    @SuppressWarnings("unchecked")
    private Map<String, IotDevicePropertyDO> captureRedisPutAllProperties(Long deviceId) {
        ArgumentCaptor<Map<String, IotDevicePropertyDO>> captor = ArgumentCaptor.forClass(Map.class);
        verify(deviceDataRedisDAO).putAll(eq(deviceId), captor.capture());
        return captor.getValue();
    }
 
}