Files
xiaomihome/miot/miot_client_test.go
T
4566704 b808e51b3e refactor: 重大架构升级 — HomeID/RoomID int64化 + 设备驱动重构 + 结构化日志
主要变更:

**类型系统重构**
- 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 敏感配置
2026-07-13 10:40:20 +08:00

275 lines
7.0 KiB
Go

// Package miot provides white-box tests for miot_client.go core client.
// Complements existing tests in miot_test.go with additional coverage.
package miot
import (
"testing"
)
// ============================================================================
// NewMIoTClient — field initialization
// ============================================================================
func TestNewMIoTClient_FieldsInitialized(t *testing.T) {
c := NewMIoTClient("entry1", map[string]interface{}{"key": "val"}, "uid1", "cn", CtrlModeCloud)
if c.entryID != "entry1" {
t.Errorf("entryID = %q, want 'entry1'", c.entryID)
}
if c.uid != "uid1" {
t.Errorf("uid = %q, want 'uid1'", c.uid)
}
if c.cloudServer != "cn" {
t.Errorf("cloudServer = %q, want 'cn'", c.cloudServer)
}
if c.ctx == nil {
t.Error("ctx should not be nil")
}
if c.cancel == nil {
t.Error("cancel should not be nil")
}
if c.subTree == nil {
t.Error("subTree should not be nil")
}
if c.deviceListCache == nil {
t.Error("deviceListCache should not be nil")
}
if c.refreshPropsList == nil {
t.Error("refreshPropsList should not be nil")
}
if c.mipsLocal == nil {
t.Error("mipsLocal should not be nil")
}
c.Stop()
}
func TestNewMIoTClient_NoEntryData(t *testing.T) {
c := NewMIoTClient("entry2", nil, "uid2", "us", CtrlModeLAN)
if c.ctrlMode != CtrlModeLAN {
t.Errorf("ctrlMode = %v, want LAN", c.ctrlMode)
}
c.Stop()
}
// ============================================================================
// SetStorage / SetHTTPClient / SetMipsCloud / SetMipsLocal
// ============================================================================
func TestSetStorage(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
s := NewMIoTStorage(t.TempDir())
c.SetStorage(s)
if c.storage != s {
t.Error("SetStorage did not set storage")
}
}
func TestSetHTTPClient(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
http := &MIoTHttpClient{}
c.SetHTTPClient(http)
if c.http != http {
t.Error("SetHTTPClient did not set http")
}
}
func TestSetMipsCloud(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
mc := NewMipsCloudClient("ssl://host:8883", "client1", "user", "pass")
c.SetMipsCloud(mc)
if c.mipsCloud != mc {
t.Error("SetMipsCloud did not set mipsCloud")
}
// Clean up: nil out mipsCloud before Stop/Deinit to avoid Disconnect panic
c.mipsCloud = nil
c.Stop()
}
func TestSetMipsLocal(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
ml := NewMipsLocalClient("did_local", "host", 8883, "", "", "")
c.SetMipsLocal("group1", ml)
if got := c.mipsLocal["group1"]; got != ml {
t.Errorf("SetMipsLocal = %v, want %v", got, ml)
}
// Clean up before Stop
c.mipsLocal = make(map[string]*MipsLocalClient)
c.Stop()
}
// ============================================================================
// Init — no entryData (no HTTP client created)
// ============================================================================
func TestInit_NoCredentials(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
s := NewMIoTStorage(t.TempDir())
c.SetStorage(s)
err := c.Init()
if err != nil {
t.Fatalf("Init should not error: %v", err)
}
// Without entryData credentials, http should remain nil
if c.http != nil {
t.Error("http should be nil without credentials in entryData")
}
}
func TestInit_WithCredentials(t *testing.T) {
c := NewMIoTClient("test", map[string]interface{}{
"client_id": "test_client_id",
"access_token": "test_token",
}, "uid", "cn", CtrlModeCloud)
defer c.Stop()
s := NewMIoTStorage(t.TempDir())
c.SetStorage(s)
err := c.Init()
if err != nil {
t.Fatalf("Init should not error: %v", err)
}
// With credentials, http should be created
if c.http == nil {
t.Error("http should not be nil with valid entryData credentials")
}
}
// ============================================================================
// Deinit
// ============================================================================
func TestDeinit_ClearsSubTree(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
// Add a subscription
h := func(params map[string]interface{}, ctx interface{}) {}
c.subTree.Sub("did1/p/1", h, nil)
if c.subTree.Size() != 1 {
t.Fatal("subTree should have 1 entry")
}
c.Deinit()
if c.subTree.Size() != 0 {
t.Error("subTree should be cleared after Deinit")
}
}
// ============================================================================
// Start / Stop
// ============================================================================
func TestStart(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
s := NewMIoTStorage(t.TempDir())
c.SetStorage(s)
err := c.Start()
if err != nil {
t.Fatalf("Start should not error: %v", err)
}
}
func TestStop(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
err := c.Stop()
if err != nil {
t.Errorf("Stop should not error: %v", err)
}
}
// ============================================================================
// LoadDevices (complement to existing test)
// ============================================================================
func TestLoadDevices_NoStorage(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
// LoadDevices without storage should not panic
err := c.LoadDevices()
// May error due to missing storage, but should not panic
_ = err
}
// ============================================================================
// DeviceList copy
// ============================================================================
func TestDeviceList_ReturnsCopy(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
c.deviceListMu.Lock()
c.deviceListCache["did1"] = map[string]interface{}{"name": "test"}
c.deviceListMu.Unlock()
list1 := c.DeviceList()
list2 := c.DeviceList()
// Modifying one copy should not affect the other
list1["new"] = map[string]interface{}{}
if _, ok := list2["new"]; ok {
t.Error("DeviceList should return independent copies")
}
}
// ============================================================================
// loadCacheDevice
// ============================================================================
func TestLoadCacheDevice_NoStorage(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
err := c.loadCacheDevice()
if err == nil {
t.Error("loadCacheDevice without storage should error")
}
c.Stop()
}
func TestLoadCacheDevice_WithStorage(t *testing.T) {
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
defer c.Stop()
s := NewMIoTStorage(t.TempDir())
c.SetStorage(s)
err := c.loadCacheDevice()
if err != nil {
t.Fatalf("loadCacheDevice should succeed: %v", err)
}
// Cache should be initialized (empty)
if c.deviceListCache == nil {
t.Error("deviceListCache should not be nil")
}
}