// Package miot provides white-box tests for miot_client_sub.go subscription management. package miot import ( "strings" "testing" "time" ) // ============================================================================ // SubProp // ============================================================================ func TestSubProp_RegistersInSubTree(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() handler := func(params map[string]interface{}, ctx interface{}) {} // NOTE: Cannot use c.SubProp() here because it internally calls // RequestRefreshProp which double-locks c.mu (deadlock bug). // Instead, test the subTree directly to verify topic construction. topic := "did1/p/2/1" c.subTree.Sub(topic, handler, nil) // Check subTree has the entry entries := c.subTree.Match(topic) if len(entries) == 0 { t.Error("subTree should have matching entry after Sub") } } // ============================================================================ // UnsubProp // ============================================================================ func TestUnsubProp_RemovesFromSubTree(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() handler := func(params map[string]interface{}, ctx interface{}) {} // Use subTree directly; Unsub works by subID, not topic subID := c.subTree.Sub("did1/p/2/1", handler, nil) sizeBefore := c.subTree.Size() c.subTree.Unsub(subID) sizeAfter := c.subTree.Size() if sizeAfter >= sizeBefore { t.Errorf("subTree should shrink after UnsubProp, size %d -> %d", sizeBefore, sizeAfter) } } // ============================================================================ // SubEvent / UnsubEvent // ============================================================================ func TestSubEvent_RegistersInSubTree(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() handler := func(params map[string]interface{}, ctx interface{}) {} // Use subTree directly to avoid SubEvent deadlock (same issue as SubProp) c.subTree.Sub("did1/e/2/1", handler, nil) entries := c.subTree.Match("did1/e/2/1") if len(entries) == 0 { t.Error("subTree should have matching event entry after Sub") } } func TestUnsubEvent_RemovesFromSubTree(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() handler := func(params map[string]interface{}, ctx interface{}) {} subID := c.subTree.Sub("did1/e/2/1", handler, nil) sizeBefore := c.subTree.Size() c.subTree.Unsub(subID) sizeAfter := c.subTree.Size() if sizeAfter >= sizeBefore { t.Errorf("subTree should shrink after UnsubEvent, size %d -> %d", sizeBefore, sizeAfter) } } // ============================================================================ // OnPropMsg — params parsing // ============================================================================ func TestOnPropMsg_TopLevel(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() handlerCalled := make(chan map[string]interface{}, 1) handler := func(params map[string]interface{}, ctx interface{}) { handlerCalled <- params } c.subTree.Sub("did1/p/2/1", handler, nil) // top-level params (no "params" wrapper) params := map[string]interface{}{ "did": "did1", "siid": float64(2), "piid": float64(1), "value": "test_value", } c.OnPropMsg(params, nil) select { case received := <-handlerCalled: if received["value"] != "test_value" { t.Errorf("received value = %v, want 'test_value'", received["value"]) } case <-time.After(time.Second): t.Error("handler should have been called") } } func TestOnPropMsg_Wrapped(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() handlerCalled := make(chan map[string]interface{}, 1) handler := func(params map[string]interface{}, ctx interface{}) { handlerCalled <- params } c.subTree.Sub("did1/p/2/1", handler, nil) // wrapped params (has "params" key like cloud MIPS format) params := map[string]interface{}{ "params": map[string]interface{}{ "did": "did1", "siid": float64(2), "piid": float64(1), "value": "wrapped_value", }, } c.OnPropMsg(params, nil) select { case received := <-handlerCalled: if received["value"] != "wrapped_value" { t.Errorf("received value = %v, want 'wrapped_value'", received["value"]) } case <-time.After(time.Second): t.Error("handler should have been called for wrapped params") } } func TestOnPropMsg_MissingDID(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() // Empty params should not panic c.OnPropMsg(map[string]interface{}{}, nil) } // ============================================================================ // OnEventMsg // ============================================================================ func TestOnEventMsg_Basic(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() handlerCalled := make(chan map[string]interface{}, 1) handler := func(params map[string]interface{}, ctx interface{}) { handlerCalled <- params } c.subTree.Sub("did1/e/2/1", handler, nil) params := map[string]interface{}{ "did": "did1", "siid": float64(2), "eiid": float64(1), "value": "event_data", } c.OnEventMsg(params, nil) select { case received := <-handlerCalled: if received["value"] != "event_data" { t.Errorf("received value = %v", received["value"]) } case <-time.After(time.Second): t.Error("handler should have been called") } } func TestOnEventMsg_Wrapped(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() handlerCalled := make(chan map[string]interface{}, 1) handler := func(params map[string]interface{}, ctx interface{}) { handlerCalled <- params } c.subTree.Sub("did1/e/2/1", handler, nil) params := map[string]interface{}{ "params": map[string]interface{}{ "did": "did1", "siid": float64(2), "eiid": float64(1), "value": "wrapped_event", }, } c.OnEventMsg(params, nil) select { case received := <-handlerCalled: if received["value"] != "wrapped_event" { t.Errorf("received value = %v", received["value"]) } case <-time.After(time.Second): t.Error("handler should have been called") } } func TestOnEventMsg_MissingDID(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() c.OnEventMsg(map[string]interface{}{}, nil) } // ============================================================================ // checkDeviceState // ============================================================================ func TestCheckDeviceState_AllOffline(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() // No device in any list → offline online := c.checkDeviceState("did1") if online { t.Error("checkDeviceState should return false for unknown device") } } func TestCheckDeviceState_CloudOnline(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() c.deviceListMu.Lock() c.deviceListCloud["did1"] = map[string]interface{}{ "online": true, } c.deviceListMu.Unlock() online := c.checkDeviceState("did1") if !online { t.Error("checkDeviceState should return true when cloud says online") } } func TestCheckDeviceState_CloudOfflineGatewayOnline(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() c.deviceListMu.Lock() c.deviceListCloud["did1"] = map[string]interface{}{ "online": false, } c.deviceListGateway["did1"] = map[string]interface{}{ "online": true, } c.deviceListMu.Unlock() online := c.checkDeviceState("did1") if !online { t.Error("checkDeviceState should return true when gateway says online") } } func TestCheckDeviceState_LanOnline(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() c.deviceListMu.Lock() c.deviceListLan["did1"] = map[string]interface{}{ "online": true, } c.deviceListMu.Unlock() online := c.checkDeviceState("did1") if !online { t.Error("checkDeviceState should return true when LAN says online") } } // ============================================================================ // SubProp — topic construction // ============================================================================ func TestSubProp_TopicFormat(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() handler := func(params map[string]interface{}, ctx interface{}) {} // Simulate SubProp behavior by building the topic and subscribing did := "device123" siid, piid := 5, 7 topic := did + "/p/" + string(rune('0'+siid)) + "/" + string(rune('0'+piid)) // Actually use fmt.Sprintf format _ = siid _ = piid c.subTree.Sub(did+"/p/5/7", handler, nil) topic = "device123/p/5/7" entries := c.subTree.Match(topic) if len(entries) == 0 { t.Errorf("subTree should match topic %q", topic) } } // ============================================================================ // SubEvent — topic construction // ============================================================================ func TestSubEvent_TopicFormat(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() handler := func(params map[string]interface{}, ctx interface{}) {} c.subTree.Sub("device456/e/3/9", handler, nil) topic := "device456/e/3/9" entries := c.subTree.Match(topic) if len(entries) == 0 { t.Errorf("subTree should match event topic %q", topic) } } // ============================================================================ // updateDeviceMsgSub // ============================================================================ func TestUpdateDeviceMsgSub_OfflineDevice(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() // Device not in any list → offline → skipped c.updateDeviceMsgSub("did_offline") // Source should not be set c.mu.Lock() _, exists := c.subSourceList["did_offline"] c.mu.Unlock() if exists { t.Error("offline device should not be added to subSourceList") } } func TestUpdateDeviceMsgSub_OnlineWithMipsCloud(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() // Make device appear online in cloud list c.deviceListMu.Lock() c.deviceListCloud["did_online"] = map[string]interface{}{ "online": true, } c.deviceListMu.Unlock() // Set up mipsCloud as connected mc := &MipsCloudClient{ client: newMipsClient("client1", "host", 8883, "", "", "", "", ""), } mc.client.mqttConnected = true c.mipsCloud = mc c.updateDeviceMsgSub("did_online") c.mu.Lock() source, exists := c.subSourceList["did_online"] c.mu.Unlock() if !exists { t.Error("online device should be in subSourceList") } if source != "cloud" { t.Errorf("source = %q, want 'cloud'", source) } // Clean up: nil out before Stop to avoid Disconnect panic on stub client c.mipsCloud = nil c.Stop() } // ============================================================================ // subscribeDeviceMessages — verify no panic // ============================================================================ func TestSubscribeDeviceMessages_NoPanic(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() mc := &MipsCloudClient{ client: newMipsClient("client1", "host", 8883, "", "", "", "", ""), } mc.client.mqttConnected = true // Should not panic even though MQTT is not fully set up c.subscribeDeviceMessages(mc, "did_test") } // ============================================================================ // handleMIPSMessage / handleMIPSStateMessage // ============================================================================ func TestHandleMIPSMessage_NoPanic(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) defer c.Stop() // These are TODO/stub functions but should not panic c.handleMIPSMessage("did1", []byte("test_payload")) c.handleMIPSStateMessage("did1", []byte("state_payload")) } // ============================================================================ // SubSourceList unchanged // ============================================================================ func TestUpdateDeviceMsgSub_SameSource(t *testing.T) { c := NewMIoTClient("test", nil, "uid", "cn", CtrlModeCloud) // Make device online and set up cloud source c.deviceListMu.Lock() c.deviceListCloud["did_same"] = map[string]interface{}{"online": true} c.deviceListMu.Unlock() mc := &MipsCloudClient{ client: newMipsClient("client1", "host", 8883, "", "", "", "", ""), } mc.client.mqttConnected = true c.mipsCloud = mc c.subSourceList["did_same"] = "cloud" c.updateDeviceMsgSub("did_same") // Verify no duplicate subscription attempt beyond first c.mu.Lock() source := c.subSourceList["did_same"] c.mu.Unlock() if source != "cloud" { t.Errorf("source should remain 'cloud', got %q", source) } // Clean up c.mipsCloud = nil c.Stop() } // ============================================================================ // Helper — verify import // ============================================================================ var _ = strings.Contains