- 新增 xiaomi/examples/ 下 8 个示例程序(用户/家庭查询、设备列表、开关/灯光/空调控制、属性订阅、高级过滤分类、SPEC 解析) - miot_client_sub.go: 重构 SubProp/SubEvent,使用 buildPropTopic/buildEventTopic 支持通配符订阅(siid/piid=0 → +),并修复锁顺序问题(将 RequestRefreshProp 移到 Lock 外) - spec_parser.go: 新增 downloadSpecFile 方法,本地 SPEC 文件缺失时自动从 miot-spec.org 下载 - ARCH_PLAN.md: 架构设计从 Proposed 更新为 Accepted(v1.0→v1.1),补充设备分类/工厂/SPEC 映射等模块设计 - xiaomi/: 新增 miot 上层强类型封装模块,包含 Client 主入口、用户/家庭/设备 API、属性读写、动作调用、订阅通知,以及 devices/ 设备控制抽象(Switch/Light/AirConditioner/Fan/Cover/Humidifier/Vacuum/WaterHeater/Thermostat)和 specs/ SPEC 查询辅助 - 更多 xiaomi 示例(风扇/窗帘/传感器控制)
191 lines
4.7 KiB
Go
191 lines
4.7 KiB
Go
// Package miot provides white-box tests for miot_cloud.go cloud API client.
|
|
package miot
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func TestToFloat64(t *testing.T) {
|
|
tests := []struct {
|
|
input interface{}
|
|
want float64
|
|
}{
|
|
{float64(3.14), 3.14},
|
|
{int(5), 5.0},
|
|
{int64(10), 10.0},
|
|
{"123", 123.0},
|
|
{nil, 0.0},
|
|
{true, 0.0},
|
|
}
|
|
for _, tt := range tests {
|
|
got := toFloat64(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("toFloat64(%v) = %f, want %f", tt.input, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestToSlice(t *testing.T) {
|
|
if s := toSlice(nil); s != nil {
|
|
t.Errorf("toSlice(nil) = %v, want nil", s)
|
|
}
|
|
if s := toSlice([]interface{}{"a", "b"}); len(s) != 2 {
|
|
t.Errorf("toSlice len = %d, want 2", len(s))
|
|
}
|
|
if s := toSlice("not_a_slice"); s != nil {
|
|
t.Errorf("toSlice(string) = %v, want nil", s)
|
|
}
|
|
}
|
|
|
|
func TestToStringSlice(t *testing.T) {
|
|
s := toStringSlice([]string{"a", "b", "c"})
|
|
if len(s) != 3 || s[0] != "a" {
|
|
t.Errorf("toStringSlice([]string) = %v", s)
|
|
}
|
|
s = toStringSlice([]interface{}{"x", "y"})
|
|
if len(s) != 2 || s[1] != "y" {
|
|
t.Errorf("toStringSlice([]interface{}) = %v", s)
|
|
}
|
|
s = toStringSlice([]interface{}{"str", 42})
|
|
if s[1] != "42" {
|
|
t.Errorf("toStringSlice with int = %v, want '42'", s)
|
|
}
|
|
s = toStringSlice(nil)
|
|
if s != nil {
|
|
t.Errorf("toStringSlice(nil) should be nil, got %v", s)
|
|
}
|
|
}
|
|
|
|
func TestIsUnsupportedModel(t *testing.T) {
|
|
if !isUnsupportedModel("chuangmi.ir.v2") {
|
|
t.Error("chuangmi.ir.v2 should be unsupported")
|
|
}
|
|
if isUnsupportedModel("lumi.plug.v1") {
|
|
t.Error("lumi.plug.v1 should be supported")
|
|
}
|
|
if isUnsupportedModel("") {
|
|
t.Error("empty string should be supported")
|
|
}
|
|
}
|
|
|
|
func TestContainsString(t *testing.T) {
|
|
slice := []string{"a", "b", "c"}
|
|
if !containsString(slice, "b") {
|
|
t.Error("should contain 'b'")
|
|
}
|
|
if containsString(slice, "z") {
|
|
t.Error("should not contain 'z'")
|
|
}
|
|
if containsString(nil, "a") {
|
|
t.Error("nil slice should not contain anything")
|
|
}
|
|
}
|
|
|
|
func TestNewMIoTHttpClient_Basic(t *testing.T) {
|
|
client, err := NewMIoTHttpClient("cn", "client_id_123", "access_token_abc")
|
|
if err != nil {
|
|
t.Fatalf("NewMIoTHttpClient failed: %v", err)
|
|
}
|
|
if client == nil {
|
|
t.Fatal("client should not be nil")
|
|
}
|
|
client.Close()
|
|
}
|
|
|
|
func TestNewMIoTHttpClient_NonCN(t *testing.T) {
|
|
client, err := NewMIoTHttpClient("us", "id", "token")
|
|
if err != nil {
|
|
t.Fatalf("NewMIoTHttpClient for us failed: %v", err)
|
|
}
|
|
client.Close()
|
|
}
|
|
|
|
func TestNewMIoTOauthClient_Basic(t *testing.T) {
|
|
oa := NewMIoTOauthClient(OAUTH2_CLIENT_ID, "http://localhost/callback", "cn", "test-uuid")
|
|
if oa == nil {
|
|
t.Fatal("NewMIoTOauthClient returned nil")
|
|
}
|
|
s := oa.State()
|
|
if s == "" {
|
|
t.Error("State should not be empty")
|
|
}
|
|
oa.Close()
|
|
}
|
|
|
|
func TestGenAuthURL(t *testing.T) {
|
|
oa := NewMIoTOauthClient(OAUTH2_CLIENT_ID, "http://localhost/callback", "cn", "test-uuid")
|
|
defer oa.Close()
|
|
|
|
url := oa.GenAuthURL("", "", nil, false)
|
|
if url == "" {
|
|
t.Error("GenAuthURL returned empty string")
|
|
}
|
|
|
|
url2 := oa.GenAuthURL("http://custom/callback", "mystate", []string{"scope1", "scope2"}, true)
|
|
if url2 == "" {
|
|
t.Error("GenAuthURL with params returned empty string")
|
|
}
|
|
}
|
|
|
|
func TestUpdateHTTPHeader_CN(t *testing.T) {
|
|
client, err := NewMIoTHttpClient("cn", "id1", "tok1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer client.Close()
|
|
client.UpdateHTTPHeader("cn", "new_id", "new_token")
|
|
}
|
|
|
|
func TestUpdateHTTPHeader_NonCN(t *testing.T) {
|
|
client, err := NewMIoTHttpClient("us", "id1", "tok1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer client.Close()
|
|
client.UpdateHTTPHeader("de", "id2", "tok2")
|
|
}
|
|
|
|
func TestMihomeAPIGet_Success(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodGet {
|
|
t.Errorf("expected GET, got %s", r.Method)
|
|
}
|
|
resp := map[string]interface{}{"code": float64(0), "message": "ok", "result": map[string]interface{}{"data": "test"}}
|
|
json.NewEncoder(w).Encode(resp)
|
|
}))
|
|
defer server.Close()
|
|
_ = server
|
|
}
|
|
|
|
func TestPropParam_Struct(t *testing.T) {
|
|
p := PropParam{DID: "did1", Siid: 2, Piid: 1, Value: "on"}
|
|
if p.DID != "did1" || p.Siid != 2 || p.Piid != 1 {
|
|
t.Error("PropParam fields wrong")
|
|
}
|
|
}
|
|
|
|
func TestPropResult_Struct(t *testing.T) {
|
|
r := PropResult{DID: "did1", Siid: 2, Piid: 1, Code: 0, Value: "ok"}
|
|
if r.Code != 0 {
|
|
t.Errorf("PropResult Code = %d, want 0", r.Code)
|
|
}
|
|
}
|
|
|
|
func TestMIoTHttpClient_Close(t *testing.T) {
|
|
client, err := NewMIoTHttpClient("cn", "id", "token")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
client.Close()
|
|
client.Close()
|
|
}
|
|
|
|
func TestTokenExpiresTSRatio(t *testing.T) {
|
|
if TokenExpiresTSRatio <= 0 || TokenExpiresTSRatio >= 1.0 {
|
|
t.Errorf("TokenExpiresTSRatio = %f, should be between 0 and 1", TokenExpiresTSRatio)
|
|
}
|
|
}
|