feat: add Go API library
This commit is contained in:
+407
@@ -0,0 +1,407 @@
|
||||
package mijia
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type apiExpectation struct {
|
||||
path string
|
||||
body string
|
||||
result string
|
||||
status int
|
||||
}
|
||||
|
||||
func TestCheckNewMessagesAndGetHomes(t *testing.T) {
|
||||
client, verify := newAPIClient(t, []apiExpectation{
|
||||
{path: "/v2/message/v2/check_new_msg", body: `{"begin_at":1700000000}`, result: `{"has_new":true}`},
|
||||
{path: "/v2/homeroom/gethome_merged", body: `{"app_ver":7,"fetch_cariot":true,"fetch_share":true,"fetch_share_dev":true,"fg":true,"limit":300,"plat_form":0}`, result: `{"homelist":[{"id":"10","name":"Main","uid":100,"room_id":"living"},{"id":20,"name":"Other","uid":200}]}`},
|
||||
})
|
||||
|
||||
message, err := client.CheckNewMessages(context.Background(), time.Unix(1700000000, 999))
|
||||
if err != nil || string(message) != `{"has_new":true}` {
|
||||
t.Fatalf("CheckNewMessages() = %s, %v", message, err)
|
||||
}
|
||||
homes, err := client.GetHomes(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(homes) != 2 || homes[0].ID != "10" || homes[1].ID != "20" || homes[1].UID != 200 {
|
||||
t.Fatalf("GetHomes() = %#v", homes)
|
||||
}
|
||||
if string(homes[0].Extra["room_id"]) != `"living"` {
|
||||
t.Fatalf("GetHomes()[0].Extra = %#v", homes[0].Extra)
|
||||
}
|
||||
verify()
|
||||
}
|
||||
|
||||
func TestGetDevicesReturnsPartialResultsForRepeatedCursor(t *testing.T) {
|
||||
client, verify := newAPIClient(t, []apiExpectation{
|
||||
{path: "/v2/homeroom/gethome_merged", body: homesBody, result: `{"homelist":[{"id":"10","uid":100},{"id":20,"uid":200}]}`},
|
||||
{path: "/home/home_device_list", body: `{"get_cariot_device":true,"get_split_device":true,"get_third_device":true,"home_id":10,"home_owner":100,"limit":200,"start_did":"","support_smart_home":true}`, result: `{"device_info":[{"did":"a","name":"Lamp","model":"lamp.a"}],"max_did":"a","has_more":true}`},
|
||||
{path: "/home/home_device_list", body: `{"get_cariot_device":true,"get_split_device":true,"get_third_device":true,"home_id":10,"home_owner":100,"limit":200,"start_did":"a","support_smart_home":true}`, result: `{"device_info":[{"did":"b","name":"Fan","model":"fan.b"}],"max_did":"a","has_more":true}`},
|
||||
})
|
||||
|
||||
devices, err := client.GetDevices(context.Background(), "")
|
||||
if err == nil || len(devices) != 2 || devices[0].HomeID != "10" || devices[1].DID != "b" || !strings.Contains(err.Error(), "max_did \"a\" repeats") {
|
||||
t.Fatalf("GetDevices() = %#v, %v", devices, err)
|
||||
}
|
||||
verify()
|
||||
}
|
||||
|
||||
func TestGetDevicesStopsCursorCycle(t *testing.T) {
|
||||
client, verify := newAPIClient(t, []apiExpectation{
|
||||
{path: "/v2/homeroom/gethome_merged", body: homesBody, result: `{"homelist":[{"id":"10","uid":100}]}`},
|
||||
{path: "/home/home_device_list", body: `{"get_cariot_device":true,"get_split_device":true,"get_third_device":true,"home_id":10,"home_owner":100,"limit":200,"start_did":"","support_smart_home":true}`, result: `{"device_info":[{"did":"a"}],"max_did":"a","has_more":true}`},
|
||||
{path: "/home/home_device_list", body: `{"get_cariot_device":true,"get_split_device":true,"get_third_device":true,"home_id":10,"home_owner":100,"limit":200,"start_did":"a","support_smart_home":true}`, result: `{"device_info":[{"did":"b"}],"max_did":"b","has_more":true}`},
|
||||
{path: "/home/home_device_list", body: `{"get_cariot_device":true,"get_split_device":true,"get_third_device":true,"home_id":10,"home_owner":100,"limit":200,"start_did":"b","support_smart_home":true}`, result: `{"device_info":[{"did":"c"}],"max_did":"a","has_more":true}`},
|
||||
})
|
||||
|
||||
devices, err := client.GetDevices(context.Background(), "10")
|
||||
if err == nil || len(devices) != 3 || !strings.Contains(err.Error(), "cursor cycle") {
|
||||
t.Fatalf("GetDevices() = %#v, %v", devices, err)
|
||||
}
|
||||
verify()
|
||||
}
|
||||
|
||||
func TestGetDevicesReturnsPartialResultsForEmptyCursor(t *testing.T) {
|
||||
client, verify := newAPIClient(t, []apiExpectation{
|
||||
{path: "/v2/homeroom/gethome_merged", body: homesBody, result: `{"homelist":[{"id":"10","uid":100}]}`},
|
||||
{path: "/home/home_device_list", body: `{"get_cariot_device":true,"get_split_device":true,"get_third_device":true,"home_id":10,"home_owner":100,"limit":200,"start_did":"","support_smart_home":true}`, result: `{"device_info":[{"did":"a"}],"has_more":true}`},
|
||||
})
|
||||
|
||||
devices, err := client.GetDevices(context.Background(), "10")
|
||||
if err == nil || len(devices) != 1 || !strings.Contains(err.Error(), "max_did is empty") {
|
||||
t.Fatalf("GetDevices() = %#v, %v", devices, err)
|
||||
}
|
||||
verify()
|
||||
}
|
||||
|
||||
func TestGetDevicesSingleHomeErrors(t *testing.T) {
|
||||
t.Run("owner missing", func(t *testing.T) {
|
||||
client, verify := newAPIClient(t, []apiExpectation{{path: "/v2/homeroom/gethome_merged", body: homesBody, result: `{"homelist":[]}`}})
|
||||
_, err := client.GetDevices(context.Background(), "10")
|
||||
var apiErr *APIError
|
||||
if !errors.As(err, &apiErr) || apiErr.Code != -1 {
|
||||
t.Fatalf("error = %v, want APIError(-1)", err)
|
||||
}
|
||||
verify()
|
||||
})
|
||||
|
||||
t.Run("invalid numeric id", func(t *testing.T) {
|
||||
client, verify := newAPIClient(t, []apiExpectation{{path: "/v2/homeroom/gethome_merged", body: homesBody, result: `{"homelist":[{"id":"home-a","uid":100}]}`}})
|
||||
_, err := client.GetDevices(context.Background(), "home-a")
|
||||
if err == nil {
|
||||
t.Fatal("GetDevices() error = nil")
|
||||
}
|
||||
verify()
|
||||
})
|
||||
}
|
||||
|
||||
func TestSharedDevicesScenesAndConsumables(t *testing.T) {
|
||||
client, verify := newAPIClient(t, []apiExpectation{
|
||||
{path: "/v2/home/device_list_page", body: `{"bssid":"02:00:00:00:00:00","getHuamiDevices":1,"getVirtualModel":true,"get_cariot_device":true,"get_miwear_device":true,"get_phone_device":true,"get_split_device":true,"get_third_device":true,"ssid":"<unknown ssid>","support_smart_home":true}`, result: `{"list":[{"did":"owned","owner":true,"localip":"192.0.2.1"},{"did":"shared","owner":false},{"did":"missing"}]}`},
|
||||
{path: "/v2/homeroom/gethome_merged", body: homesBody, result: `{"homelist":[{"id":"10","uid":100},{"id":"20","uid":200}]}`},
|
||||
{path: "/appgateway/miot/appsceneservice/AppSceneService/GetSimpleSceneList", body: `{"app_version":12,"get_type":2,"home_id":"10","owner_uid":100}`, result: `{"manual_scene_info_list":[{"scene_id":"s1","name":"Sleep","isOnline":true}]}`},
|
||||
{path: "/appgateway/miot/appsceneservice/AppSceneService/GetSimpleSceneList", body: `{"app_version":12,"get_type":2,"home_id":"20","owner_uid":200}`, result: `{}`},
|
||||
{path: "/v2/homeroom/gethome_merged", body: homesBody, result: `{"homelist":[{"id":"10","uid":100},{"id":"20","uid":200}]}`},
|
||||
{path: "/appgateway/miot/appsceneservice/AppSceneService/NewRunScene", body: `{"home_id":"10","owner_uid":100,"phone_id":"null","scene_id":"s1","scene_type":2}`, result: `true`},
|
||||
{path: "/v2/homeroom/gethome_merged", body: homesBody, result: `{"homelist":[{"id":"10","uid":100},{"id":"20","uid":200}]}`},
|
||||
{path: "/v2/home/standard_consumable_items", body: `{"filter_ignore":true,"home_id":10,"owner_id":100}`, result: `{"items":[{"consumes_data":[{"did":"a","name":"Filter","details":[{"id":"filter-life"}],"room_id":"kitchen"}]}]}`},
|
||||
{path: "/v2/home/standard_consumable_items", body: `{"filter_ignore":true,"home_id":20,"owner_id":200}`, result: `{"items":[]}`},
|
||||
})
|
||||
|
||||
shared, err := client.GetSharedDevices(context.Background())
|
||||
if err != nil || len(shared) != 1 || shared[0].DID != "owned" || shared[0].HomeID != "shared" || string(shared[0].Extra["localip"]) != `"192.0.2.1"` {
|
||||
t.Fatalf("GetSharedDevices() = %#v, %v", shared, err)
|
||||
}
|
||||
scenes, err := client.GetScenes(context.Background(), "")
|
||||
if err != nil || len(scenes) != 1 || scenes[0].HomeID != "10" || string(scenes[0].Extra["isOnline"]) != "true" {
|
||||
t.Fatalf("GetScenes() = %#v, %v", scenes, err)
|
||||
}
|
||||
runResult, err := client.RunScene(context.Background(), "s1", "10")
|
||||
if err != nil || string(runResult) != "true" {
|
||||
t.Fatalf("RunScene() = %s, %v", runResult, err)
|
||||
}
|
||||
consumables, err := client.GetConsumables(context.Background(), "")
|
||||
if err != nil || len(consumables) != 1 || consumables[0].HomeID != "10" || string(consumables[0].Details) != `{"id":"filter-life"}` || string(consumables[0].Extra["room_id"]) != `"kitchen"` {
|
||||
t.Fatalf("GetConsumables() = %#v, %v", consumables, err)
|
||||
}
|
||||
verify()
|
||||
}
|
||||
|
||||
func TestPropertiesActionsAndStatistics(t *testing.T) {
|
||||
client, verify := newAPIClient(t, []apiExpectation{
|
||||
{path: "/miotspec/prop/get", body: `{"datasource":1,"params":[{"did":"a","siid":2,"piid":1},{"did":"b","siid":3,"piid":2}]}`, result: `[{"did":"a","siid":2,"piid":1,"value":true,"code":0},{"did":"b","siid":3,"piid":2,"code":-704030013}]`},
|
||||
{path: "/miotspec/prop/set", body: `{"params":[{"did":"a","siid":2,"piid":1,"value":true},{"did":"b","siid":3,"piid":2,"value":5}]}`, result: `[{"did":"a","siid":2,"piid":1,"code":1},{"did":"b","siid":3,"piid":2,"code":-704030023}]`},
|
||||
{path: "/miotspec/action", body: `{"params":{"did":"a","siid":2,"aiid":1}}`, result: `{"did":"a","siid":2,"aiid":1,"code":0,"out":{"enabled":true}}`},
|
||||
{path: "/miotspec/action", body: `{"params":{"did":"b","siid":3,"aiid":2,"value":[5]}}`, result: `{"did":"b","siid":3,"aiid":2,"code":-704040005}`},
|
||||
{path: "/v2/user/statistics", body: `{"data_type":"stat_day_v3","did":"a","key":"2.1","limit":2,"time_end":20,"time_start":10}`, result: `[{"time":10,"value":"[1]"}]`},
|
||||
{path: "/v2/user/statistics", body: `{"data_type":"stat_hour_v3","did":"b","key":"3.2","limit":1,"time_end":40,"time_start":30}`, result: `[{"time":30,"value":"[2]"}]`},
|
||||
})
|
||||
|
||||
properties, err := client.GetProperties(context.Background(), []PropertyRequest{{DID: "a", SIID: 2, PIID: 1}, {DID: "b", SIID: 3, PIID: 2}})
|
||||
if err != nil || len(properties) != 2 || properties[0].Value != true {
|
||||
t.Fatalf("GetProperties() = %#v, %v", properties, err)
|
||||
}
|
||||
setResults, err := client.SetProperties(context.Background(), []PropertySetRequest{{DID: "a", SIID: 2, PIID: 1, Value: true}, {DID: "b", SIID: 3, PIID: 2, Value: 5}})
|
||||
if err != nil || setResults[0].Message != "成功" || setResults[1].Message != ErrorMessage(-704030023) {
|
||||
t.Fatalf("SetProperties() = %#v, %v", setResults, err)
|
||||
}
|
||||
actions, err := client.RunActions(context.Background(), []ActionRequest{{DID: "a", SIID: 2, AIID: 1}, {DID: "b", SIID: 3, AIID: 2, Value: []any{5}}})
|
||||
if err != nil || actions[0].Message != "成功" || string(actions[0].Out) != `{"enabled":true}` || actions[1].Message != ErrorMessage(-704040005) || actions[1].Out != nil {
|
||||
t.Fatalf("RunActions() = %#v, %v", actions, err)
|
||||
}
|
||||
statistics, err := client.GetStatistics(context.Background(), []StatisticsRequest{
|
||||
{DID: "a", Key: "2.1", DataType: "stat_day_v3", Limit: 2, TimeStart: 10, TimeEnd: 20},
|
||||
{DID: "b", Key: "3.2", DataType: "stat_hour_v3", Limit: 1, TimeStart: 30, TimeEnd: 40},
|
||||
})
|
||||
if err != nil || len(statistics) != 2 || string(statistics[1]) != `[{"time":30,"value":"[2]"}]` {
|
||||
t.Fatalf("GetStatistics() = %#v, %v", statistics, err)
|
||||
}
|
||||
verify()
|
||||
}
|
||||
|
||||
func TestPropertyResultsPreserveDynamicJSONNumbers(t *testing.T) {
|
||||
client, verify := newAPIClient(t, []apiExpectation{
|
||||
{path: "/miotspec/prop/get", body: `{"datasource":1,"params":[{"did":"integer","siid":2,"piid":1},{"did":"float","siid":2,"piid":2},{"did":"bool","siid":2,"piid":3},{"did":"string","siid":2,"piid":4}]}`, result: `[{"did":"integer","value":9007199254740993},{"did":"float","value":1.25},{"did":"bool","value":true},{"did":"string","value":"on"}]`},
|
||||
})
|
||||
|
||||
properties, err := client.GetProperties(context.Background(), []PropertyRequest{
|
||||
{DID: "integer", SIID: 2, PIID: 1},
|
||||
{DID: "float", SIID: 2, PIID: 2},
|
||||
{DID: "bool", SIID: 2, PIID: 3},
|
||||
{DID: "string", SIID: 2, PIID: 4},
|
||||
})
|
||||
integer, integerOK := properties[0].Value.(json.Number)
|
||||
decimal, decimalOK := properties[1].Value.(json.Number)
|
||||
if err != nil || !integerOK || integer.String() != "9007199254740993" || !decimalOK || decimal.String() != "1.25" || properties[2].Value != true || properties[3].Value != "on" {
|
||||
t.Fatalf("GetProperties() = %#v, %v", properties, err)
|
||||
}
|
||||
verify()
|
||||
}
|
||||
|
||||
func TestDecodeErrorsIncludeEndpoint(t *testing.T) {
|
||||
client, verify := newAPIClient(t, []apiExpectation{
|
||||
{path: "/miotspec/prop/get", body: `{"datasource":1,"params":[{"did":"a","siid":2,"piid":1}]}`, result: `{`},
|
||||
})
|
||||
|
||||
_, err := client.GetProperties(context.Background(), []PropertyRequest{{DID: "a", SIID: 2, PIID: 1}})
|
||||
if err == nil || !strings.Contains(err.Error(), "/miotspec/prop/get") {
|
||||
t.Fatalf("GetProperties() error = %v", err)
|
||||
}
|
||||
verify()
|
||||
}
|
||||
|
||||
func TestResponseTypesMarshalExtraWithoutKnownFieldConflicts(t *testing.T) {
|
||||
responses := []any{
|
||||
Home{ID: "10", Extra: map[string]json.RawMessage{"id": json.RawMessage(`"wrong"`), "room_id": json.RawMessage(`"living"`)}},
|
||||
Device{DID: "device", Extra: map[string]json.RawMessage{"did": json.RawMessage(`"wrong"`), "localip": json.RawMessage(`"192.0.2.1"`)}},
|
||||
Scene{SceneID: "scene", Extra: map[string]json.RawMessage{"scene_id": json.RawMessage(`"wrong"`), "isOnline": json.RawMessage(`true`)}},
|
||||
Consumable{DID: "filter", Extra: map[string]json.RawMessage{"did": json.RawMessage(`"wrong"`), "room_id": json.RawMessage(`"kitchen"`)}},
|
||||
}
|
||||
|
||||
for _, response := range responses {
|
||||
payload, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal(%T) error = %v", response, err)
|
||||
}
|
||||
var fields map[string]json.RawMessage
|
||||
if err := json.Unmarshal(payload, &fields); err != nil {
|
||||
t.Fatalf("json.Unmarshal(%T payload) error = %v", response, err)
|
||||
}
|
||||
if string(fields["id"]) == `"wrong"` || string(fields["did"]) == `"wrong"` || string(fields["scene_id"]) == `"wrong"` {
|
||||
t.Fatalf("json.Marshal(%T) allowed Extra conflict: %s", response, payload)
|
||||
}
|
||||
if fields["room_id"] == nil && fields["localip"] == nil && fields["isOnline"] == nil {
|
||||
t.Fatalf("json.Marshal(%T) dropped Extra: %s", response, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPropertiesEmptyRequestsDoNotUseNetwork(t *testing.T) {
|
||||
client, verify := newAPIClient(t, nil)
|
||||
|
||||
properties, err := client.GetProperties(context.Background(), nil)
|
||||
if err != nil || properties == nil || len(properties) != 0 {
|
||||
t.Fatalf("GetProperties(nil) = %#v, %v", properties, err)
|
||||
}
|
||||
setResults, err := client.SetProperties(context.Background(), []PropertySetRequest{})
|
||||
if err != nil || setResults == nil || len(setResults) != 0 {
|
||||
t.Fatalf("SetProperties(empty) = %#v, %v", setResults, err)
|
||||
}
|
||||
verify()
|
||||
}
|
||||
|
||||
func TestBatchRequestsReturnPartialResults(t *testing.T) {
|
||||
t.Run("actions", func(t *testing.T) {
|
||||
client, verify := newAPIClient(t, []apiExpectation{
|
||||
{path: "/miotspec/action", body: `{"params":{"did":"a","siid":2,"aiid":1}}`, result: `{"did":"a","siid":2,"aiid":1,"code":0}`},
|
||||
{path: "/miotspec/action", body: `{"params":{"did":"b","siid":2,"aiid":1}}`, status: http.StatusInternalServerError, result: `failed`},
|
||||
})
|
||||
|
||||
results, err := client.RunActions(context.Background(), []ActionRequest{{DID: "a", SIID: 2, AIID: 1}, {DID: "b", SIID: 2, AIID: 1}})
|
||||
if err == nil || len(results) != 1 || results[0].DID != "a" {
|
||||
t.Fatalf("RunActions() = %#v, %v", results, err)
|
||||
}
|
||||
verify()
|
||||
})
|
||||
|
||||
t.Run("statistics", func(t *testing.T) {
|
||||
requests := []StatisticsRequest{{DID: "a"}, {DID: "b"}}
|
||||
client, verify := newAPIClient(t, []apiExpectation{
|
||||
{path: "/v2/user/statistics", body: `{"data_type":"","did":"a","key":"","limit":0,"time_end":0,"time_start":0}`, result: `[1]`},
|
||||
{path: "/v2/user/statistics", body: `{"data_type":"","did":"b","key":"","limit":0,"time_end":0,"time_start":0}`, status: http.StatusInternalServerError, result: `failed`},
|
||||
})
|
||||
|
||||
results, err := client.GetStatistics(context.Background(), requests)
|
||||
if err == nil || len(results) != 1 || string(results[0]) != `[1]` {
|
||||
t.Fatalf("GetStatistics() = %#v, %v", results, err)
|
||||
}
|
||||
verify()
|
||||
})
|
||||
}
|
||||
|
||||
func TestResultMessagesPreserveUnknownDiagnostics(t *testing.T) {
|
||||
client, verify := newAPIClient(t, []apiExpectation{
|
||||
{path: "/miotspec/prop/set", body: `{"params":[{"did":"a","siid":2,"piid":1,"value":true},{"did":"b","siid":2,"piid":1,"value":true},{"did":"c","siid":2,"piid":1,"value":true}]}`, result: `[{"did":"a","code":0,"message":"wrong"},{"did":"b","code":-704030023,"message":"server known"},{"did":"c","code":-99999,"message":"server detail"}]`},
|
||||
{path: "/miotspec/action", body: `{"params":{"did":"d","siid":2,"aiid":1}}`, result: `{"did":"d","code":-99998,"message":"action detail"}`},
|
||||
{path: "/miotspec/action", body: `{"params":{"did":"e","siid":2,"aiid":1}}`, result: `{"did":"e","code":-99997}`},
|
||||
})
|
||||
|
||||
properties, err := client.SetProperties(context.Background(), []PropertySetRequest{{DID: "a", SIID: 2, PIID: 1, Value: true}, {DID: "b", SIID: 2, PIID: 1, Value: true}, {DID: "c", SIID: 2, PIID: 1, Value: true}})
|
||||
if err != nil || properties[0].Message != "成功" || properties[1].Message != ErrorMessage(-704030023) || properties[2].Message != "server detail" {
|
||||
t.Fatalf("SetProperties() = %#v, %v", properties, err)
|
||||
}
|
||||
actions, err := client.RunActions(context.Background(), []ActionRequest{{DID: "d", SIID: 2, AIID: 1}, {DID: "e", SIID: 2, AIID: 1}})
|
||||
if err != nil || actions[0].Message != "action detail" || actions[1].Message != "未知错误" {
|
||||
t.Fatalf("RunActions() = %#v, %v", actions, err)
|
||||
}
|
||||
verify()
|
||||
}
|
||||
|
||||
func TestActionRequestMarshalJSON(t *testing.T) {
|
||||
t.Run("nil extra matches legacy encoding", func(t *testing.T) {
|
||||
payload, err := json.Marshal(ActionRequest{DID: "d", SIID: 1, AIID: 2})
|
||||
if err != nil || string(payload) != `{"did":"d","siid":1,"aiid":2}` {
|
||||
t.Fatalf("json.Marshal() = %s, %v", payload, err)
|
||||
}
|
||||
payload, err = json.Marshal(ActionRequest{DID: "d", SIID: 1, AIID: 2, Value: []any{5}})
|
||||
if err != nil || string(payload) != `{"did":"d","siid":1,"aiid":2,"value":[5]}` {
|
||||
t.Fatalf("json.Marshal() = %s, %v", payload, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("extra keys are merged", func(t *testing.T) {
|
||||
payload, err := json.Marshal(ActionRequest{DID: "d", SIID: 5, AIID: 4, Extra: map[string]any{"in": []any{"打开空调", 1}}})
|
||||
if err != nil || !jsonEqual(payload, []byte(`{"did":"d","siid":5,"aiid":4,"in":["打开空调",1]}`)) {
|
||||
t.Fatalf("json.Marshal() = %s, %v", payload, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reserved key conflicts fail", func(t *testing.T) {
|
||||
for _, key := range []string{"did", "siid", "aiid", "value"} {
|
||||
if _, err := json.Marshal(ActionRequest{DID: "d", SIID: 1, AIID: 2, Extra: map[string]any{key: "x"}}); err == nil {
|
||||
t.Fatalf("json.Marshal() with extra key %q error = nil", key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunActionsSendsExtraInsideParams(t *testing.T) {
|
||||
client, verify := newAPIClient(t, []apiExpectation{
|
||||
{path: "/miotspec/action", body: `{"params":{"did":"speaker","siid":5,"aiid":4,"in":["打开空调",1]}}`, result: `{"did":"speaker","siid":5,"aiid":4,"code":0}`},
|
||||
})
|
||||
|
||||
results, err := client.RunActions(context.Background(), []ActionRequest{{DID: "speaker", SIID: 5, AIID: 4, Extra: map[string]any{"in": []any{"打开空调", 1}}}})
|
||||
if err != nil || len(results) != 1 || results[0].Code != 0 {
|
||||
t.Fatalf("RunActions() = %#v, %v", results, err)
|
||||
}
|
||||
verify()
|
||||
|
||||
client, verify = newAPIClient(t, nil)
|
||||
if _, err := client.RunActions(context.Background(), []ActionRequest{{DID: "speaker", SIID: 5, AIID: 4, Extra: map[string]any{"value": 1}}}); err == nil {
|
||||
t.Fatal("RunActions() with conflicting extra key error = nil")
|
||||
}
|
||||
verify()
|
||||
}
|
||||
|
||||
const homesBody = `{"app_ver":7,"fetch_cariot":true,"fetch_share":true,"fetch_share_dev":true,"fg":true,"limit":300,"plat_form":0}`
|
||||
|
||||
func newAPIClient(t *testing.T, expectations []apiExpectation) (*Client, func()) {
|
||||
t.Helper()
|
||||
var mutex sync.Mutex
|
||||
requestIndex := 0
|
||||
var handlerErrors []error
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
if requestIndex >= len(expectations) {
|
||||
handlerErrors = append(handlerErrors, fmt.Errorf("unexpected request %s", request.URL.Path))
|
||||
_, _ = io.WriteString(writer, `{"code":0,"result":{}}`)
|
||||
return
|
||||
}
|
||||
expectation := expectations[requestIndex]
|
||||
requestIndex++
|
||||
if request.URL.Path != expectation.path {
|
||||
handlerErrors = append(handlerErrors, fmt.Errorf("request %d path = %s, want %s", requestIndex, request.URL.Path, expectation.path))
|
||||
}
|
||||
if err := request.ParseForm(); err != nil {
|
||||
handlerErrors = append(handlerErrors, err)
|
||||
return
|
||||
}
|
||||
nonce := request.PostForm.Get("_nonce")
|
||||
signed, err := signedNonce(testSsecurity, nonce)
|
||||
if err != nil {
|
||||
handlerErrors = append(handlerErrors, err)
|
||||
return
|
||||
}
|
||||
payload, err := decryptRC4(signed, request.PostForm.Get("data"))
|
||||
if err != nil {
|
||||
handlerErrors = append(handlerErrors, err)
|
||||
return
|
||||
}
|
||||
if !jsonEqual(payload, []byte(expectation.body)) {
|
||||
handlerErrors = append(handlerErrors, fmt.Errorf("request %d body = %s, want %s", requestIndex, payload, expectation.body))
|
||||
}
|
||||
if expectation.status != 0 {
|
||||
writer.WriteHeader(expectation.status)
|
||||
_, _ = io.WriteString(writer, expectation.result)
|
||||
return
|
||||
}
|
||||
_, _ = io.WriteString(writer, `{"code":0,"result":`+expectation.result+`}`)
|
||||
}))
|
||||
|
||||
client := testClient(t, server.Client())
|
||||
client.baseURL = server.URL
|
||||
return client, func() {
|
||||
t.Helper()
|
||||
server.Close()
|
||||
mutex.Lock()
|
||||
count := requestIndex
|
||||
errors := append([]error(nil), handlerErrors...)
|
||||
mutex.Unlock()
|
||||
for _, err := range errors {
|
||||
t.Error(err)
|
||||
}
|
||||
if count != len(expectations) {
|
||||
t.Errorf("requests = %d, want %d", count, len(expectations))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func jsonEqual(left, right []byte) bool {
|
||||
var leftValue any
|
||||
var rightValue any
|
||||
return json.Unmarshal(left, &leftValue) == nil && json.Unmarshal(right, &rightValue) == nil && reflect.DeepEqual(leftValue, rightValue)
|
||||
}
|
||||
Reference in New Issue
Block a user