fix: refresh action cache and classify auth failures

This commit is contained in:
2026-07-21 15:26:32 +08:00
parent 2cbac73f2a
commit 3f05417b83
4 changed files with 138 additions and 11 deletions
+70 -7
View File
@@ -141,6 +141,7 @@ func TestGetDeviceInfoParsesSpecAndCaches(t *testing.T) {
t.Fatal(err)
}
var pythonCache struct {
Version int `json:"version"`
Properties []struct {
Method cacheMethod `json:"method"`
} `json:"properties"`
@@ -148,7 +149,7 @@ func TestGetDeviceInfoParsesSpecAndCaches(t *testing.T) {
Method cacheMethod `json:"method"`
} `json:"actions"`
}
if err := json.Unmarshal(cacheData, &pythonCache); err != nil || pythonCache.Properties[0].Method.SIID != 2 || pythonCache.Properties[0].Method.PIID != 1 || pythonCache.Actions[0].Method.SIID != 2 || pythonCache.Actions[0].Method.AIID != 1 {
if err := json.Unmarshal(cacheData, &pythonCache); err != nil || pythonCache.Version != 2 || pythonCache.Properties[0].Method.SIID != 2 || pythonCache.Properties[0].Method.PIID != 1 || pythonCache.Actions[0].Method.SIID != 2 || pythonCache.Actions[0].Method.AIID != 1 {
t.Fatalf("Python-compatible cache = %#v, %v", pythonCache, err)
}
testServer.fixture = nil
@@ -186,7 +187,7 @@ func TestPythonDeviceInfoCacheSupportsOperations(t *testing.T) {
"properties": [{"name":"power","description":"Power","type":"bool","rw":"rw","method":{"siid":7,"piid":8}}],
"actions": [{"name":"toggle","description":"Toggle","method":{"siid":9,"aiid":10}}]
}`
testServer := newDeviceTestServer(t, nil, []string{
testServer := newDeviceTestServer(t, loadSpecFixture(t), []string{
`{"homelist":[{"id":"10","uid":1}]}`,
`{"device_info":[{"did":"a","name":"Lamp","model":"test.light.v1"}],"has_more":false}`,
`[{"did":"a","siid":7,"piid":8,"value":true,"code":0}]`,
@@ -213,9 +214,9 @@ func TestPythonDeviceInfoCacheSupportsOperations(t *testing.T) {
t.Fatal(err)
}
for index, want := range []map[string]any{
{"siid": float64(7), "piid": float64(8)},
{"siid": float64(7), "piid": float64(8)},
{"siid": float64(9), "aiid": float64(10)},
{"siid": float64(2), "piid": float64(1)},
{"siid": float64(2), "piid": float64(1)},
{"siid": float64(2), "aiid": float64(1)},
} {
params := testServer.requests[index+2]["params"]
var request map[string]any
@@ -230,8 +231,68 @@ func TestPythonDeviceInfoCacheSupportsOperations(t *testing.T) {
}
}
}
if testServer.specCalls != 0 {
t.Fatalf("spec calls = %d, want 0", testServer.specCalls)
if testServer.specCalls != 1 {
t.Fatalf("spec calls = %d, want 1", testServer.specCalls)
}
}
func TestOldDeviceInfoCacheRefreshesActionInputs(t *testing.T) {
for _, version := range []int{0, 1} {
t.Run(fmt.Sprintf("version %d", version), func(t *testing.T) {
testServer := newDeviceTestServer(t, loadSpecFixture(t), nil)
cacheDir := t.TempDir()
cache := fmt.Sprintf(`{
"version":%d,
"name":"Old Lamp",
"model":"test.light.v1",
"properties":[{"name":"power","type":"bool","rw":"rw","siid":2,"piid":1}],
"actions":[{"name":"toggle","siid":2,"aiid":1}]
}`, version)
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 != 1 || len(info.Actions) == 0 || len(info.Actions[0].Inputs) != 2 {
t.Fatalf("GetDeviceInfo() = %#v, %v, calls=%d", info, err, testServer.specCalls)
}
})
}
}
func TestVersion2DeviceInfoCacheTrustsActionWithoutInputs(t *testing.T) {
testServer := newDeviceTestServer(t, nil, nil)
cacheDir := t.TempDir()
const cache = `{
"version":2,
"name":"Cached Lamp",
"model":"test.light.v1",
"properties":[{"name":"power","type":"bool","rw":"rw","siid":2,"piid":1}],
"actions":[{"name":"toggle","siid":2,"aiid":1}]
}`
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 != 0 || len(info.Actions) != 1 || len(info.Actions[0].Inputs) != 0 {
t.Fatalf("GetDeviceInfo() = %#v, %v, calls=%d", info, err, testServer.specCalls)
}
}
func TestStaleDeviceInfoCacheRefreshFailureIncludesBothErrors(t *testing.T) {
testServer := newDeviceTestServer(t, []byte("unavailable"), nil)
testServer.status = http.StatusServiceUnavailable
cacheDir := t.TempDir()
cachePath := filepath.Join(cacheDir, "test.light.v1.json")
const cache = `{"version":1,"model":"test.light.v1","properties":[],"actions":[]}`
if err := os.WriteFile(cachePath, []byte(cache), 0o600); err != nil {
t.Fatal(err)
}
_, err := GetDeviceInfo(context.Background(), testServer.server.Client(), "test.light.v1", cacheDir)
if err == nil || !strings.Contains(err.Error(), "cache stale") || !strings.Contains(err.Error(), "refresh failed") || !strings.Contains(err.Error(), "503") {
t.Fatalf("error = %v", err)
}
}
@@ -270,6 +331,7 @@ func TestInvalidDeviceInfoCacheRefreshesAndOverwrites(t *testing.T) {
func TestDecodeDeviceInfoRejectsNonexistentActionInput(t *testing.T) {
const cache = `{
"version":2,
"model":"test.light.v1",
"properties":[{"name":"power","description":"Power","type":"bool","rw":"rw","siid":2,"piid":1}],
"actions":[{"name":"toggle","siid":2,"aiid":1,"inputs":[{"name":"missing","type":"bool","rw":"rw","siid":2,"piid":99}]}]
@@ -282,6 +344,7 @@ func TestDecodeDeviceInfoRejectsNonexistentActionInput(t *testing.T) {
func TestDecodeDeviceInfoCanonicalizesActionInputMetadata(t *testing.T) {
const cache = `{
"version":2,
"model":"test.light.v1",
"properties":[{"name":"power","description":"Power","type":"bool","rw":"rw","value-list":[{"value":0,"description":"Off"},{"value":1,"description":"On"}],"siid":2,"piid":1}],
"actions":[{"name":"toggle","siid":2,"aiid":1,"inputs":[{"name":"forged","description":"Forged","type":"string","rw":"w","range":[1,9,1],"siid":2,"piid":1}]}]