Compare commits
2
Commits
25c4fbe089
...
v0.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3200089fa4 | ||
|
|
93799df53c |
@@ -499,6 +499,23 @@ type cacheMethod struct {
|
|||||||
type propertyCache struct {
|
type propertyCache struct {
|
||||||
PropertySpec
|
PropertySpec
|
||||||
Method cacheMethod `json:"method"`
|
Method cacheMethod `json:"method"`
|
||||||
|
rwPresent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *propertyCache) UnmarshalJSON(data []byte) error {
|
||||||
|
type propertyCacheAlias propertyCache
|
||||||
|
decoded := struct {
|
||||||
|
*propertyCacheAlias
|
||||||
|
RW *string `json:"rw"`
|
||||||
|
}{propertyCacheAlias: (*propertyCacheAlias)(cache)}
|
||||||
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cache.rwPresent = decoded.RW != nil
|
||||||
|
if decoded.RW != nil {
|
||||||
|
cache.RW = *decoded.RW
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type actionCache struct {
|
type actionCache struct {
|
||||||
@@ -536,6 +553,9 @@ func decodeDeviceInfo(data []byte, model string) (DeviceInfo, error) {
|
|||||||
|
|
||||||
info := DeviceInfo{Name: cache.Name, Model: cache.Model}
|
info := DeviceInfo{Name: cache.Name, Model: cache.Model}
|
||||||
for _, cachedProperty := range cache.Properties {
|
for _, cachedProperty := range cache.Properties {
|
||||||
|
if !cachedProperty.rwPresent {
|
||||||
|
return DeviceInfo{}, fmt.Errorf("property %q is missing access metadata", cachedProperty.Name)
|
||||||
|
}
|
||||||
property := cachedProperty.PropertySpec
|
property := cachedProperty.PropertySpec
|
||||||
if property.SIID == 0 {
|
if property.SIID == 0 {
|
||||||
property.SIID = cachedProperty.Method.SIID
|
property.SIID = cachedProperty.Method.SIID
|
||||||
@@ -572,7 +592,7 @@ func validateDeviceInfo(info *DeviceInfo, model string) error {
|
|||||||
properties := make(map[propertyID]PropertySpec, len(info.Properties))
|
properties := make(map[propertyID]PropertySpec, len(info.Properties))
|
||||||
for index, property := range info.Properties {
|
for index, property := range info.Properties {
|
||||||
if strings.TrimSpace(property.Name) == "" || !validPropertyType(property.Type) ||
|
if strings.TrimSpace(property.Name) == "" || !validPropertyType(property.Type) ||
|
||||||
(property.RW != "r" && property.RW != "w" && property.RW != "rw") || property.SIID <= 0 || property.PIID <= 0 {
|
(property.RW != "" && property.RW != "r" && property.RW != "w" && property.RW != "rw") || property.SIID <= 0 || property.PIID <= 0 {
|
||||||
return fmt.Errorf("property %d is invalid", index)
|
return fmt.Errorf("property %d is invalid", index)
|
||||||
}
|
}
|
||||||
properties[propertyID{siid: property.SIID, piid: property.PIID}] = property
|
properties[propertyID{siid: property.SIID, piid: property.PIID}] = property
|
||||||
|
|||||||
+75
-1
@@ -104,6 +104,15 @@ func loadSpecFixture(t *testing.T) []byte {
|
|||||||
return fixture
|
return fixture
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadNonControllableSpecFixture(t *testing.T) []byte {
|
||||||
|
t.Helper()
|
||||||
|
fixture, err := os.ReadFile("testdata/miot-spec-non-controllable.html")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return fixture
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetDeviceInfoParsesSpecAndCaches(t *testing.T) {
|
func TestGetDeviceInfoParsesSpecAndCaches(t *testing.T) {
|
||||||
testServer := newDeviceTestServer(t, loadSpecFixture(t), nil)
|
testServer := newDeviceTestServer(t, loadSpecFixture(t), nil)
|
||||||
httpClient := testServer.server.Client()
|
httpClient := testServer.server.Client()
|
||||||
@@ -171,6 +180,41 @@ func TestParseDeviceInfoRejectsUnknownActionInput(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetDeviceInfoPreservesNonControllableProperties(t *testing.T) {
|
||||||
|
testServer := newDeviceTestServer(t, loadNonControllableSpecFixture(t), nil)
|
||||||
|
cacheDir := t.TempDir()
|
||||||
|
info, err := GetDeviceInfo(context.Background(), testServer.server.Client(), "test.sensor.v1", cacheDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(info.Properties) != 2 || info.Properties[0].Name != "event" || info.Properties[0].RW != "" || info.Properties[1].Name != "command" || info.Properties[1].RW != "" {
|
||||||
|
t.Fatalf("properties = %#v", info.Properties)
|
||||||
|
}
|
||||||
|
if len(info.Actions) != 1 || len(info.Actions[0].Inputs) != 1 || !reflect.DeepEqual(info.Actions[0].Inputs[0], info.Properties[1]) {
|
||||||
|
t.Fatalf("actions = %#v", info.Actions)
|
||||||
|
}
|
||||||
|
|
||||||
|
testServer.fixture = nil
|
||||||
|
cached, err := GetDeviceInfo(context.Background(), testServer.server.Client(), "test.sensor.v1", cacheDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(cached.Properties) != 2 || cached.Properties[0].RW != "" || cached.Properties[1].RW != "" ||
|
||||||
|
len(cached.Actions) != 1 || len(cached.Actions[0].Inputs) != 1 || !reflect.DeepEqual(cached.Actions[0].Inputs[0], cached.Properties[1]) || testServer.specCalls != 1 {
|
||||||
|
t.Fatalf("cached = %#v, spec calls = %d", cached, testServer.specCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeviceRejectsGetSetForNonControllableProperty(t *testing.T) {
|
||||||
|
device := Device{properties: map[string]PropertySpec{"event": {Name: "event", Type: "string", SIID: 2, PIID: 1}}}
|
||||||
|
if _, err := device.Get(context.Background(), "event"); err == nil || !strings.Contains(err.Error(), "不可读取") {
|
||||||
|
t.Fatalf("Get() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := device.Set(context.Background(), "event", "value"); err == nil || !strings.Contains(err.Error(), "不可写入") {
|
||||||
|
t.Fatalf("Set() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeviceActionsDeepCopyInputs(t *testing.T) {
|
func TestDeviceActionsDeepCopyInputs(t *testing.T) {
|
||||||
device := Device{actions: map[string]ActionSpec{"toggle": {Inputs: []PropertySpec{{Range: []json.Number{"1", "2"}, ValueList: []ValueListItem{{Value: "1"}}}}}}}
|
device := Device{actions: map[string]ActionSpec{"toggle": {Inputs: []PropertySpec{{Range: []json.Number{"1", "2"}, ValueList: []ValueListItem{{Value: "1"}}}}}}}
|
||||||
actions := device.Actions()
|
actions := device.Actions()
|
||||||
@@ -282,6 +326,35 @@ func TestVersion2DeviceInfoCacheTrustsActionWithoutInputs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVersion2DeviceInfoCacheRequiresPropertyAccessField(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
property string
|
||||||
|
wantCalls int
|
||||||
|
}{
|
||||||
|
{name: "missing", property: `{"name":"power","type":"bool","siid":2,"piid":1}`, wantCalls: 1},
|
||||||
|
{name: "explicit empty", property: `{"name":"power","type":"bool","rw":"","siid":2,"piid":1}`},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
fixture := loadSpecFixture(t)
|
||||||
|
if test.wantCalls == 0 {
|
||||||
|
fixture = nil
|
||||||
|
}
|
||||||
|
testServer := newDeviceTestServer(t, fixture, nil)
|
||||||
|
cacheDir := t.TempDir()
|
||||||
|
cache := fmt.Sprintf(`{"version":2,"model":"test.light.v1","properties":[%s],"actions":[]}`, test.property)
|
||||||
|
if err := os.WriteFile(filepath.Join(cacheDir, "test.light.v1.json"), []byte(cache), 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := GetDeviceInfo(context.Background(), testServer.server.Client(), "test.light.v1", cacheDir)
|
||||||
|
if err != nil || testServer.specCalls != test.wantCalls || len(info.Properties) == 0 {
|
||||||
|
t.Fatalf("GetDeviceInfo() = %#v, %v, calls=%d, want calls=%d", info, err, testServer.specCalls, test.wantCalls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestStaleDeviceInfoCacheRefreshFailureIncludesBothErrors(t *testing.T) {
|
func TestStaleDeviceInfoCacheRefreshFailureIncludesBothErrors(t *testing.T) {
|
||||||
testServer := newDeviceTestServer(t, []byte("unavailable"), nil)
|
testServer := newDeviceTestServer(t, []byte("unavailable"), nil)
|
||||||
testServer.status = http.StatusServiceUnavailable
|
testServer.status = http.StatusServiceUnavailable
|
||||||
@@ -678,7 +751,8 @@ func TestDeviceGetManyReturnsTransportError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
func TestDeviceGetManyValidatesBeforeNetwork(t *testing.T) {
|
func TestDeviceGetManyValidatesBeforeNetwork(t *testing.T) {
|
||||||
device, testServer := fixtureDeviceWithServer(t, nil)
|
device, testServer := fixtureDeviceWithServer(t, nil)
|
||||||
tests := [][]string{{"power", "power"}, {"power", "missing"}, {"power", "write-only"}}
|
device.properties["no-access"] = PropertySpec{Name: "no-access", Type: "bool", SIID: 2, PIID: 4}
|
||||||
|
tests := [][]string{{"power", "power"}, {"power", "missing"}, {"power", "write-only"}, {"power", "no-access"}}
|
||||||
for _, names := range tests {
|
for _, names := range tests {
|
||||||
if results, err := device.GetMany(context.Background(), names); err == nil || results != nil {
|
if results, err := device.GetMany(context.Background(), names); err == nil || results != nil {
|
||||||
t.Fatalf("GetMany(%v) = %#v, %v", names, results, err)
|
t.Fatalf("GetMany(%v) = %#v, %v", names, results, err)
|
||||||
|
|||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<script data-page="app" type="application/json">{"props":{"product":{"name":"Test Sensor","model":"test.sensor.v1"},"i18n":{"zh_cn":{}},"tree":{"services":[{"iid":2,"type":"sensor","properties":[{"iid":1,"type":"event","description":"Event","format":"string","access":["notify"]},{"iid":2,"type":"command","description":"Command","format":"uint8","access":[],"valueRange":[0,10,1]}],"actions":[{"iid":1,"type":"execute","description":"Execute","in":[2]}]}]}}}</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user