427 lines
11 KiB
Go
427 lines
11 KiB
Go
// ------------------------------------------------------------
|
|
// Copyright (c) Microsoft Corporation and Dapr Contributors.
|
|
// Licensed under the MIT License.
|
|
// ------------------------------------------------------------
|
|
|
|
package redis
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/agrea/ptr"
|
|
miniredis "github.com/alicebob/miniredis/v2"
|
|
redis "github.com/go-redis/redis/v8"
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
rediscomponent "github.com/dapr/components-contrib/internal/component/redis"
|
|
"github.com/dapr/components-contrib/state"
|
|
"github.com/dapr/kit/logger"
|
|
)
|
|
|
|
func TestGetKeyVersion(t *testing.T) {
|
|
store := NewRedisStateStore(logger.NewLogger("test"))
|
|
t.Run("With all required fields", func(t *testing.T) {
|
|
key, ver, err := store.getKeyVersion([]interface{}{"data", "TEST_KEY", "version", "TEST_VER"})
|
|
assert.Equal(t, nil, err, "failed to read all fields")
|
|
assert.Equal(t, "TEST_KEY", key, "failed to read key")
|
|
assert.Equal(t, ptr.String("TEST_VER"), ver, "failed to read version")
|
|
})
|
|
t.Run("With missing data", func(t *testing.T) {
|
|
_, _, err := store.getKeyVersion([]interface{}{"version", "TEST_VER"})
|
|
assert.NotNil(t, err, "failed to respond to missing data field")
|
|
})
|
|
t.Run("With missing version", func(t *testing.T) {
|
|
_, _, err := store.getKeyVersion([]interface{}{"data", "TEST_KEY"})
|
|
assert.NotNil(t, err, "failed to respond to missing version field")
|
|
})
|
|
t.Run("With all required fields - out of order", func(t *testing.T) {
|
|
key, ver, err := store.getKeyVersion([]interface{}{"version", "TEST_VER", "dragon", "TEST_DRAGON", "data", "TEST_KEY"})
|
|
assert.Equal(t, nil, err, "failed to read all fields")
|
|
assert.Equal(t, "TEST_KEY", key, "failed to read key")
|
|
assert.Equal(t, ptr.String("TEST_VER"), ver, "failed to read version")
|
|
})
|
|
t.Run("With no fields", func(t *testing.T) {
|
|
_, _, err := store.getKeyVersion([]interface{}{})
|
|
assert.NotNil(t, err, "failed to respond to missing fields")
|
|
})
|
|
t.Run("With wrong fields", func(t *testing.T) {
|
|
_, _, err := store.getKeyVersion([]interface{}{"dragon", "TEST_DRAGON"})
|
|
assert.NotNil(t, err, "failed to respond to missing fields")
|
|
})
|
|
}
|
|
|
|
func TestParseEtag(t *testing.T) {
|
|
store := NewRedisStateStore(logger.NewLogger("test"))
|
|
t.Run("Empty ETag", func(t *testing.T) {
|
|
etag := ""
|
|
ver, err := store.parseETag(&state.SetRequest{
|
|
ETag: &etag,
|
|
})
|
|
assert.Equal(t, nil, err, "failed to parse ETag")
|
|
assert.Equal(t, 0, ver, "default version should be 0")
|
|
})
|
|
t.Run("Number ETag", func(t *testing.T) {
|
|
etag := "354"
|
|
ver, err := store.parseETag(&state.SetRequest{
|
|
ETag: &etag,
|
|
})
|
|
assert.Equal(t, nil, err, "failed to parse ETag")
|
|
assert.Equal(t, 354, ver, "version should be 254")
|
|
})
|
|
t.Run("String ETag", func(t *testing.T) {
|
|
etag := "dragon"
|
|
_, err := store.parseETag(&state.SetRequest{
|
|
ETag: &etag,
|
|
})
|
|
assert.NotNil(t, err, "shouldn't recognize string ETag")
|
|
})
|
|
t.Run("Concurrency=LastWrite", func(t *testing.T) {
|
|
etag := "dragon"
|
|
ver, err := store.parseETag(&state.SetRequest{
|
|
Options: state.SetStateOption{
|
|
Concurrency: state.LastWrite,
|
|
},
|
|
ETag: &etag,
|
|
})
|
|
assert.Equal(t, nil, err, "failed to parse ETag")
|
|
assert.Equal(t, 0, ver, "version should be 0")
|
|
})
|
|
t.Run("Concurrency=FirstWrite", func(t *testing.T) {
|
|
ver, err := store.parseETag(&state.SetRequest{
|
|
Options: state.SetStateOption{
|
|
Concurrency: state.FirstWrite,
|
|
},
|
|
})
|
|
assert.Equal(t, nil, err, "failed to parse Concurrency")
|
|
assert.Equal(t, 0, ver, "version should be 0")
|
|
|
|
// ETag is nil
|
|
req := &state.SetRequest{
|
|
Options: state.SetStateOption{},
|
|
}
|
|
ver, err = store.parseETag(req)
|
|
assert.Equal(t, nil, err, "failed to parse Concurrency")
|
|
assert.Equal(t, 0, ver, "version should be 0")
|
|
|
|
// ETag is empty
|
|
emptyString := ""
|
|
req = &state.SetRequest{
|
|
ETag: &emptyString,
|
|
}
|
|
ver, err = store.parseETag(req)
|
|
assert.Equal(t, nil, err, "failed to parse Concurrency")
|
|
assert.Equal(t, 0, ver, "version should be 0")
|
|
})
|
|
}
|
|
|
|
func TestParseTTL(t *testing.T) {
|
|
store := NewRedisStateStore(logger.NewLogger("test"))
|
|
t.Run("TTL Not an integer", func(t *testing.T) {
|
|
ttlInSeconds := "not an integer"
|
|
ttl, err := store.parseTTL(&state.SetRequest{
|
|
Metadata: map[string]string{
|
|
"ttlInSeconds": ttlInSeconds,
|
|
},
|
|
})
|
|
assert.Error(t, err)
|
|
assert.Nil(t, ttl)
|
|
})
|
|
t.Run("TTL specified with wrong key", func(t *testing.T) {
|
|
ttlInSeconds := 12345
|
|
ttl, err := store.parseTTL(&state.SetRequest{
|
|
Metadata: map[string]string{
|
|
"expirationTime": strconv.Itoa(ttlInSeconds),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, ttl)
|
|
})
|
|
t.Run("TTL is a number", func(t *testing.T) {
|
|
ttlInSeconds := 12345
|
|
ttl, err := store.parseTTL(&state.SetRequest{
|
|
Metadata: map[string]string{
|
|
"ttlInSeconds": strconv.Itoa(ttlInSeconds),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, *ttl, ttlInSeconds)
|
|
})
|
|
|
|
t.Run("TTL never expires", func(t *testing.T) {
|
|
ttlInSeconds := -1
|
|
ttl, err := store.parseTTL(&state.SetRequest{
|
|
Metadata: map[string]string{
|
|
"ttlInSeconds": strconv.Itoa(ttlInSeconds),
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, *ttl, ttlInSeconds)
|
|
})
|
|
}
|
|
|
|
func TestParseConnectedSlavs(t *testing.T) {
|
|
store := NewRedisStateStore(logger.NewLogger("test"))
|
|
|
|
t.Run("Empty info", func(t *testing.T) {
|
|
slaves := store.parseConnectedSlaves("")
|
|
assert.Equal(t, 0, slaves, "connected slaves must be 0")
|
|
})
|
|
|
|
t.Run("connectedSlaves property is not included", func(t *testing.T) {
|
|
slaves := store.parseConnectedSlaves("# Replication\r\nrole:master\r\n")
|
|
assert.Equal(t, 0, slaves, "connected slaves must be 0")
|
|
})
|
|
|
|
t.Run("connectedSlaves is 2", func(t *testing.T) {
|
|
slaves := store.parseConnectedSlaves("# Replication\r\nrole:master\r\nconnected_slaves:2\r\n")
|
|
assert.Equal(t, 2, slaves, "connected slaves must be 2")
|
|
})
|
|
|
|
t.Run("connectedSlaves is 1", func(t *testing.T) {
|
|
slaves := store.parseConnectedSlaves("# Replication\r\nrole:master\r\nconnected_slaves:1")
|
|
assert.Equal(t, 1, slaves, "connected slaves must be 1")
|
|
})
|
|
}
|
|
|
|
func TestTransactionalUpsert(t *testing.T) {
|
|
s, c := setupMiniredis()
|
|
defer s.Close()
|
|
|
|
ss := &StateStore{
|
|
client: c,
|
|
json: jsoniter.ConfigFastest,
|
|
logger: logger.NewLogger("test"),
|
|
}
|
|
ss.ctx, ss.cancel = context.WithCancel(context.Background())
|
|
|
|
err := ss.Multi(&state.TransactionalStateRequest{
|
|
Operations: []state.TransactionalStateOperation{
|
|
{
|
|
Operation: state.Upsert,
|
|
Request: state.SetRequest{
|
|
Key: "weapon",
|
|
Value: "deathstar",
|
|
},
|
|
},
|
|
{
|
|
Operation: state.Upsert,
|
|
Request: state.SetRequest{
|
|
Key: "weapon2",
|
|
Value: "deathstar2",
|
|
Metadata: map[string]string{
|
|
"ttlInSeconds": "123",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Operation: state.Upsert,
|
|
Request: state.SetRequest{
|
|
Key: "weapon3",
|
|
Value: "deathstar3",
|
|
Metadata: map[string]string{
|
|
"ttlInSeconds": "-1",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
assert.Equal(t, nil, err)
|
|
|
|
res, err := c.Do(context.Background(), "HGETALL", "weapon").Result()
|
|
assert.Equal(t, nil, err)
|
|
|
|
vals := res.([]interface{})
|
|
data, version, err := ss.getKeyVersion(vals)
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, ptr.String("1"), version)
|
|
assert.Equal(t, `"deathstar"`, data)
|
|
|
|
res, err = c.Do(context.Background(), "TTL", "weapon").Result()
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, int64(-1), res)
|
|
|
|
res, err = c.Do(context.Background(), "TTL", "weapon2").Result()
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, int64(123), res)
|
|
|
|
res, err = c.Do(context.Background(), "TTL", "weapon3").Result()
|
|
assert.Equal(t, nil, err)
|
|
assert.Equal(t, int64(-1), res)
|
|
}
|
|
|
|
func TestTransactionalDelete(t *testing.T) {
|
|
s, c := setupMiniredis()
|
|
defer s.Close()
|
|
|
|
ss := &StateStore{
|
|
client: c,
|
|
json: jsoniter.ConfigFastest,
|
|
logger: logger.NewLogger("test"),
|
|
}
|
|
ss.ctx, ss.cancel = context.WithCancel(context.Background())
|
|
|
|
// Insert a record first.
|
|
ss.Set(&state.SetRequest{
|
|
Key: "weapon",
|
|
Value: "deathstar",
|
|
})
|
|
|
|
etag := "1"
|
|
err := ss.Multi(&state.TransactionalStateRequest{
|
|
Operations: []state.TransactionalStateOperation{{
|
|
Operation: state.Delete,
|
|
Request: state.DeleteRequest{
|
|
Key: "weapon",
|
|
ETag: &etag,
|
|
},
|
|
}},
|
|
})
|
|
assert.Equal(t, nil, err)
|
|
|
|
res, err := c.Do(context.Background(), "HGETALL", "weapon").Result()
|
|
assert.Equal(t, nil, err)
|
|
|
|
vals := res.([]interface{})
|
|
assert.Equal(t, 0, len(vals))
|
|
}
|
|
|
|
func TestPing(t *testing.T) {
|
|
s, c := setupMiniredis()
|
|
|
|
ss := &StateStore{
|
|
client: c,
|
|
json: jsoniter.ConfigFastest,
|
|
logger: logger.NewLogger("test"),
|
|
clientSettings: &rediscomponent.Settings{},
|
|
}
|
|
|
|
err := ss.Ping()
|
|
assert.NoError(t, err)
|
|
|
|
s.Close()
|
|
|
|
err = ss.Ping()
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestSetRequestWithTTL(t *testing.T) {
|
|
s, c := setupMiniredis()
|
|
defer s.Close()
|
|
|
|
ss := &StateStore{
|
|
client: c,
|
|
json: jsoniter.ConfigFastest,
|
|
logger: logger.NewLogger("test"),
|
|
}
|
|
ss.ctx, ss.cancel = context.WithCancel(context.Background())
|
|
|
|
t.Run("TTL specified", func(t *testing.T) {
|
|
ttlInSeconds := 100
|
|
ss.Set(&state.SetRequest{
|
|
Key: "weapon100",
|
|
Value: "deathstar100",
|
|
Metadata: map[string]string{
|
|
"ttlInSeconds": strconv.Itoa(ttlInSeconds),
|
|
},
|
|
})
|
|
|
|
ttl, _ := ss.client.TTL(ss.ctx, "weapon100").Result()
|
|
|
|
assert.Equal(t, time.Duration(ttlInSeconds)*time.Second, ttl)
|
|
})
|
|
|
|
t.Run("TTL not specified", func(t *testing.T) {
|
|
ss.Set(&state.SetRequest{
|
|
Key: "weapon200",
|
|
Value: "deathstar200",
|
|
})
|
|
|
|
ttl, _ := ss.client.TTL(ss.ctx, "weapon200").Result()
|
|
|
|
assert.Equal(t, time.Duration(-1), ttl)
|
|
})
|
|
|
|
t.Run("TTL Changed for Existing Key", func(t *testing.T) {
|
|
ss.Set(&state.SetRequest{
|
|
Key: "weapon300",
|
|
Value: "deathstar300",
|
|
})
|
|
ttl, _ := ss.client.TTL(ss.ctx, "weapon300").Result()
|
|
assert.Equal(t, time.Duration(-1), ttl)
|
|
|
|
// make the key no longer persistent
|
|
ttlInSeconds := 123
|
|
ss.Set(&state.SetRequest{
|
|
Key: "weapon300",
|
|
Value: "deathstar300",
|
|
Metadata: map[string]string{
|
|
"ttlInSeconds": strconv.Itoa(ttlInSeconds),
|
|
},
|
|
})
|
|
ttl, _ = ss.client.TTL(ss.ctx, "weapon300").Result()
|
|
assert.Equal(t, time.Duration(ttlInSeconds)*time.Second, ttl)
|
|
|
|
// make the key persistent again
|
|
ss.Set(&state.SetRequest{
|
|
Key: "weapon300",
|
|
Value: "deathstar301",
|
|
Metadata: map[string]string{
|
|
"ttlInSeconds": strconv.Itoa(-1),
|
|
},
|
|
})
|
|
ttl, _ = ss.client.TTL(ss.ctx, "weapon300").Result()
|
|
assert.Equal(t, time.Duration(-1), ttl)
|
|
})
|
|
}
|
|
|
|
func TestTransactionalDeleteNoEtag(t *testing.T) {
|
|
s, c := setupMiniredis()
|
|
defer s.Close()
|
|
|
|
ss := &StateStore{
|
|
client: c,
|
|
json: jsoniter.ConfigFastest,
|
|
logger: logger.NewLogger("test"),
|
|
}
|
|
ss.ctx, ss.cancel = context.WithCancel(context.Background())
|
|
|
|
// Insert a record first.
|
|
ss.Set(&state.SetRequest{
|
|
Key: "weapon100",
|
|
Value: "deathstar100",
|
|
})
|
|
|
|
err := ss.Multi(&state.TransactionalStateRequest{
|
|
Operations: []state.TransactionalStateOperation{{
|
|
Operation: state.Delete,
|
|
Request: state.DeleteRequest{
|
|
Key: "weapon100",
|
|
},
|
|
}},
|
|
})
|
|
assert.Equal(t, nil, err)
|
|
|
|
res, err := c.Do(context.Background(), "HGETALL", "weapon100").Result()
|
|
assert.Equal(t, nil, err)
|
|
|
|
vals := res.([]interface{})
|
|
assert.Equal(t, 0, len(vals))
|
|
}
|
|
|
|
func setupMiniredis() (*miniredis.Miniredis, *redis.Client) {
|
|
s, err := miniredis.Run()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
opts := &redis.Options{
|
|
Addr: s.Addr(),
|
|
DB: defaultDB,
|
|
}
|
|
|
|
return s, redis.NewClient(opts)
|
|
}
|