mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-31 13:39:48 +08:00
- 修复 WebView2 zoom factor 跨线程调用风险,切回窗口线程执行并增加 recover 与超时保护 - 完善 Redis 命令结果 JSON-safe 兜底,避免复杂返回值格式化触发程序崩溃 - 调整 Windows driver-agent 校验逻辑,仅读取 PE Machine 字段判断架构兼容性 - 避免 COFF string table EOF 被误判为无效 Windows 可执行文件,修复驱动在线安装和本地导入失败 - 补充窗口缩放、Redis 返回值和驱动代理 PE 校验回归测试
383 lines
11 KiB
Go
383 lines
11 KiB
Go
package redis
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"math"
|
||
"math/big"
|
||
"sort"
|
||
"testing"
|
||
)
|
||
|
||
// 回归保护:HGETALL 在 RESP3 下返回 map[interface{}]interface{}(go-redis v9 默认 RESP3),
|
||
// 这种类型 encoding/json 无法序列化,原值穿透到 Wails RPC 会让 Windows 进程退出(用户感知闪退)。
|
||
// formatCommandResult 必须把 map 平展成交错 [k1, v1, k2, v2, ...] array,前端按 array 渲染。
|
||
func TestFormatCommandResultFlattensRESP3MapForJSONMarshal(t *testing.T) {
|
||
input := map[interface{}]interface{}{
|
||
"name": "alice",
|
||
"age": "30",
|
||
}
|
||
got := formatCommandResult(input)
|
||
|
||
arr, ok := got.([]interface{})
|
||
if !ok {
|
||
t.Fatalf("expected flattened []interface{}, got %T (%#v)", got, got)
|
||
}
|
||
if len(arr) != 4 {
|
||
t.Fatalf("expected 4 elements (2 pairs flattened), got %d: %#v", len(arr), arr)
|
||
}
|
||
|
||
// 平展后必须能被 encoding/json 序列化——这是修复的根本目的
|
||
if _, err := json.Marshal(arr); err != nil {
|
||
t.Fatalf("flattened result must be JSON-marshalable, got error: %v", err)
|
||
}
|
||
|
||
// 验证 key+value 都保留下来了(顺序由 map 遍历决定,不强断言顺序)
|
||
collected := make([]string, 0, 4)
|
||
for _, item := range arr {
|
||
s, ok := item.(string)
|
||
if !ok {
|
||
t.Fatalf("expected string element, got %T", item)
|
||
}
|
||
collected = append(collected, s)
|
||
}
|
||
sort.Strings(collected)
|
||
want := []string{"30", "age", "alice", "name"}
|
||
for i, w := range want {
|
||
if collected[i] != w {
|
||
t.Fatalf("flattened content mismatch at %d: got %q want %q (all=%v)", i, collected[i], w, collected)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 嵌套 array 内含 map[interface{}]interface{} 也要递归平展,
|
||
// 保证 XINFO STREAM 等返回嵌套结构的命令不会卡在序列化阶段。
|
||
func TestFormatCommandResultRecursivelyFlattensNestedMap(t *testing.T) {
|
||
input := []interface{}{
|
||
"stream-id",
|
||
map[interface{}]interface{}{"k": "v"},
|
||
}
|
||
got := formatCommandResult(input)
|
||
arr, ok := got.([]interface{})
|
||
if !ok || len(arr) != 2 {
|
||
t.Fatalf("expected []interface{} of length 2, got %#v", got)
|
||
}
|
||
nested, ok := arr[1].([]interface{})
|
||
if !ok {
|
||
t.Fatalf("expected nested map to be flattened to []interface{}, got %T", arr[1])
|
||
}
|
||
if _, err := json.Marshal(arr); err != nil {
|
||
t.Fatalf("recursively flattened result must be JSON-marshalable, got error: %v", err)
|
||
}
|
||
_ = nested
|
||
}
|
||
|
||
// 已经是 string-key 简单类型的命令(HGET、SET 之类)不应被改变。
|
||
func TestFormatCommandResultPreservesScalarAndByteSlice(t *testing.T) {
|
||
if got := formatCommandResult("ok"); got != "ok" {
|
||
t.Fatalf("string scalar should pass through, got %v", got)
|
||
}
|
||
if got := formatCommandResult([]byte("ok")); got != "ok" {
|
||
t.Fatalf("[]byte should be converted to string, got %v", got)
|
||
}
|
||
if got := formatCommandResult(int64(42)); got != int64(42) {
|
||
t.Fatalf("int64 scalar should pass through, got %v", got)
|
||
}
|
||
}
|
||
|
||
func TestFormatCommandResultRecursivelyFormatsStringKeyMapValues(t *testing.T) {
|
||
input := map[string]interface{}{
|
||
"nestedMap": map[interface{}]interface{}{"k": "v"},
|
||
"bytes": []byte("ok"),
|
||
}
|
||
|
||
got := formatCommandResult(input)
|
||
formatted, ok := got.(map[string]interface{})
|
||
if !ok {
|
||
t.Fatalf("expected map[string]interface{}, got %T (%#v)", got, got)
|
||
}
|
||
if formatted["bytes"] != "ok" {
|
||
t.Fatalf("expected []byte value converted to string, got %#v", formatted["bytes"])
|
||
}
|
||
if _, ok := formatted["nestedMap"].([]interface{}); !ok {
|
||
t.Fatalf("expected nested RESP3 map to be flattened, got %T", formatted["nestedMap"])
|
||
}
|
||
if _, err := json.Marshal(formatted); err != nil {
|
||
t.Fatalf("formatted string-key map must be JSON-marshalable, got error: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestFormatCommandResultFormatsJSONUnsupportedScalars(t *testing.T) {
|
||
input := []interface{}{
|
||
math.Inf(1),
|
||
math.Inf(-1),
|
||
math.NaN(),
|
||
big.NewInt(1234567890123456789),
|
||
errors.New("redis nested error"),
|
||
}
|
||
|
||
got := formatCommandResult(input)
|
||
arr, ok := got.([]interface{})
|
||
if !ok || len(arr) != len(input) {
|
||
t.Fatalf("expected formatted array of length %d, got %#v", len(input), got)
|
||
}
|
||
for i, item := range arr {
|
||
if _, ok := item.(string); !ok {
|
||
t.Fatalf("expected item %d to be string after formatting, got %T (%#v)", i, item, item)
|
||
}
|
||
}
|
||
if _, err := json.Marshal(arr); err != nil {
|
||
t.Fatalf("formatted unsupported scalars must be JSON-marshalable, got error: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestFormatCommandResultFormatsGenericMapsAndSlices(t *testing.T) {
|
||
input := map[int][]byte{
|
||
1: []byte("one"),
|
||
}
|
||
|
||
got := formatCommandResult(input)
|
||
arr, ok := got.([]interface{})
|
||
if !ok || len(arr) != 2 {
|
||
t.Fatalf("expected generic non-string map to be flattened into 2 elements, got %#v", got)
|
||
}
|
||
if _, err := json.Marshal(arr); err != nil {
|
||
t.Fatalf("formatted generic map must be JSON-marshalable, got error: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestSanitizeRedisPassword(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
input string
|
||
expected string
|
||
}{
|
||
{
|
||
name: "empty password",
|
||
input: "",
|
||
expected: "",
|
||
},
|
||
{
|
||
name: "plain password without special chars",
|
||
input: "mypassword123",
|
||
expected: "mypassword123",
|
||
},
|
||
{
|
||
name: "password with @ not encoded",
|
||
input: "p@ssword",
|
||
expected: "p@ssword",
|
||
},
|
||
{
|
||
name: "password with @ URL-encoded as %40",
|
||
input: "p%40ssword",
|
||
expected: "p@ssword",
|
||
},
|
||
{
|
||
name: "password with multiple encoded chars",
|
||
input: "p%40ss%23word",
|
||
expected: "p@ss#word",
|
||
},
|
||
{
|
||
name: "password with + encoded as %2B",
|
||
input: "p%2Bss",
|
||
expected: "p+ss",
|
||
},
|
||
{
|
||
name: "password that is purely encoded",
|
||
input: "%40%23%24",
|
||
expected: "@#$",
|
||
},
|
||
{
|
||
name: "password with invalid percent encoding",
|
||
input: "p%ZZssword",
|
||
expected: "p%ZZssword",
|
||
},
|
||
{
|
||
name: "password with trailing percent",
|
||
input: "password%",
|
||
expected: "password%",
|
||
},
|
||
{
|
||
name: "password with literal percent not encoding anything",
|
||
input: "100%safe",
|
||
expected: "100%safe",
|
||
},
|
||
{
|
||
name: "password with space encoded as %20",
|
||
input: "my%20pass",
|
||
expected: "my pass",
|
||
},
|
||
{
|
||
name: "complex password with mixed content",
|
||
input: "P%40ss%23w0rd!",
|
||
expected: "P@ss#w0rd!",
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
result := sanitizeRedisPassword(tt.input)
|
||
if result != tt.expected {
|
||
t.Errorf("sanitizeRedisPassword(%q) = %q, want %q", tt.input, result, tt.expected)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestIsRedisKeyGone(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
keyType string
|
||
ttl int64
|
||
want bool
|
||
}{
|
||
{name: "type none", keyType: "none", ttl: -2, want: true},
|
||
{name: "type none without ttl", keyType: "none", ttl: -1, want: true},
|
||
{name: "missing by ttl", keyType: "string", ttl: -2, want: true},
|
||
{name: "normal string", keyType: "string", ttl: 30, want: false},
|
||
{name: "permanent hash", keyType: "hash", ttl: -1, want: false},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
if got := isRedisKeyGone(tt.keyType, tt.ttl); got != tt.want {
|
||
t.Fatalf("isRedisKeyGone(%q, %d)=%v, want %v", tt.keyType, tt.ttl, got, tt.want)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestNormalizeRedisGetValueError(t *testing.T) {
|
||
err := normalizeRedisGetValueError("none", -2)
|
||
if !errors.Is(err, ErrRedisKeyGone) {
|
||
t.Fatalf("expected ErrRedisKeyGone, got %v", err)
|
||
}
|
||
if err == nil || err.Error() != "Redis Key 不存在或已过期" {
|
||
t.Fatalf("unexpected error text: %v", err)
|
||
}
|
||
|
||
if normalizeRedisGetValueError("hash", -1) != nil {
|
||
t.Fatal("expected nil for supported existing key")
|
||
}
|
||
}
|
||
|
||
func TestRedisGlobPatternLiteralKey(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
pattern string
|
||
wantKey string
|
||
wantExact bool
|
||
}{
|
||
{name: "plain exact key", pattern: "Agent", wantKey: "Agent", wantExact: true},
|
||
{name: "escaped glob characters stay literal", pattern: `user:\*:\[id\]\?\\raw`, wantKey: `user:*:[id]?\raw`, wantExact: true},
|
||
{name: "fuzzy wildcard is not exact", pattern: "*[aA][gG][eE][nN][tT]*", wantExact: false},
|
||
{name: "unescaped suffix wildcard is not exact", pattern: "Agent*", wantExact: false},
|
||
{name: "unescaped single character wildcard is not exact", pattern: "Agent?", wantExact: false},
|
||
{name: "unescaped character class is not exact", pattern: "Agent[0-9]", wantExact: false},
|
||
{name: "empty pattern is not exact", pattern: "", wantExact: false},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
gotKey, gotExact := redisGlobPatternLiteralKey(tt.pattern)
|
||
if gotExact != tt.wantExact {
|
||
t.Fatalf("redisGlobPatternLiteralKey(%q) exact=%v, want %v", tt.pattern, gotExact, tt.wantExact)
|
||
}
|
||
if gotKey != tt.wantKey {
|
||
t.Fatalf("redisGlobPatternLiteralKey(%q) key=%q, want %q", tt.pattern, gotKey, tt.wantKey)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestRedisExactSearchPattern(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
literalKey string
|
||
wantExactKey string
|
||
wantNamespace string
|
||
}{
|
||
{
|
||
name: "plain namespace folder",
|
||
literalKey: "Agent",
|
||
wantExactKey: "Agent",
|
||
wantNamespace: "Agent:*",
|
||
},
|
||
{
|
||
name: "escaped namespace keeps glob chars literal",
|
||
literalKey: `user:*:[id]?\raw`,
|
||
wantExactKey: `user:*:[id]?\raw`,
|
||
wantNamespace: `user:\*:\[id\]\?\\raw:*`,
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
gotExactKey, gotNamespace := redisExactSearchPattern(tt.literalKey)
|
||
if gotExactKey != tt.wantExactKey {
|
||
t.Fatalf("redisExactSearchPattern(%q) exactKey=%q, want %q", tt.literalKey, gotExactKey, tt.wantExactKey)
|
||
}
|
||
if gotNamespace != tt.wantNamespace {
|
||
t.Fatalf("redisExactSearchPattern(%q) namespace=%q, want %q", tt.literalKey, gotNamespace, tt.wantNamespace)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestReadRedisHashEntriesWithFallbackUsesHScanWhenHGetAllForbidden(t *testing.T) {
|
||
scanCalls := 0
|
||
values, length, err := readRedisHashEntriesWithFallback(
|
||
func() (map[string]string, error) {
|
||
return nil, errors.New("ERR command 'HGETALL' not support for normal user")
|
||
},
|
||
func() (int64, error) {
|
||
return 2, nil
|
||
},
|
||
func(cursor uint64, count int64) ([]string, uint64, error) {
|
||
scanCalls++
|
||
if cursor != 0 {
|
||
t.Fatalf("expected first scan cursor to be 0, got %d", cursor)
|
||
}
|
||
if count <= 0 {
|
||
t.Fatalf("expected positive scan count, got %d", count)
|
||
}
|
||
return []string{"field-a", "value-a", "field-b", "value-b"}, 0, nil
|
||
},
|
||
)
|
||
if err != nil {
|
||
t.Fatalf("readRedisHashEntriesWithFallback() unexpected error: %v", err)
|
||
}
|
||
if scanCalls != 1 {
|
||
t.Fatalf("expected exactly one HSCAN fallback, got %d", scanCalls)
|
||
}
|
||
if length != 2 {
|
||
t.Fatalf("expected hash length 2, got %d", length)
|
||
}
|
||
if got := values["field-a"]; got != "value-a" {
|
||
t.Fatalf("expected field-a=value-a, got %q", got)
|
||
}
|
||
if got := values["field-b"]; got != "value-b" {
|
||
t.Fatalf("expected field-b=value-b, got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestReadRedisHashEntriesWithFallbackReturnsOriginalErrorForNonPermissionFailure(t *testing.T) {
|
||
expectedErr := errors.New("ERR wrong type")
|
||
_, _, err := readRedisHashEntriesWithFallback(
|
||
func() (map[string]string, error) {
|
||
return nil, expectedErr
|
||
},
|
||
func() (int64, error) {
|
||
t.Fatal("expected HLEN not to run for non-permission failure")
|
||
return 0, nil
|
||
},
|
||
func(cursor uint64, count int64) ([]string, uint64, error) {
|
||
t.Fatal("expected HSCAN not to run for non-permission failure")
|
||
return nil, 0, nil
|
||
},
|
||
)
|
||
if !errors.Is(err, expectedErr) {
|
||
t.Fatalf("expected original error %v, got %v", expectedErr, err)
|
||
}
|
||
}
|