主要变更: **类型系统重构** - HomeInfo.ID/RoomInfo.ID/HomeID 从 string 改为 int64,json tag 使用 ,string - GetHome/GetRooms/GetRoomDevices 接口同步修改 - 新增 int64FromStr 工具函数,更新 parseHomeInfo 解析逻辑 **新增 Speaker 设备** - 新增 KindSpeaker 类型 - classify 映射覆盖 speaker/bluetooth-speaker/alarm-clock/media-player/playback/music **设备驱动重构** - AC 驱动彻底重写:从硬编码常量改为基于 SPEC ValueList 的动态 valMapper - 所有设备(Fan/Humidifier/Light/OccupancySensor/Switch/TempHumiditySensor/Thermostat/Vacuum/WaterHeater/Cover)新增 GetState() 和 FetchState(ctx) 接口 - base.go 新增 specResolver.format() 方法 **MIoT 模块增强** - MIoTClient 新增 homeIDs 字段,支持按家庭过滤设备 - RefreshDevices 支持 homeIDs 参数 - SpecStdLib 引入 logger.Logger,log.Printf 全部迁移为结构化日志 **其他** - Config 新增 RedirectURL 和 HomeIDs 字段 - AuthInfo.ExpiresIn 类型从 int 改为 int32 - UUID 生成简化为 hex.EncodeToString - 新增 github.com/sirupsen/logrus 依赖 - .gitignore 新增 examples/*/data/ 忽略规则 - 删除已提交的 config.json 敏感配置
292 lines
7.9 KiB
Go
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(nil)
|
|
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)
|
|
}
|
|
}
|