feat: 初始化小米 IoT (MIoT) 智能家居 Go 库
- 实现 MIoT 客户端核心功能(MQTT 连接、设备管理、属性读写) - 支持云端 API 调用与局域网设备发现(mDNS) - 集成国际化(i18n)多语言支持 - 添加 MIoT 设备规约解析器(spec_parser) - 包含单元测试与使用示例
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
// 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"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeinit_StopsTimers(t *testing.T) {
|
||||
c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud)
|
||||
|
||||
// Simulate a timer being set
|
||||
c.refreshTokenTimer = time.NewTimer(time.Hour)
|
||||
c.refreshCloudDevicesTimer = time.NewTimer(time.Hour)
|
||||
|
||||
c.Deinit()
|
||||
|
||||
// Timers should be stopped (check by trying to stop again)
|
||||
if c.refreshTokenTimer != nil {
|
||||
// Already stopped by Deinit, Stop() returns false
|
||||
_ = c.refreshTokenTimer
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user