Files
xiaomihome/miot/miot_device_test.go
4566704 cdc459b5ff fix(xiaomihome): 模块路径改为 git.zeroonesoft.cn/golib/xiaomihome
- 改动:go.mod module 声明与全部内部导入路径补全为完整域名路径,README 同步
- 原因:裸模块名无法被 go get 解析,发版前必须修正为 GOPRIVATE 可拉取路径
2026-10-05 08:53:23 +08:00

580 lines
15 KiB
Go

// Package miot_test provides black-box tests for miot_device.go device model.
package miot_test
import (
"sync"
"testing"
"time"
miot "git.zeroonesoft.cn/golib/xiaomihome/miot"
)
// ============================================================================
// NewMIoTDevice — field parsing
// ============================================================================
func TestDevice_NewAllFields(t *testing.T) {
// nil client is valid for testing basic field parsing
deviceInfo := map[string]interface{}{
"did": "device_001",
"name": "Test Light",
"model": "lumi.plug.v1",
"online": true,
"home_id": "home_123",
"home_name": "My Home",
"room_id": "room_456",
"room_name": "Living Room",
"group_id": "group_789",
"fw_version": "1.0.0",
"manufacturer": "Xiaomi",
"icon": "https://example.com/icon.png",
"token": "abc123token",
"local_ip": "192.168.1.100",
"connect_type": float64(2),
"rssi": float64(-65),
"ssid": "MyWiFi",
"bssid": "00:11:22:33:44:55",
}
dev := miot.NewMIoTDevice(nil, deviceInfo, nil)
if dev.DID() != "device_001" {
t.Errorf("DID = %q, want %q", dev.DID(), "device_001")
}
if dev.Name() != "Test Light" {
t.Errorf("Name = %q, want %q", dev.Name(), "Test Light")
}
if dev.Model() != "lumi.plug.v1" {
t.Errorf("Model = %q, want %q", dev.Model(), "lumi.plug.v1")
}
if !dev.Online() {
t.Error("Online should be true")
}
if !dev.IsOnline() {
t.Error("IsOnline should be true")
}
if dev.HomeID() != "home_123" {
t.Errorf("HomeID = %q", dev.HomeID())
}
if dev.HomeName() != "My Home" {
t.Errorf("HomeName = %q", dev.HomeName())
}
if dev.RoomID() != "room_456" {
t.Errorf("RoomID = %q", dev.RoomID())
}
if dev.RoomName() != "Living Room" {
t.Errorf("RoomName = %q", dev.RoomName())
}
if dev.GroupID() != "group_789" {
t.Errorf("GroupID = %q", dev.GroupID())
}
if dev.FWVersion() != "1.0.0" {
t.Errorf("FWVersion = %q", dev.FWVersion())
}
if dev.Manufacturer() != "Xiaomi" {
t.Errorf("Manufacturer = %q", dev.Manufacturer())
}
if dev.Icon() != "https://example.com/icon.png" {
t.Errorf("Icon = %q", dev.Icon())
}
if dev.Token() != "abc123token" {
t.Errorf("Token = %q", dev.Token())
}
if dev.IP() != "192.168.1.100" {
t.Errorf("IP = %q", dev.IP())
}
if dev.ConnectType() != 2 {
t.Errorf("ConnectType = %d, want 2", dev.ConnectType())
}
if dev.RSSI() != -65 {
t.Errorf("RSSI = %d, want -65", dev.RSSI())
}
if dev.SSID() != "MyWiFi" {
t.Errorf("SSID = %q", dev.SSID())
}
if dev.BSSID() != "00:11:22:33:44:55" {
t.Errorf("BSSID = %q", dev.BSSID())
}
}
func TestDevice_NewMinimalFields(t *testing.T) {
// Only minimum fields
deviceInfo := map[string]interface{}{
"did": "device_min",
"name": "Min Device",
}
dev := miot.NewMIoTDevice(nil, deviceInfo, nil)
if dev.DID() != "device_min" {
t.Errorf("DID = %q", dev.DID())
}
if dev.Name() != "Min Device" {
t.Errorf("Name = %q", dev.Name())
}
if dev.Model() != "" {
t.Errorf("Model should be empty, got %q", dev.Model())
}
if dev.Online() {
t.Error("Online should default to false")
}
if dev.HomeID() != "" {
t.Error("HomeID should be empty")
}
}
func TestDevice_NewEmptyInfo(t *testing.T) {
// Empty device info should not panic
dev := miot.NewMIoTDevice(nil, map[string]interface{}{}, nil)
if dev == nil {
t.Fatal("NewMIoTDevice should not return nil")
}
if dev.DID() != "" {
t.Errorf("DID should be empty, got %q", dev.DID())
}
}
func TestDevice_NewNilInfo(t *testing.T) {
// nil device info should not panic
dev := miot.NewMIoTDevice(nil, nil, nil)
if dev == nil {
t.Fatal("NewMIoTDevice with nil info should still return device")
}
}
// ============================================================================
// Model splitting
// ============================================================================
func TestDevice_ModelStrs(t *testing.T) {
tests := []struct {
model string
wantLen int
wantPart string
}{
{"lumi.plug.maeu01", 3, "lumi"},
{"xiaomi.device", 2, "xiaomi"},
{"simple", 1, "simple"},
}
for _, tt := range tests {
t.Run(tt.model, func(t *testing.T) {
dev := miot.NewMIoTDevice(nil, map[string]interface{}{
"did": "d",
"model": tt.model,
}, nil)
if dev.Model() != tt.model {
t.Errorf("Model = %q, want %q", dev.Model(), tt.model)
}
// ModelStrs are internal, verified via manufacturer (first part)
if dev.Manufacturer() != tt.wantPart {
// manufacturer is set from deviceInfo["manufacturer"], not from model split
// This test verifies Model() returns correctly
}
})
}
}
// ============================================================================
// Online / SetOnline
// ============================================================================
func TestDevice_SetOnline(t *testing.T) {
dev := miot.NewMIoTDevice(nil, map[string]interface{}{
"did": "d",
"online": false,
}, nil)
if dev.Online() {
t.Error("Online should be false initially")
}
dev.SetOnline(true)
if !dev.Online() {
t.Error("Online should be true after SetOnline(true)")
}
if !dev.IsOnline() {
t.Error("IsOnline should be true")
}
dev.SetOnline(false)
if dev.Online() {
t.Error("Online should be false after SetOnline(false)")
}
}
// ============================================================================
// HasWiFi
// ============================================================================
func TestDevice_HasWiFi(t *testing.T) {
tests := []struct {
name string
connectType float64
want bool
}{
{"connected_pid2", 2, true},
{"connected_pid0", 0, true},
{"not_connected", -1, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dev := miot.NewMIoTDevice(nil, map[string]interface{}{
"did": "d",
"connect_type": tt.connectType,
}, nil)
if dev.HasWiFi() != tt.want {
t.Errorf("HasWiFi = %v, want %v", dev.HasWiFi(), tt.want)
}
})
}
}
// ============================================================================
// LoadFromCloud
// ============================================================================
func TestDevice_LoadFromCloud(t *testing.T) {
cloudData := map[string]interface{}{
"did": "cloud_dev",
"name": "Cloud Device",
}
dev := miot.LoadFromCloud(nil, cloudData)
if dev == nil {
t.Fatal("LoadFromCloud should not return nil")
}
if dev.DID() != "cloud_dev" {
t.Errorf("DID = %q, want 'cloud_dev'", dev.DID())
}
}
// ============================================================================
// LoadSPEC / SpecInstance
// ============================================================================
func TestDevice_LoadSPEC(t *testing.T) {
dev := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "d"}, nil)
// nil spec → error
err := dev.LoadSPEC(nil)
if err == nil {
t.Error("LoadSPEC(nil) should return error")
}
if dev.SpecInstance() != nil {
t.Error("SpecInstance should be nil before LoadSPEC")
}
}
func TestDevice_SetSpecInstance(t *testing.T) {
dev := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "d"}, nil)
// Create a minimal spec instance
spec := &miot.MIoTSpecInstance{
URN: "urn:example:1",
Type: "example",
Description: "Test Spec",
DescriptionTrans: "Test Spec Translation",
}
dev.SetSpecInstance(spec)
if dev.SpecInstance() != spec {
t.Error("SpecInstance should return the set value")
}
}
// ============================================================================
// Sub/Unsub device state (no client needed)
// ============================================================================
func TestDevice_SubscribeState(t *testing.T) {
dev := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "d"}, nil)
var calledMu sync.Mutex
callCount := 0
handler := func(did string, state interface{}) {
calledMu.Lock()
callCount++
calledMu.Unlock()
}
subID := dev.SubscribeState("online", handler)
if subID == "" {
t.Error("SubscribeState should return non-empty subID")
}
// Notify and check handler is called
done := make(chan bool, 1)
go func() {
dev.NotifyStateChange(map[string]interface{}{"online": true})
time.Sleep(50 * time.Millisecond)
done <- true
}()
<-done
calledMu.Lock()
count := callCount
calledMu.Unlock()
if count != 1 {
t.Errorf("handler should be called once, got %d", count)
}
// Unsubscribe
dev.UnsubscribeState("online", subID)
// Notify again - should not call handler
go func() {
dev.NotifyStateChange(map[string]interface{}{"online": false})
time.Sleep(50 * time.Millisecond)
done <- true
}()
<-done
calledMu.Lock()
count = callCount
calledMu.Unlock()
if count != 1 {
t.Errorf("handler should not be called after unsub, got %d calls", count)
}
}
// ============================================================================
// SubProp / UnsubProp (no client → warns)
// ============================================================================
func TestDevice_SubPropNoClient(t *testing.T) {
dev := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "d"}, nil)
handler := func(params map[string]interface{}, ctx interface{}) {}
subID := dev.SubProp(2, 1, handler)
// With nil client, should return empty string and not panic
if subID != "" {
t.Errorf("SubProp with nil client should return empty string, got %q", subID)
}
// UnsubProp with nil client should not panic
dev.UnsubProp("some_id")
}
func TestDevice_SubEventNoClient(t *testing.T) {
dev := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "d"}, nil)
handler := func(params map[string]interface{}, ctx interface{}) {}
subID := dev.SubEvent(2, 1, handler)
if subID != "" {
t.Errorf("SubEvent with nil client should return empty string, got %q", subID)
}
dev.UnsubEvent("some_id")
}
// ============================================================================
// ToDict / FromDict
// ============================================================================
func TestDevice_ToDict(t *testing.T) {
dev := miot.NewMIoTDevice(nil, map[string]interface{}{
"did": "dev1",
"name": "Test Device",
"model": "test.model.v1",
"online": true,
"home_id": "h1",
"home_name": "Home",
"room_id": "r1",
"room_name": "Room",
"group_id": "g1",
"fw_version": "1.0",
"icon": "icon.png",
"token": "tok",
"local_ip": "127.0.0.1",
"connect_type": float64(1),
"rssi": float64(-50),
"ssid": "wifi",
"bssid": "aa:bb",
}, nil)
dict := dev.ToDict()
if dict["did"] != "dev1" {
t.Errorf("did = %v", dict["did"])
}
if dict["name"] != "Test Device" {
t.Errorf("name = %v", dict["name"])
}
if dict["online"] != true {
t.Errorf("online = %v", dict["online"])
}
if dict["model"] != "test.model.v1" {
t.Errorf("model = %v", dict["model"])
}
if dict["home_id"] != "h1" {
t.Errorf("home_id = %v", dict["home_id"])
}
if dict["room_id"] != "r1" {
t.Errorf("room_id = %v", dict["room_id"])
}
if dict["connect_type"] != 1 {
t.Errorf("connect_type = %v", dict["connect_type"])
}
if dict["local_ip"] != "127.0.0.1" {
t.Errorf("local_ip = %v", dict["local_ip"])
}
}
func TestDevice_FromDict(t *testing.T) {
data := map[string]interface{}{
"did": "from_dict_dev",
"name": "From Dict Device",
"online": true,
"model": "model.x",
"manufacturer": "TestCo",
}
dev := miot.FromDict(nil, data)
if dev == nil {
t.Fatal("FromDict should not return nil")
}
if dev.DID() != "from_dict_dev" {
t.Errorf("DID = %q", dev.DID())
}
if dev.Name() != "From Dict Device" {
t.Errorf("Name = %q", dev.Name())
}
if dev.Manufacturer() != "TestCo" {
t.Errorf("Manufacturer = %q", dev.Manufacturer())
}
}
func TestDevice_ToDictFromDictRoundtrip(t *testing.T) {
original := miot.NewMIoTDevice(nil, map[string]interface{}{
"did": "roundtrip",
"name": "Roundtrip",
"model": "m.v",
"online": true,
"manufacturer": "Mfr",
"fw_version": "2.0",
"icon": "i.png",
"home_id": "h",
"home_name": "hn",
"room_id": "r",
"room_name": "rn",
"group_id": "g",
"connect_type": float64(3),
"rssi": float64(-30),
"ssid": "s",
"bssid": "b",
"token": "t",
"local_ip": "10.0.0.1",
}, nil)
dict := original.ToDict()
restored := miot.FromDict(nil, dict)
if restored.DID() != original.DID() {
t.Error("DID mismatch")
}
if restored.Name() != original.Name() {
t.Error("Name mismatch")
}
if restored.Model() != original.Model() {
t.Error("Model mismatch")
}
if restored.Manufacturer() != original.Manufacturer() {
t.Error("Manufacturer mismatch")
}
if restored.FWVersion() != original.FWVersion() {
t.Error("FWVersion mismatch")
}
if restored.Icon() != original.Icon() {
t.Error("Icon mismatch")
}
if restored.HomeID() != original.HomeID() {
t.Error("HomeID mismatch")
}
if restored.RoomID() != original.RoomID() {
t.Error("RoomID mismatch")
}
}
// ============================================================================
// Sub devices
// ============================================================================
func TestDevice_AddGetSubDevice(t *testing.T) {
parent := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "parent"}, nil)
child := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "child"}, nil)
// Get non-existent
if parent.GetSubDevice(1) != nil {
t.Error("GetSubDevice should return nil for non-existent")
}
parent.AddSubDevice(1, child)
got := parent.GetSubDevice(1)
if got == nil {
t.Fatal("GetSubDevice should return added device")
}
if got.DID() != "child" {
t.Errorf("sub device DID = %q, want 'child'", got.DID())
}
// Get non-existent again
if parent.GetSubDevice(2) != nil {
t.Error("GetSubDevice(2) should return nil")
}
}
func TestDevice_SubDevicesAccessor(t *testing.T) {
parent := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "parent"}, nil)
subs := parent.SubDevices()
if len(subs) != 0 {
t.Errorf("SubDevices should be empty, got %d", len(subs))
}
child := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "child"}, nil)
parent.AddSubDevice(1, child)
subs = parent.SubDevices()
if len(subs) != 1 {
t.Errorf("SubDevices should have 1 element, got %d", len(subs))
}
}
func TestDevice_ToDictWithSubDevices(t *testing.T) {
parent := miot.NewMIoTDevice(nil, map[string]interface{}{
"did": "parent_dev",
}, nil)
child := miot.NewMIoTDevice(nil, map[string]interface{}{
"did": "child_dev",
"name": "Child Device",
}, nil)
parent.AddSubDevice(2, child)
dict := parent.ToDict()
if subDevs, ok := dict["sub_devices"].(map[string]interface{}); !ok {
t.Error("ToDict should contain sub_devices")
} else {
if _, ok := subDevs["s2"]; !ok {
t.Errorf("sub_devices should contain key 's2', got %v", subDevs)
}
}
}
// ============================================================================
// ServiceList
// ============================================================================
func TestDevice_ServiceList(t *testing.T) {
dev := miot.NewMIoTDevice(nil, map[string]interface{}{"did": "d"}, nil)
// Initially nil
if dev.ServiceList() != nil {
t.Error("ServiceList should be nil initially")
}
}