Files
xiaomihome/miot/miot_client_device_test.go
T
4566704 3a44cb9e6a feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写)
- 支持云端 API 调用与局域网设备发现(mDNS)
- 集成国际化(i18n)多语言支持
- 添加 MIoT 设备规约解析器(spec_parser)
- 包含单元测试与使用示例
2026-06-28 22:05:49 +08:00

292 lines
7.9 KiB
Go

// Package miot provides white-box tests for miot_client_device.go device management.
package miot
import (
"testing"
"time"
)
// ============================================================================
// GetDeviceInfo
// ============================================================================
func TestGetDeviceInfo_Exists(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
c.deviceListMu.Lock()
c.deviceListCache["did1"] = map[string]interface{}{
"did": "did1",
"name": "Test Device",
}
c.deviceListMu.Unlock()
info := c.GetDeviceInfo("did1")
if info == nil {
t.Fatal("GetDeviceInfo should return non-nil for existing device")
}
if info["name"] != "Test Device" {
t.Errorf("name = %v, want 'Test Device'", info["name"])
}
}
func TestGetDeviceInfo_NotExists(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
info := c.GetDeviceInfo("nonexistent")
if info != nil {
t.Errorf("GetDeviceInfo should return nil for non-existent, got %v", info)
}
}
// ============================================================================
// GetDeviceInfos
// ============================================================================
func TestGetDeviceInfos(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
c.deviceListMu.Lock()
c.deviceListCache["did1"] = map[string]interface{}{"name": "dev1"}
c.deviceListCache["did2"] = map[string]interface{}{"name": "dev2"}
c.deviceListMu.Unlock()
infos := c.GetDeviceInfos()
if len(infos) != 2 {
t.Errorf("GetDeviceInfos len = %d, want 2", len(infos))
}
if infos["did1"]["name"] != "dev1" {
t.Errorf("did1 name = %v", infos["did1"]["name"])
}
// Should be a copy
infos["new"] = map[string]interface{}{}
if _, ok := c.GetDeviceInfos()["new"]; ok {
t.Error("GetDeviceInfos should return independent copies")
}
}
// ============================================================================
// RefreshDevices — no HTTP client
// ============================================================================
func TestRefreshDevices_NoHTTP(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
err := c.RefreshDevices()
if err == nil {
t.Error("RefreshDevices without HTTP client should error")
}
}
// ============================================================================
// Helper functions
// ============================================================================
func TestGetConnectType(t *testing.T) {
tests := []struct {
info map[string]interface{}
want int
}{
{map[string]interface{}{"connect_type": float64(23)}, 23},
{map[string]interface{}{"connect_type": float64(8)}, 8},
{map[string]interface{}{}, 0},
{nil, 0},
}
for _, tt := range tests {
got := getConnectType(tt.info)
if got != tt.want {
t.Errorf("getConnectType(%v) = %d, want %d", tt.info, got, tt.want)
}
}
}
func TestGetOnline(t *testing.T) {
tests := []struct {
info map[string]interface{}
want bool
}{
{map[string]interface{}{"online": true}, true},
{map[string]interface{}{"online": false}, false},
{map[string]interface{}{}, false},
{nil, false},
}
for _, tt := range tests {
got := getOnline(tt.info)
if got != tt.want {
t.Errorf("getOnline(%v) = %v, want %v", tt.info, got, tt.want)
}
}
}
func TestIsSharedDevice(t *testing.T) {
tests := []struct {
info map[string]interface{}
want bool
}{
{map[string]interface{}{"owner": map[string]interface{}{"userid": "123"}}, true},
{map[string]interface{}{"owner": map[string]interface{}{"nickname": "user"}}, false},
{map[string]interface{}{}, false},
{map[string]interface{}{"owner": "not_a_map"}, false},
}
for _, tt := range tests {
got := isSharedDevice(tt.info)
if got != tt.want {
t.Errorf("isSharedDevice(%v) = %v, want %v", tt.info, got, tt.want)
}
}
}
func TestCloneMap(t *testing.T) {
orig := map[string]interface{}{"a": "1", "b": 2}
clone := cloneMap(orig)
if clone["a"] != "1" || clone["b"] != 2 {
t.Error("cloneMap should preserve values")
}
clone["a"] = "changed"
if orig["a"] != "1" {
t.Error("cloneMap should create independent copy")
}
}
func TestAggregateDeviceState(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
c.deviceListMu.Lock()
c.deviceListCache["did1"] = map[string]interface{}{"did": "did1", "online": false}
// All sources offline
c.deviceListCloud["did1"] = map[string]interface{}{"online": false}
c.aggregateDeviceState("did1")
if c.deviceListCache["did1"]["online"].(bool) {
t.Error("should be offline when all sources offline")
}
// Cloud online
c.deviceListCloud["did1"] = map[string]interface{}{"online": true}
c.aggregateDeviceState("did1")
if !c.deviceListCache["did1"]["online"].(bool) {
t.Error("should be online when cloud is online")
}
// Gateway online
c.deviceListCloud["did1"] = map[string]interface{}{"online": false}
c.deviceListGateway["did1"] = map[string]interface{}{"online": true}
c.aggregateDeviceState("did1")
if !c.deviceListCache["did1"]["online"].(bool) {
t.Error("should be online when gateway is online")
}
// LAN online
c.deviceListGateway["did1"] = map[string]interface{}{"online": false}
c.deviceListLan["did1"] = map[string]interface{}{"online": true}
c.aggregateDeviceState("did1")
if !c.deviceListCache["did1"]["online"].(bool) {
t.Error("should be online when LAN is online")
}
c.deviceListMu.Unlock()
}
// ============================================================================
// ScheduleRefreshDevices
// ============================================================================
func TestScheduleRefreshDevices_SetsTimer(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
c.ScheduleRefreshDevices(100 * time.Millisecond)
c.mu.Lock()
timer := c.refreshCloudDevicesTimer
c.mu.Unlock()
if timer == nil {
t.Error("refreshCloudDevicesTimer should be set")
}
// Cancel to avoid actual refresh
if timer != nil {
timer.Stop()
}
}
func TestScheduleRefreshDevices_ReplacesTimer(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
firstTimer := time.NewTimer(time.Hour)
c.refreshCloudDevicesTimer = firstTimer
c.ScheduleRefreshDevices(100 * time.Millisecond)
c.mu.Lock()
secondTimer := c.refreshCloudDevicesTimer
c.mu.Unlock()
if secondTimer == firstTimer {
t.Error("ScheduleRefreshDevices should replace existing timer")
}
if secondTimer != nil {
secondTimer.Stop()
}
}
// ============================================================================
// OnDeviceStateChanged
// ============================================================================
func TestOnDeviceStateChanged_UpdatesCache(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
c.deviceListMu.Lock()
c.deviceListCache["did1"] = map[string]interface{}{
"did": "did1",
"online": false,
}
c.deviceListMu.Unlock()
c.OnDeviceStateChanged("did1", map[string]interface{}{
"online": true,
})
c.deviceListMu.RLock()
online := c.deviceListCache["did1"]["online"]
c.deviceListMu.RUnlock()
if online != true {
t.Errorf("online should be true, got %v", online)
}
}
func TestOnDeviceStateChanged_NonExistentDevice(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
// Should not panic for non-existent device
c.OnDeviceStateChanged("nonexistent", map[string]interface{}{
"online": true,
})
}
// ============================================================================
// GetDeviceInfo nil check
// ============================================================================
func TestGetDeviceInfo_EmptyCache(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
// Empty cache
info := c.GetDeviceInfo("anything")
if info != nil {
t.Errorf("GetDeviceInfo on empty cache should return nil, got %v", info)
}
}