Files
MyGoNavi/internal/app/methods_redis_test.go
Syngnat 22e4299d3e 🐛 fix(redis): 修正 hash 字段删除参数序列化错误
- 前端统一按数组传递 hash 字段删除参数
- 后端兼容单字符串与数组两种删除入参
- 补充 Redis hash 字段删除回归测试

Fixes #343
2026-04-17 12:45:21 +08:00

513 lines
16 KiB
Go

package app
import (
"errors"
"testing"
"GoNavi-Wails/internal/connection"
redislib "GoNavi-Wails/internal/redis"
)
type capturingRedisClient struct {
connectConfig connection.ConnectionConfig
deletedHashKey string
deletedHashFields []string
}
func (c *capturingRedisClient) Connect(config connection.ConnectionConfig) error {
c.connectConfig = config
return nil
}
func (c *capturingRedisClient) Close() error { return nil }
func (c *capturingRedisClient) Ping() error { return nil }
func (c *capturingRedisClient) ScanKeys(pattern string, cursor uint64, count int64) (*redislib.RedisScanResult, error) {
return &redislib.RedisScanResult{}, nil
}
func (c *capturingRedisClient) GetKeyType(key string) (string, error) { return "", nil }
func (c *capturingRedisClient) GetTTL(key string) (int64, error) { return 0, nil }
func (c *capturingRedisClient) SetTTL(key string, ttl int64) error { return nil }
func (c *capturingRedisClient) DeleteKeys(keys []string) (int64, error) { return 0, nil }
func (c *capturingRedisClient) RenameKey(oldKey, newKey string) error { return nil }
func (c *capturingRedisClient) KeyExists(key string) (bool, error) { return false, nil }
func (c *capturingRedisClient) GetValue(key string) (*redislib.RedisValue, error) {
return &redislib.RedisValue{}, nil
}
func (c *capturingRedisClient) GetString(key string) (string, error) { return "", nil }
func (c *capturingRedisClient) SetString(key, value string, ttl int64) error { return nil }
func (c *capturingRedisClient) GetHash(key string) (map[string]string, error) {
return map[string]string{}, nil
}
func (c *capturingRedisClient) SetHashField(key, field, value string) error { return nil }
func (c *capturingRedisClient) DeleteHashField(key string, fields ...string) error {
c.deletedHashKey = key
c.deletedHashFields = append([]string(nil), fields...)
return nil
}
func (c *capturingRedisClient) GetList(key string, start, stop int64) ([]string, error) {
return nil, nil
}
func (c *capturingRedisClient) ListPush(key string, values ...string) error { return nil }
func (c *capturingRedisClient) ListSet(key string, index int64, value string) error { return nil }
func (c *capturingRedisClient) GetSet(key string) ([]string, error) { return nil, nil }
func (c *capturingRedisClient) SetAdd(key string, members ...string) error { return nil }
func (c *capturingRedisClient) SetRemove(key string, members ...string) error { return nil }
func (c *capturingRedisClient) GetZSet(key string, start, stop int64) ([]redislib.ZSetMember, error) {
return nil, nil
}
func (c *capturingRedisClient) ZSetAdd(key string, members ...redislib.ZSetMember) error { return nil }
func (c *capturingRedisClient) ZSetRemove(key string, members ...string) error { return nil }
func (c *capturingRedisClient) GetStream(key, start, stop string, count int64) ([]redislib.StreamEntry, error) {
return nil, nil
}
func (c *capturingRedisClient) StreamAdd(key string, fields map[string]string, id string) (string, error) {
return "", nil
}
func (c *capturingRedisClient) StreamDelete(key string, ids ...string) (int64, error) { return 0, nil }
func (c *capturingRedisClient) ExecuteCommand(args []string) (interface{}, error) { return nil, nil }
func (c *capturingRedisClient) GetServerInfo() (map[string]string, error) {
return map[string]string{}, nil
}
func (c *capturingRedisClient) GetDatabases() ([]redislib.RedisDBInfo, error) { return nil, nil }
func (c *capturingRedisClient) SelectDB(index int) error { return nil }
func (c *capturingRedisClient) GetCurrentDB() int { return 0 }
func (c *capturingRedisClient) FlushDB() error { return nil }
type scriptedRedisClient struct {
capturingRedisClient
connectErr error
connectCalls *[]connection.ConnectionConfig
}
func (c *scriptedRedisClient) Connect(config connection.ConnectionConfig) error {
c.connectConfig = config
if c.connectCalls != nil {
*c.connectCalls = append(*c.connectCalls, config)
}
return c.connectErr
}
func TestRedisConnectResolvesSavedSecretsByConnectionID(t *testing.T) {
testCases := []struct {
name string
savedConfig connection.ConnectionConfig
runtimeConfig connection.ConnectionConfig
assertResolved func(t *testing.T, got connection.ConnectionConfig)
}{
{
name: "redis and ssh secrets",
savedConfig: connection.ConnectionConfig{
ID: "redis-1",
Type: "redis",
Host: "redis.local",
Port: 6379,
Password: "redis-secret",
UseSSH: true,
SSH: connection.SSHConfig{
Host: "ssh.local",
Port: 22,
User: "ops",
Password: "ssh-secret",
},
},
runtimeConfig: connection.ConnectionConfig{
ID: "redis-1",
Type: "redis",
Host: "redis.local",
Port: 6379,
UseSSH: true,
SSH: connection.SSHConfig{
Host: "ssh.local",
Port: 22,
User: "ops",
},
},
assertResolved: func(t *testing.T, got connection.ConnectionConfig) {
t.Helper()
if got.Password != "redis-secret" {
t.Fatalf("expected RedisConnect to resolve saved Redis password, got %q", got.Password)
}
if got.SSH.Password != "ssh-secret" {
t.Fatalf("expected RedisConnect to resolve saved SSH password, got %q", got.SSH.Password)
}
},
},
{
name: "proxy secret",
savedConfig: connection.ConnectionConfig{
ID: "redis-1",
Type: "redis",
Host: "redis.local",
Port: 6379,
Password: "redis-secret",
UseProxy: true,
Proxy: connection.ProxyConfig{
Type: "http",
Host: "proxy.local",
Port: 8080,
User: "proxy-user",
Password: "proxy-secret",
},
},
runtimeConfig: connection.ConnectionConfig{
ID: "redis-1",
Type: "redis",
Host: "redis.local",
Port: 6379,
UseProxy: true,
Proxy: connection.ProxyConfig{
Type: "http",
Host: "proxy.local",
Port: 8080,
User: "proxy-user",
},
},
assertResolved: func(t *testing.T, got connection.ConnectionConfig) {
t.Helper()
if got.Password != "redis-secret" {
t.Fatalf("expected RedisConnect to resolve saved Redis password, got %q", got.Password)
}
if got.Proxy.Password != "proxy-secret" {
t.Fatalf("expected RedisConnect to resolve saved proxy password, got %q", got.Proxy.Password)
}
},
},
{
name: "http tunnel secret",
savedConfig: connection.ConnectionConfig{
ID: "redis-1",
Type: "redis",
Host: "redis.local",
Port: 6379,
Password: "redis-secret",
UseHTTPTunnel: true,
HTTPTunnel: connection.HTTPTunnelConfig{
Host: "tunnel.local",
Port: 8443,
User: "tunnel-user",
Password: "tunnel-secret",
},
},
runtimeConfig: connection.ConnectionConfig{
ID: "redis-1",
Type: "redis",
Host: "redis.local",
Port: 6379,
UseHTTPTunnel: true,
HTTPTunnel: connection.HTTPTunnelConfig{
Host: "tunnel.local",
Port: 8443,
User: "tunnel-user",
},
},
assertResolved: func(t *testing.T, got connection.ConnectionConfig) {
t.Helper()
if got.Password != "redis-secret" {
t.Fatalf("expected RedisConnect to resolve saved Redis password, got %q", got.Password)
}
if got.HTTPTunnel.Password != "tunnel-secret" {
t.Fatalf("expected RedisConnect to resolve saved HTTP tunnel password, got %q", got.HTTPTunnel.Password)
}
},
},
{
name: "explicit redis username from uri is preserved even when it is root",
savedConfig: connection.ConnectionConfig{
ID: "redis-1",
Type: "redis",
Host: "redis.local",
Port: 6379,
User: "root",
Password: "redis-secret",
URI: "redis://root:redis-secret@redis.local:6379/0",
},
runtimeConfig: connection.ConnectionConfig{
ID: "redis-1",
Type: "redis",
Host: "redis.local",
Port: 6379,
User: "root",
},
assertResolved: func(t *testing.T, got connection.ConnectionConfig) {
t.Helper()
if got.User != "root" {
t.Fatalf("expected RedisConnect to preserve explicit uri user root, got %q", got.User)
}
if got.URI != "redis://root:redis-secret@redis.local:6379/0" {
t.Fatalf("expected RedisConnect to restore saved redis uri, got %q", got.URI)
}
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
_, err := app.SaveConnection(connection.SavedConnectionInput{
ID: "redis-1",
Name: "Redis Saved",
Config: testCase.savedConfig,
})
if err != nil {
t.Fatalf("SaveConnection returned error: %v", err)
}
CloseAllRedisClients()
client := &capturingRedisClient{}
originalNewRedisClientFunc := newRedisClientFunc
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
defer func() {
newRedisClientFunc = originalNewRedisClientFunc
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
CloseAllRedisClients()
}()
newRedisClientFunc = func() redislib.RedisClient {
return client
}
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
return raw, nil
}
result := app.RedisConnect(testCase.runtimeConfig)
if !result.Success {
t.Fatalf("RedisConnect returned failure: %+v", result)
}
testCase.assertResolved(t, client.connectConfig)
})
}
}
func TestRedisConnectPreservesExplicitRootUserWithoutURIWhenConnectSucceeds(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
_, err := app.SaveConnection(connection.SavedConnectionInput{
ID: "redis-1",
Name: "Redis Saved",
Config: connection.ConnectionConfig{
ID: "redis-1",
Type: "redis",
Host: "redis.local",
Port: 6379,
User: "root",
Password: "redis-secret",
},
})
if err != nil {
t.Fatalf("SaveConnection returned error: %v", err)
}
CloseAllRedisClients()
connectCalls := make([]connection.ConnectionConfig, 0, 1)
client := &scriptedRedisClient{connectCalls: &connectCalls}
originalNewRedisClientFunc := newRedisClientFunc
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
defer func() {
newRedisClientFunc = originalNewRedisClientFunc
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
CloseAllRedisClients()
}()
newRedisClientFunc = func() redislib.RedisClient {
return client
}
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
return raw, nil
}
result := app.RedisConnect(connection.ConnectionConfig{
ID: "redis-1",
Type: "redis",
Host: "redis.local",
Port: 6379,
User: "root",
})
if !result.Success {
t.Fatalf("RedisConnect returned failure: %+v", result)
}
if len(connectCalls) != 1 {
t.Fatalf("expected exactly one Redis connect attempt, got %d", len(connectCalls))
}
if connectCalls[0].User != "root" {
t.Fatalf("expected RedisConnect to preserve explicit root user when connect succeeds, got %q", connectCalls[0].User)
}
}
func TestRedisConnectRetriesLegacyDefaultRootUserWithoutUsernameAfterAuthFailure(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
_, err := app.SaveConnection(connection.SavedConnectionInput{
ID: "redis-1",
Name: "Redis Saved",
Config: connection.ConnectionConfig{
ID: "redis-1",
Type: "redis",
Host: "redis.local",
Port: 6379,
User: "root",
Password: "redis-secret",
},
})
if err != nil {
t.Fatalf("SaveConnection returned error: %v", err)
}
CloseAllRedisClients()
connectCalls := make([]connection.ConnectionConfig, 0, 2)
clients := []redislib.RedisClient{
&scriptedRedisClient{
connectErr: errors.New("WRONGPASS invalid username-password pair"),
connectCalls: &connectCalls,
},
&scriptedRedisClient{
connectCalls: &connectCalls,
},
}
clientIndex := 0
originalNewRedisClientFunc := newRedisClientFunc
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
defer func() {
newRedisClientFunc = originalNewRedisClientFunc
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
CloseAllRedisClients()
}()
newRedisClientFunc = func() redislib.RedisClient {
if clientIndex >= len(clients) {
t.Fatalf("unexpected Redis client allocation #%d", clientIndex+1)
}
client := clients[clientIndex]
clientIndex++
return client
}
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
return raw, nil
}
result := app.RedisConnect(connection.ConnectionConfig{
ID: "redis-1",
Type: "redis",
Host: "redis.local",
Port: 6379,
User: "root",
})
if !result.Success {
t.Fatalf("RedisConnect returned failure after fallback: %+v", result)
}
if len(connectCalls) != 2 {
t.Fatalf("expected RedisConnect to retry exactly once after auth failure, got %d attempts", len(connectCalls))
}
if connectCalls[0].User != "root" {
t.Fatalf("expected first Redis connect attempt to keep root user, got %q", connectCalls[0].User)
}
if connectCalls[1].User != "" {
t.Fatalf("expected fallback Redis connect attempt to clear legacy root user, got %q", connectCalls[1].User)
}
}
func TestRedisDeleteHashFieldAcceptsSingleStringField(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
CloseAllRedisClients()
client := &capturingRedisClient{}
originalNewRedisClientFunc := newRedisClientFunc
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
defer func() {
newRedisClientFunc = originalNewRedisClientFunc
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
CloseAllRedisClients()
}()
newRedisClientFunc = func() redislib.RedisClient {
return client
}
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
return raw, nil
}
result := app.RedisDeleteHashField(connection.ConnectionConfig{
Type: "redis",
Host: "redis.local",
Port: 6379,
}, "profile", "nickname")
if !result.Success {
t.Fatalf("RedisDeleteHashField returned failure: %+v", result)
}
if client.deletedHashKey != "profile" {
t.Fatalf("expected hash key profile, got %q", client.deletedHashKey)
}
if len(client.deletedHashFields) != 1 || client.deletedHashFields[0] != "nickname" {
t.Fatalf("expected one deleted hash field nickname, got %v", client.deletedHashFields)
}
}
func TestRedisDeleteHashFieldAcceptsStringSlice(t *testing.T) {
app := NewAppWithSecretStore(newFakeAppSecretStore())
app.configDir = t.TempDir()
CloseAllRedisClients()
client := &capturingRedisClient{}
originalNewRedisClientFunc := newRedisClientFunc
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
defer func() {
newRedisClientFunc = originalNewRedisClientFunc
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
CloseAllRedisClients()
}()
newRedisClientFunc = func() redislib.RedisClient {
return client
}
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
return raw, nil
}
result := app.RedisDeleteHashField(connection.ConnectionConfig{
Type: "redis",
Host: "redis.local",
Port: 6379,
}, "profile", []string{"nickname", "avatar"})
if !result.Success {
t.Fatalf("RedisDeleteHashField returned failure: %+v", result)
}
if client.deletedHashKey != "profile" {
t.Fatalf("expected hash key profile, got %q", client.deletedHashKey)
}
if len(client.deletedHashFields) != 2 || client.deletedHashFields[0] != "nickname" || client.deletedHashFields[1] != "avatar" {
t.Fatalf("unexpected deleted hash fields: %v", client.deletedHashFields)
}
}