Files
MyGoNavi/internal/redis/redis_impl_test.go
Syngnat e3515b9eb2 🐛 fix(windows): 修复闪退与驱动代理安装失败
- 修复 WebView2 zoom factor 跨线程调用风险,切回窗口线程执行并增加 recover 与超时保护
- 完善 Redis 命令结果 JSON-safe 兜底,避免复杂返回值格式化触发程序崩溃
- 调整 Windows driver-agent 校验逻辑,仅读取 PE Machine 字段判断架构兼容性
- 避免 COFF string table EOF 被误判为无效 Windows 可执行文件,修复驱动在线安装和本地导入失败
- 补充窗口缩放、Redis 返回值和驱动代理 PE 校验回归测试
2026-05-18 10:28:18 +08:00

383 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}