// 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") } }