mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-13 00:13:33 +08:00
- 新增 Redis 导入预览与按 key 勾选导入能力 - 后端支持导入文件预读、选中 key 过滤与本地化错误提示 - 打开数据库节点时同步展示加载态并补齐前后端回归测试 Fixes #607 Fixes #609
1046 lines
32 KiB
Go
1046 lines
32 KiB
Go
package app
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"testing"
|
|
|
|
"GoNavi-Wails/internal/connection"
|
|
redislib "GoNavi-Wails/internal/redis"
|
|
)
|
|
|
|
type capturingRedisClient struct {
|
|
connectConfig connection.ConnectionConfig
|
|
deletedHashKey string
|
|
deletedHashFields []string
|
|
closed int
|
|
closeErr error
|
|
}
|
|
|
|
func (c *capturingRedisClient) Connect(config connection.ConnectionConfig) error {
|
|
c.connectConfig = config
|
|
return nil
|
|
}
|
|
|
|
func (c *capturingRedisClient) Close() error {
|
|
c.closed++
|
|
return c.closeErr
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
type redisTransferTestClient struct {
|
|
capturingRedisClient
|
|
scanPages []*redislib.RedisScanResult
|
|
scanCallCount int
|
|
keyTypes map[string]string
|
|
ttls map[string]int64
|
|
stringValues map[string]string
|
|
hashValues map[string]map[string]string
|
|
listValues map[string][]string
|
|
setValues map[string][]string
|
|
zsetValues map[string][]redislib.ZSetMember
|
|
streamValues map[string][]redislib.StreamEntry
|
|
existingKeys map[string]bool
|
|
deletedKeys []string
|
|
setStringCalls []struct {
|
|
key string
|
|
value string
|
|
ttl int64
|
|
}
|
|
setTTLCalls []struct {
|
|
key string
|
|
ttl int64
|
|
}
|
|
setHashFieldCalls []struct {
|
|
key string
|
|
field string
|
|
value string
|
|
}
|
|
listPushCalls []struct {
|
|
key string
|
|
values []string
|
|
}
|
|
setAddCalls []struct {
|
|
key string
|
|
members []string
|
|
}
|
|
zsetAddCalls []struct {
|
|
key string
|
|
members []redislib.ZSetMember
|
|
}
|
|
streamAddCalls []struct {
|
|
key string
|
|
fields map[string]string
|
|
id string
|
|
}
|
|
}
|
|
|
|
func (c *redisTransferTestClient) ScanKeys(pattern string, cursor uint64, count int64) (*redislib.RedisScanResult, error) {
|
|
if c.scanCallCount >= len(c.scanPages) {
|
|
return &redislib.RedisScanResult{Cursor: "0"}, nil
|
|
}
|
|
page := c.scanPages[c.scanCallCount]
|
|
c.scanCallCount++
|
|
return page, nil
|
|
}
|
|
|
|
func (c *redisTransferTestClient) GetKeyType(key string) (string, error) {
|
|
if value, ok := c.keyTypes[key]; ok {
|
|
return value, nil
|
|
}
|
|
return "", fmt.Errorf("missing key type for %s", key)
|
|
}
|
|
|
|
func (c *redisTransferTestClient) GetTTL(key string) (int64, error) {
|
|
if value, ok := c.ttls[key]; ok {
|
|
return value, nil
|
|
}
|
|
return -1, nil
|
|
}
|
|
|
|
func (c *redisTransferTestClient) DeleteKeys(keys []string) (int64, error) {
|
|
c.deletedKeys = append(c.deletedKeys, keys...)
|
|
for _, key := range keys {
|
|
if c.existingKeys != nil {
|
|
delete(c.existingKeys, key)
|
|
}
|
|
}
|
|
return int64(len(keys)), nil
|
|
}
|
|
|
|
func (c *redisTransferTestClient) KeyExists(key string) (bool, error) {
|
|
if c.existingKeys == nil {
|
|
return false, nil
|
|
}
|
|
return c.existingKeys[key], nil
|
|
}
|
|
|
|
func (c *redisTransferTestClient) GetString(key string) (string, error) {
|
|
if value, ok := c.stringValues[key]; ok {
|
|
return value, nil
|
|
}
|
|
return "", fmt.Errorf("missing string value for %s", key)
|
|
}
|
|
|
|
func (c *redisTransferTestClient) SetString(key, value string, ttl int64) error {
|
|
c.setStringCalls = append(c.setStringCalls, struct {
|
|
key string
|
|
value string
|
|
ttl int64
|
|
}{key: key, value: value, ttl: ttl})
|
|
if c.existingKeys == nil {
|
|
c.existingKeys = make(map[string]bool)
|
|
}
|
|
c.existingKeys[key] = true
|
|
return nil
|
|
}
|
|
|
|
func (c *redisTransferTestClient) GetHash(key string) (map[string]string, error) {
|
|
if value, ok := c.hashValues[key]; ok {
|
|
cloned := make(map[string]string, len(value))
|
|
for field, item := range value {
|
|
cloned[field] = item
|
|
}
|
|
return cloned, nil
|
|
}
|
|
return nil, fmt.Errorf("missing hash value for %s", key)
|
|
}
|
|
|
|
func (c *redisTransferTestClient) SetHashField(key, field, value string) error {
|
|
c.setHashFieldCalls = append(c.setHashFieldCalls, struct {
|
|
key string
|
|
field string
|
|
value string
|
|
}{key: key, field: field, value: value})
|
|
if c.existingKeys == nil {
|
|
c.existingKeys = make(map[string]bool)
|
|
}
|
|
c.existingKeys[key] = true
|
|
return nil
|
|
}
|
|
|
|
func (c *redisTransferTestClient) GetList(key string, start, stop int64) ([]string, error) {
|
|
if value, ok := c.listValues[key]; ok {
|
|
return append([]string(nil), value...), nil
|
|
}
|
|
return nil, fmt.Errorf("missing list value for %s", key)
|
|
}
|
|
|
|
func (c *redisTransferTestClient) ListPush(key string, values ...string) error {
|
|
c.listPushCalls = append(c.listPushCalls, struct {
|
|
key string
|
|
values []string
|
|
}{key: key, values: append([]string(nil), values...)})
|
|
if c.existingKeys == nil {
|
|
c.existingKeys = make(map[string]bool)
|
|
}
|
|
c.existingKeys[key] = true
|
|
return nil
|
|
}
|
|
|
|
func (c *redisTransferTestClient) GetSet(key string) ([]string, error) {
|
|
if value, ok := c.setValues[key]; ok {
|
|
return append([]string(nil), value...), nil
|
|
}
|
|
return nil, fmt.Errorf("missing set value for %s", key)
|
|
}
|
|
|
|
func (c *redisTransferTestClient) SetAdd(key string, members ...string) error {
|
|
c.setAddCalls = append(c.setAddCalls, struct {
|
|
key string
|
|
members []string
|
|
}{key: key, members: append([]string(nil), members...)})
|
|
if c.existingKeys == nil {
|
|
c.existingKeys = make(map[string]bool)
|
|
}
|
|
c.existingKeys[key] = true
|
|
return nil
|
|
}
|
|
|
|
func (c *redisTransferTestClient) GetZSet(key string, start, stop int64) ([]redislib.ZSetMember, error) {
|
|
if value, ok := c.zsetValues[key]; ok {
|
|
return append([]redislib.ZSetMember(nil), value...), nil
|
|
}
|
|
return nil, fmt.Errorf("missing zset value for %s", key)
|
|
}
|
|
|
|
func (c *redisTransferTestClient) ZSetAdd(key string, members ...redislib.ZSetMember) error {
|
|
c.zsetAddCalls = append(c.zsetAddCalls, struct {
|
|
key string
|
|
members []redislib.ZSetMember
|
|
}{key: key, members: append([]redislib.ZSetMember(nil), members...)})
|
|
if c.existingKeys == nil {
|
|
c.existingKeys = make(map[string]bool)
|
|
}
|
|
c.existingKeys[key] = true
|
|
return nil
|
|
}
|
|
|
|
func (c *redisTransferTestClient) GetStream(key, start, stop string, count int64) ([]redislib.StreamEntry, error) {
|
|
values, ok := c.streamValues[key]
|
|
if !ok {
|
|
return nil, fmt.Errorf("missing stream value for %s", key)
|
|
}
|
|
startIndex := 0
|
|
if start != "" && start != "-" {
|
|
for index, item := range values {
|
|
if item.ID == start {
|
|
startIndex = index
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if startIndex >= len(values) {
|
|
return []redislib.StreamEntry{}, nil
|
|
}
|
|
endIndex := len(values)
|
|
if count > 0 && startIndex+int(count) < endIndex {
|
|
endIndex = startIndex + int(count)
|
|
}
|
|
return append([]redislib.StreamEntry(nil), values[startIndex:endIndex]...), nil
|
|
}
|
|
|
|
func (c *redisTransferTestClient) StreamAdd(key string, fields map[string]string, id string) (string, error) {
|
|
cloned := make(map[string]string, len(fields))
|
|
for field, value := range fields {
|
|
cloned[field] = value
|
|
}
|
|
c.streamAddCalls = append(c.streamAddCalls, struct {
|
|
key string
|
|
fields map[string]string
|
|
id string
|
|
}{key: key, fields: cloned, id: id})
|
|
if c.existingKeys == nil {
|
|
c.existingKeys = make(map[string]bool)
|
|
}
|
|
c.existingKeys[key] = true
|
|
return id, nil
|
|
}
|
|
|
|
func (c *redisTransferTestClient) SetTTL(key string, ttl int64) error {
|
|
c.setTTLCalls = append(c.setTTLCalls, struct {
|
|
key string
|
|
ttl int64
|
|
}{key: key, ttl: ttl})
|
|
return nil
|
|
}
|
|
|
|
func TestRedisTestConnectionUsesIsolatedClientAndClosesIt(t *testing.T) {
|
|
originalNewRedisClientFunc := newRedisClientFunc
|
|
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
|
|
proxySnapshot := currentGlobalProxyConfig()
|
|
defer func() {
|
|
newRedisClientFunc = originalNewRedisClientFunc
|
|
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
|
|
if _, err := setGlobalProxyConfig(proxySnapshot.Enabled, proxySnapshot.Proxy); err != nil {
|
|
t.Fatalf("restore global proxy failed: %v", err)
|
|
}
|
|
CloseAllRedisClients()
|
|
}()
|
|
CloseAllRedisClients()
|
|
if _, err := setGlobalProxyConfig(false, proxySnapshot.Proxy); err != nil {
|
|
t.Fatalf("disable global proxy failed: %v", err)
|
|
}
|
|
|
|
client := &capturingRedisClient{}
|
|
newRedisClientFunc = func() redislib.RedisClient {
|
|
return client
|
|
}
|
|
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
|
|
return raw, nil
|
|
}
|
|
|
|
app := NewApp()
|
|
result := app.RedisTestConnection(connection.ConnectionConfig{
|
|
Type: "redis",
|
|
Host: "127.0.0.1",
|
|
Port: 6379,
|
|
})
|
|
|
|
if !result.Success {
|
|
t.Fatalf("expected redis test connection success, got %s", result.Message)
|
|
}
|
|
if client.closed != 1 {
|
|
t.Fatalf("expected isolated redis test client to be closed once, got %d", client.closed)
|
|
}
|
|
if len(redisCache) != 0 {
|
|
t.Fatalf("redis test connection must not write global redis cache, got %d entries", len(redisCache))
|
|
}
|
|
}
|
|
|
|
func TestRedisTestConnectionReturnsLocalizedCloseFailure(t *testing.T) {
|
|
originalNewRedisClientFunc := newRedisClientFunc
|
|
originalResolveDialConfigWithProxyFunc := resolveDialConfigWithProxyFunc
|
|
proxySnapshot := currentGlobalProxyConfig()
|
|
defer func() {
|
|
newRedisClientFunc = originalNewRedisClientFunc
|
|
resolveDialConfigWithProxyFunc = originalResolveDialConfigWithProxyFunc
|
|
if _, err := setGlobalProxyConfig(proxySnapshot.Enabled, proxySnapshot.Proxy); err != nil {
|
|
t.Fatalf("restore global proxy failed: %v", err)
|
|
}
|
|
CloseAllRedisClients()
|
|
}()
|
|
CloseAllRedisClients()
|
|
if _, err := setGlobalProxyConfig(false, proxySnapshot.Proxy); err != nil {
|
|
t.Fatalf("disable global proxy failed: %v", err)
|
|
}
|
|
|
|
client := &capturingRedisClient{closeErr: errors.New("close failed")}
|
|
newRedisClientFunc = func() redislib.RedisClient {
|
|
return client
|
|
}
|
|
resolveDialConfigWithProxyFunc = func(raw connection.ConnectionConfig) (connection.ConnectionConfig, error) {
|
|
return raw, nil
|
|
}
|
|
|
|
app := NewApp()
|
|
result := app.RedisTestConnection(connection.ConnectionConfig{
|
|
Type: "redis",
|
|
Host: "127.0.0.1",
|
|
Port: 6379,
|
|
})
|
|
|
|
if result.Success {
|
|
t.Fatalf("expected localized close failure, got success with %q", result.Message)
|
|
}
|
|
if want := app.appText("redis.backend.error.test_connection_close_failed", map[string]any{"detail": "close failed"}); result.Message != want {
|
|
t.Fatalf("expected localized close failure message %q, got %q", want, result.Message)
|
|
}
|
|
if client.closed != 1 {
|
|
t.Fatalf("expected isolated redis test client to be closed once, got %d", client.closed)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestBuildRedisExportPayloadCollectsPagedKeysAndSerializesAllSupportedShapes(t *testing.T) {
|
|
client := &redisTransferTestClient{
|
|
scanPages: []*redislib.RedisScanResult{
|
|
{
|
|
Keys: []redislib.RedisKeyInfo{
|
|
{Key: "beta"},
|
|
{Key: "alpha"},
|
|
},
|
|
Cursor: "1",
|
|
},
|
|
{
|
|
Keys: []redislib.RedisKeyInfo{
|
|
{Key: "alpha"},
|
|
{Key: "events"},
|
|
},
|
|
Cursor: "0",
|
|
},
|
|
},
|
|
keyTypes: map[string]string{
|
|
"alpha": "string",
|
|
"beta": "set",
|
|
"events": "stream",
|
|
},
|
|
ttls: map[string]int64{
|
|
"alpha": 120,
|
|
"beta": -1,
|
|
"events": 45,
|
|
},
|
|
stringValues: map[string]string{
|
|
"alpha": "value-1",
|
|
},
|
|
setValues: map[string][]string{
|
|
"beta": {"member-b", "member-a"},
|
|
},
|
|
streamValues: map[string][]redislib.StreamEntry{
|
|
"events": {
|
|
{ID: "1710000000000-0", Fields: map[string]string{"kind": "start"}},
|
|
{ID: "1710000000001-0", Fields: map[string]string{"kind": "finish"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
payload, err := buildRedisExportPayload(client, 7, RedisExportKeysOptions{
|
|
Scope: "all",
|
|
Pattern: "app:*",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("buildRedisExportPayload returned error: %v", err)
|
|
}
|
|
|
|
if payload.Format != redisTransferFileFormat || payload.Version != redisTransferFileVersion {
|
|
t.Fatalf("unexpected redis transfer header: %+v", payload)
|
|
}
|
|
if payload.Database != 7 || payload.Scope != "all" || payload.Pattern != "app:*" {
|
|
t.Fatalf("unexpected redis transfer metadata: %+v", payload)
|
|
}
|
|
if got := len(payload.Keys); got != 3 {
|
|
t.Fatalf("expected 3 exported keys, got %d", got)
|
|
}
|
|
if payload.Keys[0].Key != "alpha" || payload.Keys[1].Key != "beta" || payload.Keys[2].Key != "events" {
|
|
t.Fatalf("expected sorted exported keys, got %+v", payload.Keys)
|
|
}
|
|
|
|
if value, ok := payload.Keys[0].Value.(string); !ok || value != "value-1" {
|
|
t.Fatalf("expected string payload for alpha, got %#v", payload.Keys[0].Value)
|
|
}
|
|
|
|
setMembers, ok := payload.Keys[1].Value.([]string)
|
|
if !ok {
|
|
t.Fatalf("expected set payload slice, got %#v", payload.Keys[1].Value)
|
|
}
|
|
if !reflect.DeepEqual(setMembers, []string{"member-a", "member-b"}) {
|
|
t.Fatalf("expected sorted set members, got %v", setMembers)
|
|
}
|
|
|
|
streamEntries, ok := payload.Keys[2].Value.([]redislib.StreamEntry)
|
|
if !ok {
|
|
t.Fatalf("expected stream payload slice, got %#v", payload.Keys[2].Value)
|
|
}
|
|
if len(streamEntries) != 2 || streamEntries[1].Fields["kind"] != "finish" {
|
|
t.Fatalf("unexpected stream payload: %+v", streamEntries)
|
|
}
|
|
}
|
|
|
|
func TestImportRedisTransferPayloadHonorsConflictMode(t *testing.T) {
|
|
basePayload := redisTransferFile{
|
|
Keys: []redisTransferEntry{
|
|
{Key: "existing", Type: "string", TTL: 90, Value: "updated"},
|
|
{Key: "profile", Type: "hash", TTL: 30, Value: map[string]string{"name": "neo", "role": "admin"}},
|
|
{Key: "events", Type: "stream", TTL: -1, Value: []redislib.StreamEntry{{ID: "1710000000000-0", Fields: map[string]string{"status": "ok"}}}},
|
|
},
|
|
}
|
|
|
|
skipClient := &redisTransferTestClient{
|
|
existingKeys: map[string]bool{
|
|
"existing": true,
|
|
},
|
|
}
|
|
skipSummary, err := importRedisTransferPayload(skipClient, basePayload, RedisImportKeysOptions{ConflictMode: "skip"})
|
|
if err != nil {
|
|
t.Fatalf("importRedisTransferPayload(skip) returned error: %v", err)
|
|
}
|
|
if skipSummary["imported"] != 2 || skipSummary["skipped"] != 1 || skipSummary["total"] != 3 {
|
|
t.Fatalf("unexpected skip import summary: %+v", skipSummary)
|
|
}
|
|
if len(skipClient.deletedKeys) != 0 {
|
|
t.Fatalf("skip mode should not delete existing keys, got %v", skipClient.deletedKeys)
|
|
}
|
|
if len(skipClient.setStringCalls) != 0 {
|
|
t.Fatalf("skip mode should not overwrite existing string keys, got %+v", skipClient.setStringCalls)
|
|
}
|
|
if len(skipClient.setHashFieldCalls) != 2 {
|
|
t.Fatalf("expected hash fields to import in skip mode, got %+v", skipClient.setHashFieldCalls)
|
|
}
|
|
sort.Slice(skipClient.setHashFieldCalls, func(i, j int) bool {
|
|
return skipClient.setHashFieldCalls[i].field < skipClient.setHashFieldCalls[j].field
|
|
})
|
|
if skipClient.setHashFieldCalls[0].field != "name" || skipClient.setHashFieldCalls[1].field != "role" {
|
|
t.Fatalf("unexpected imported hash fields: %+v", skipClient.setHashFieldCalls)
|
|
}
|
|
if len(skipClient.setTTLCalls) != 1 || skipClient.setTTLCalls[0].key != "profile" || skipClient.setTTLCalls[0].ttl != 30 {
|
|
t.Fatalf("expected imported hash ttl to be restored, got %+v", skipClient.setTTLCalls)
|
|
}
|
|
if len(skipClient.streamAddCalls) != 1 || skipClient.streamAddCalls[0].id != "1710000000000-0" {
|
|
t.Fatalf("expected stream entry import in skip mode, got %+v", skipClient.streamAddCalls)
|
|
}
|
|
|
|
overwriteClient := &redisTransferTestClient{
|
|
existingKeys: map[string]bool{
|
|
"existing": true,
|
|
},
|
|
}
|
|
overwriteSummary, err := importRedisTransferPayload(overwriteClient, basePayload, RedisImportKeysOptions{ConflictMode: "overwrite"})
|
|
if err != nil {
|
|
t.Fatalf("importRedisTransferPayload(overwrite) returned error: %v", err)
|
|
}
|
|
if overwriteSummary["imported"] != 3 || overwriteSummary["skipped"] != 0 || overwriteSummary["total"] != 3 {
|
|
t.Fatalf("unexpected overwrite import summary: %+v", overwriteSummary)
|
|
}
|
|
if !reflect.DeepEqual(overwriteClient.deletedKeys, []string{"existing"}) {
|
|
t.Fatalf("expected overwrite mode to delete the existing key first, got %v", overwriteClient.deletedKeys)
|
|
}
|
|
if len(overwriteClient.setStringCalls) != 1 {
|
|
t.Fatalf("expected overwrite mode to restore one string key, got %+v", overwriteClient.setStringCalls)
|
|
}
|
|
if overwriteClient.setStringCalls[0].key != "existing" || overwriteClient.setStringCalls[0].value != "updated" || overwriteClient.setStringCalls[0].ttl != 90 {
|
|
t.Fatalf("unexpected overwrite string restore call: %+v", overwriteClient.setStringCalls[0])
|
|
}
|
|
}
|
|
|
|
func TestImportRedisTransferPayloadHonorsSelectedScope(t *testing.T) {
|
|
payload := redisTransferFile{
|
|
Keys: []redisTransferEntry{
|
|
{Key: "existing", Type: "string", TTL: 90, Value: "updated"},
|
|
{Key: "profile", Type: "hash", TTL: 30, Value: map[string]string{"name": "neo"}},
|
|
{Key: "events", Type: "stream", TTL: -1, Value: []redislib.StreamEntry{{ID: "1710000000000-0", Fields: map[string]string{"status": "ok"}}}},
|
|
},
|
|
}
|
|
|
|
client := &redisTransferTestClient{
|
|
existingKeys: map[string]bool{
|
|
"existing": true,
|
|
},
|
|
}
|
|
result, err := importRedisTransferPayload(client, payload, RedisImportKeysOptions{
|
|
Scope: "selected",
|
|
Keys: []string{"profile", "events"},
|
|
ConflictMode: "overwrite",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("importRedisTransferPayload(selected) returned error: %v", err)
|
|
}
|
|
if result["imported"] != 2 || result["skipped"] != 0 || result["total"] != 2 {
|
|
t.Fatalf("unexpected selected import summary: %+v", result)
|
|
}
|
|
if len(client.deletedKeys) != 0 {
|
|
t.Fatalf("selected import should not touch unselected keys, got %v", client.deletedKeys)
|
|
}
|
|
if len(client.setStringCalls) != 0 {
|
|
t.Fatalf("selected import should not restore unselected string keys, got %+v", client.setStringCalls)
|
|
}
|
|
if len(client.setHashFieldCalls) != 1 || client.setHashFieldCalls[0].key != "profile" {
|
|
t.Fatalf("expected only selected hash key import, got %+v", client.setHashFieldCalls)
|
|
}
|
|
if len(client.streamAddCalls) != 1 || client.streamAddCalls[0].key != "events" {
|
|
t.Fatalf("expected only selected stream key import, got %+v", client.streamAddCalls)
|
|
}
|
|
}
|
|
|
|
func TestSelectRedisTransferEntriesForImportRequiresSelectedKeys(t *testing.T) {
|
|
payload := redisTransferFile{
|
|
Keys: []redisTransferEntry{
|
|
{Key: "alpha", Type: "string", TTL: -1, Value: "1"},
|
|
},
|
|
}
|
|
|
|
_, err := selectRedisTransferEntriesForImport(payload, RedisImportKeysOptions{Scope: "selected"})
|
|
if !errors.Is(err, errRedisImportNoKeysSelected) {
|
|
t.Fatalf("expected errRedisImportNoKeysSelected, got %v", err)
|
|
}
|
|
}
|