mattermost-community-enterp.../channels/store/storetest/plugin_store.go
Claude ec1f89217a Merge: Complete Mattermost Server with Community Enterprise
Full Mattermost server source with integrated Community Enterprise features.
Includes vendor directory for offline/air-gapped builds.

Structure:
- enterprise-impl/: Enterprise feature implementations
- enterprise-community/: Init files that register implementations
- enterprise/: Bridge imports (community_imports.go)
- vendor/: All dependencies for offline builds

Build (online):
  go build ./cmd/mattermost

Build (offline/air-gapped):
  go build -mod=vendor ./cmd/mattermost

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-17 23:59:07 +09:00

1373 lines
35 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package storetest
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/request"
"github.com/mattermost/mattermost/server/v8/channels/store"
)
func TestPluginStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
t.Run("SaveOrUpdate", func(t *testing.T) { testPluginSaveOrUpdate(t, rctx, ss) })
t.Run("CompareAndSet", func(t *testing.T) { testPluginCompareAndSet(t, rctx, ss) })
t.Run("CompareAndDelete", func(t *testing.T) { testPluginCompareAndDelete(t, rctx, ss) })
t.Run("SetWithOptions", func(t *testing.T) { testPluginSetWithOptions(t, rctx, ss) })
t.Run("Get", func(t *testing.T) { testPluginGet(t, rctx, ss) })
t.Run("Delete", func(t *testing.T) { testPluginDelete(t, rctx, ss) })
t.Run("DeleteAllForPlugin", func(t *testing.T) { testPluginDeleteAllForPlugin(t, rctx, ss) })
t.Run("DeleteAllExpired", func(t *testing.T) { testPluginDeleteAllExpired(t, rctx, ss) })
t.Run("List", func(t *testing.T) { testPluginList(t, rctx, ss) })
}
func setupKVs(t *testing.T, rctx request.CTX, ss store.Store) (string, func()) {
pluginID := model.NewId()
otherPluginID := model.NewId()
// otherKV is another key value for the current plugin, and used to verify other keys
// aren't modified unintentionally.
otherKV := &model.PluginKeyValue{
PluginId: pluginID,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(otherKV)
require.NoError(t, err)
// otherPluginKV is a key value for another plugin, and used to verify other plugins' keys
// aren't modified unintentionally.
otherPluginKV := &model.PluginKeyValue{
PluginId: otherPluginID,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(otherPluginKV)
require.NoError(t, err)
return pluginID, func() {
actualOtherKV, err := ss.Plugin().Get(otherKV.PluginId, otherKV.Key)
require.NoError(t, err, "failed to find other key value for same plugin")
assert.Equal(t, otherKV, actualOtherKV)
actualOtherPluginKV, err := ss.Plugin().Get(otherPluginKV.PluginId, otherPluginKV.Key)
require.NoError(t, err, "failed to find other key value from different plugin")
assert.Equal(t, otherPluginKV, actualOtherPluginKV)
}
}
func doTestPluginSaveOrUpdate(t *testing.T, rctx request.CTX, ss store.Store, doer func(kv *model.PluginKeyValue) (*model.PluginKeyValue, error)) {
t.Run("invalid kv", func(t *testing.T) {
_, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
kv := &model.PluginKeyValue{
PluginId: "",
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
kv, err := doer(kv)
require.Error(t, err)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
require.Equal(t, "model.plugin_key_value.is_valid.plugin_id.app_error", appErr.Id)
assert.Nil(t, kv)
})
t.Run("new key", func(t *testing.T) {
pluginID, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
retKV, err := doer(kv)
require.NoError(t, err)
assert.Equal(t, kv, retKV)
// SaveOrUpdate returns the kv passed in, so test each field individually for
// completeness. It should probably be changed to not bother doing that.
assert.Equal(t, pluginID, kv.PluginId)
assert.Equal(t, key, kv.Key)
assert.Equal(t, []byte(value), kv.Value)
assert.Equal(t, expireAt, kv.ExpireAt)
actualKV, nErr := ss.Plugin().Get(pluginID, key)
require.NoError(t, nErr)
assert.Equal(t, kv, actualKV)
})
t.Run("nil value for new key", func(t *testing.T) {
pluginID, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
key := model.NewId()
var value []byte
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: value,
ExpireAt: expireAt,
}
retKV, err := doer(kv)
require.NoError(t, err)
assert.Equal(t, kv, retKV)
// SaveOrUpdate returns the kv passed in, so test each field individually for
// completeness. It should probably be changed to not bother doing that.
assert.Equal(t, pluginID, kv.PluginId)
assert.Equal(t, key, kv.Key)
assert.Nil(t, kv.Value)
assert.Equal(t, expireAt, kv.ExpireAt)
actualKV, nErr := ss.Plugin().Get(pluginID, key)
_, ok := nErr.(*store.ErrNotFound)
require.Error(t, nErr)
assert.True(t, ok)
assert.Nil(t, actualKV)
})
t.Run("existing key", func(t *testing.T) {
pluginID, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := doer(kv)
require.NoError(t, err)
newValue := model.NewId()
kv.Value = []byte(newValue)
retKV, err := doer(kv)
require.NoError(t, err)
assert.Equal(t, kv, retKV)
// SaveOrUpdate returns the kv passed in, so test each field individually for
// completeness. It should probably be changed to not bother doing that.
assert.Equal(t, pluginID, kv.PluginId)
assert.Equal(t, key, kv.Key)
assert.Equal(t, []byte(newValue), kv.Value)
assert.Equal(t, expireAt, kv.ExpireAt)
actualKV, nErr := ss.Plugin().Get(pluginID, key)
require.NoError(t, nErr)
assert.Equal(t, kv, actualKV)
})
t.Run("nil value for existing key", func(t *testing.T) {
pluginID, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := doer(kv)
require.NoError(t, err)
kv.Value = nil
retKV, err := doer(kv)
require.NoError(t, err)
assert.Equal(t, kv, retKV)
// SaveOrUpdate returns the kv passed in, so test each field individually for
// completeness. It should probably be changed to not bother doing that.
assert.Equal(t, pluginID, kv.PluginId)
assert.Equal(t, key, kv.Key)
assert.Nil(t, kv.Value)
assert.Equal(t, expireAt, kv.ExpireAt)
actualKV, nErr := ss.Plugin().Get(pluginID, key)
_, ok := nErr.(*store.ErrNotFound)
require.Error(t, nErr)
assert.True(t, ok)
assert.Nil(t, actualKV)
})
}
func testPluginSaveOrUpdate(t *testing.T, rctx request.CTX, ss store.Store) {
doTestPluginSaveOrUpdate(t, rctx, ss, func(kv *model.PluginKeyValue) (*model.PluginKeyValue, error) {
return ss.Plugin().SaveOrUpdate(kv)
})
}
// doTestPluginCompareAndSet exercises the CompareAndSet functionality, but abstracts the actual
// call to same to allow reuse with SetWithOptions
func doTestPluginCompareAndSet(t *testing.T, rctx request.CTX, ss store.Store, compareAndSet func(kv *model.PluginKeyValue, oldValue []byte) (bool, error)) {
t.Run("invalid kv", func(t *testing.T) {
_, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
kv := &model.PluginKeyValue{
PluginId: "",
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
ok, err := compareAndSet(kv, nil)
require.Error(t, err)
assert.False(t, ok)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
assert.Equal(t, "model.plugin_key_value.is_valid.plugin_id.app_error", appErr.Id)
})
// assertChanged verifies that CompareAndSet successfully changes to the given value.
assertChanged := func(t *testing.T, kv *model.PluginKeyValue, oldValue []byte) {
t.Helper()
ok, err := compareAndSet(kv, oldValue)
require.NoError(t, err)
require.True(t, ok, "should have succeeded to CompareAndSet")
actualKV, nErr := ss.Plugin().Get(kv.PluginId, kv.Key)
require.NoError(t, nErr)
// When tested with KVSetWithOptions, a strict comparison can fail because that
// function accepts a relative time and makes its own call to model.GetMillis(),
// leading to off-by-one issues. All these tests are written with 15+ second
// differences, so allow for an off-by-1000ms in either direction.
require.NotNil(t, actualKV)
expiryDelta := actualKV.ExpireAt - kv.ExpireAt
if expiryDelta > -1000 && expiryDelta < 1000 {
actualKV.ExpireAt = kv.ExpireAt
}
assert.Equal(t, kv, actualKV)
}
// assertUnchanged verifies that CompareAndSet fails, leaving the existing value.
assertUnchanged := func(t *testing.T, kv, existingKV *model.PluginKeyValue, oldValue []byte) {
t.Helper()
ok, err := compareAndSet(kv, oldValue)
require.NoError(t, err)
require.False(t, ok, "should have failed to CompareAndSet")
actualKV, nErr := ss.Plugin().Get(kv.PluginId, kv.Key)
if existingKV == nil {
require.Error(t, nErr)
_, ok := nErr.(*store.ErrNotFound)
assert.True(t, ok)
assert.Nil(t, actualKV)
} else {
require.NoError(t, nErr)
assert.Equal(t, existingKV, actualKV)
}
}
// assertRemoved verifies that CompareAndSet successfully removes the given value.
assertRemoved := func(t *testing.T, kv *model.PluginKeyValue, oldValue []byte) {
t.Helper()
ok, err := compareAndSet(kv, oldValue)
require.NoError(t, err)
require.True(t, ok, "should have succeeded to CompareAndSet")
actualKV, nErr := ss.Plugin().Get(kv.PluginId, kv.Key)
_, ok = nErr.(*store.ErrNotFound)
require.Error(t, nErr)
assert.True(t, ok)
assert.Nil(t, actualKV)
}
// Non-existent keys and expired keys should behave identically.
for description, setup := range map[string]func(t *testing.T) (*model.PluginKeyValue, func()){
"non-existent key": func(t *testing.T) (*model.PluginKeyValue, func()) {
pluginID, tearDown := setupKVs(t, rctx, ss)
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
return kv, tearDown
},
"expired key": func(t *testing.T) (*model.PluginKeyValue, func()) {
pluginID, tearDown := setupKVs(t, rctx, ss)
expiredKV := &model.PluginKeyValue{
PluginId: pluginID,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 1,
}
_, err := ss.Plugin().SaveOrUpdate(expiredKV)
require.NoError(t, err)
return expiredKV, tearDown
},
} {
t.Run(description, func(t *testing.T) {
t.Run("setting a nil value should fail", func(t *testing.T) {
testCases := map[string][]byte{
"given nil old value": nil,
"given non-nil old value": []byte(model.NewId()),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
kv.Value = nil
assertUnchanged(t, kv, nil, oldValue)
})
}
})
t.Run("setting a non-nil value", func(t *testing.T) {
t.Run("should succeed given non-expiring, nil old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
kv.ExpireAt = 0
assertChanged(t, kv, []byte(nil))
})
t.Run("should succeed given not-yet-expired, nil old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
kv.ExpireAt = model.GetMillis() + 15*1000
assertChanged(t, kv, []byte(nil))
})
t.Run("should fail given expired, nil old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
kv.ExpireAt = 1
assertRemoved(t, kv, []byte(nil))
})
t.Run("should fail given 'different' old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
assertUnchanged(t, kv, nil, []byte(model.NewId()))
})
t.Run("should fail given 'same' old value", func(t *testing.T) {
kv, tearDown := setup(t)
defer tearDown()
assertUnchanged(t, kv, nil, kv.Value)
})
})
})
}
t.Run("existing key", func(t *testing.T) {
setup := func(t *testing.T) (*model.PluginKeyValue, func()) {
pluginID, tearDown := setupKVs(t, rctx, ss)
existingKV := &model.PluginKeyValue{
PluginId: pluginID,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(existingKV)
require.NoError(t, err)
return existingKV, tearDown
}
testCases := map[string]bool{
// CompareAndSet should succeed even if the value isn't changing.
"setting the same value": true,
"setting a different value": false,
}
for description, setToSameValue := range testCases {
makeKV := func(existingKV *model.PluginKeyValue) *model.PluginKeyValue {
kv := &model.PluginKeyValue{
PluginId: existingKV.PluginId,
Key: existingKV.Key,
ExpireAt: existingKV.ExpireAt,
}
if setToSameValue {
kv.Value = existingKV.Value
} else {
kv.Value = []byte(model.NewId())
}
return kv
}
t.Run(description, func(t *testing.T) {
t.Run("should fail", func(t *testing.T) {
testCases := map[string][]byte{
"given nil old value": nil,
"given different old value": []byte(model.NewId()),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
assertUnchanged(t, kv, existingKV, oldValue)
})
}
})
t.Run("should succeed given same old value", func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
assertChanged(t, kv, existingKV.Value)
})
t.Run("and future expiry should succeed given same old value", func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
kv.ExpireAt = model.GetMillis() + 15*1000
assertChanged(t, kv, existingKV.Value)
})
t.Run("and past expiry should succeed given same old value", func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
kv.ExpireAt = model.GetMillis() - 15*1000
assertRemoved(t, kv, existingKV.Value)
})
})
}
t.Run("setting a nil value", func(t *testing.T) {
makeKV := func(existingKV *model.PluginKeyValue) *model.PluginKeyValue {
kv := &model.PluginKeyValue{
PluginId: existingKV.PluginId,
Key: existingKV.Key,
Value: existingKV.Value,
ExpireAt: existingKV.ExpireAt,
}
kv.Value = nil
return kv
}
t.Run("should fail", func(t *testing.T) {
testCases := map[string][]byte{
"given nil old value": nil,
"given different old value": []byte(model.NewId()),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
assertUnchanged(t, kv, existingKV, oldValue)
})
}
})
t.Run("should succeed, deleting, given same old value", func(t *testing.T) {
existingKV, tearDown := setup(t)
defer tearDown()
kv := makeKV(existingKV)
assertRemoved(t, kv, existingKV.Value)
})
})
})
}
func testPluginCompareAndSet(t *testing.T, rctx request.CTX, ss store.Store) {
doTestPluginCompareAndSet(t, rctx, ss, func(kv *model.PluginKeyValue, oldValue []byte) (bool, error) {
return ss.Plugin().CompareAndSet(kv, oldValue)
})
}
func testPluginCompareAndDelete(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("invalid kv", func(t *testing.T) {
_, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
kv := &model.PluginKeyValue{
PluginId: "",
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
ok, err := ss.Plugin().CompareAndDelete(kv, nil)
require.Error(t, err)
assert.False(t, ok)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
assert.Equal(t, "model.plugin_key_value.is_valid.plugin_id.app_error", appErr.Id)
})
t.Run("non-existent key should fail", func(t *testing.T) {
pluginID, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
testCases := map[string][]byte{
"given nil old value": nil,
"given non-nil old value": []byte(model.NewId()),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
ok, err := ss.Plugin().CompareAndDelete(kv, oldValue)
require.NoError(t, err)
assert.False(t, ok)
})
}
})
t.Run("expired key should fail", func(t *testing.T) {
pluginID, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(1)
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
testCases := map[string][]byte{
"given nil old value": nil,
"given different old value": []byte(model.NewId()),
"given same old value": []byte(value),
}
for description, oldValue := range testCases {
t.Run(description, func(t *testing.T) {
ok, err := ss.Plugin().CompareAndDelete(kv, oldValue)
require.NoError(t, err)
assert.False(t, ok)
})
}
})
t.Run("existing key should fail given different old value", func(t *testing.T) {
pluginID, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
oldValue := []byte(model.NewId())
ok, err := ss.Plugin().CompareAndDelete(kv, oldValue)
require.NoError(t, err)
assert.False(t, ok)
})
t.Run("existing key should succeed given same old value", func(t *testing.T) {
pluginID, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
oldValue := []byte(value)
ok, err := ss.Plugin().CompareAndDelete(kv, oldValue)
require.NoError(t, err)
assert.True(t, ok)
})
}
func testPluginSetWithOptions(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("invalid options", func(t *testing.T) {
_, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
pluginID := ""
key := model.NewId()
value := model.NewId()
options := model.PluginKVSetOptions{
Atomic: false,
OldValue: []byte("not-nil"),
}
ok, err := ss.Plugin().SetWithOptions(pluginID, key, []byte(value), options)
require.Error(t, err)
assert.False(t, ok)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
require.Equal(t, "model.plugin_kvset_options.is_valid.old_value.app_error", appErr.Id)
})
t.Run("invalid kv", func(t *testing.T) {
_, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
pluginID := ""
key := model.NewId()
value := model.NewId()
options := model.PluginKVSetOptions{}
ok, err := ss.Plugin().SetWithOptions(pluginID, key, []byte(value), options)
require.Error(t, err)
assert.False(t, ok)
appErr, ok := err.(*model.AppError)
require.True(t, ok)
require.Equal(t, "model.plugin_key_value.is_valid.plugin_id.app_error", appErr.Id)
})
t.Run("atomic", func(t *testing.T) {
doTestPluginCompareAndSet(t, rctx, ss, func(kv *model.PluginKeyValue, oldValue []byte) (bool, error) {
now := model.GetMillis()
options := model.PluginKVSetOptions{
Atomic: true,
OldValue: oldValue,
}
if kv.ExpireAt != 0 {
options.ExpireInSeconds = (kv.ExpireAt - now) / 1000
}
return ss.Plugin().SetWithOptions(kv.PluginId, kv.Key, kv.Value, options)
})
})
t.Run("non-atomic", func(t *testing.T) {
doTestPluginSaveOrUpdate(t, rctx, ss, func(kv *model.PluginKeyValue) (*model.PluginKeyValue, error) {
now := model.GetMillis()
options := model.PluginKVSetOptions{
Atomic: false,
}
if kv.ExpireAt != 0 {
options.ExpireInSeconds = (kv.ExpireAt - now) / 1000
}
ok, err := ss.Plugin().SetWithOptions(kv.PluginId, kv.Key, kv.Value, options)
if !ok {
return nil, err
}
return kv, err
})
})
}
func testPluginGet(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("no matching key value", func(t *testing.T) {
pluginID := model.NewId()
key := model.NewId()
kv, nErr := ss.Plugin().Get(pluginID, key)
_, ok := nErr.(*store.ErrNotFound)
require.Error(t, nErr)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("no-matching key value for plugin id", func(t *testing.T) {
pluginID := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv, err = ss.Plugin().Get(model.NewId(), key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("no-matching key value for key", func(t *testing.T) {
pluginID := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv, err = ss.Plugin().Get(pluginID, model.NewId())
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("old expired key value", func(t *testing.T) {
pluginID := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := int64(1)
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv, err = ss.Plugin().Get(pluginID, model.NewId())
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("recently expired key value", func(t *testing.T) {
pluginID := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := model.GetMillis() - 15*1000
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv, err = ss.Plugin().Get(pluginID, model.NewId())
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
t.Run("matching key value, non-expiring", func(t *testing.T) {
pluginID := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := int64(0)
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
actualKV, err := ss.Plugin().Get(pluginID, key)
require.NoError(t, err)
require.Equal(t, kv, actualKV)
})
t.Run("matching key value, not yet expired", func(t *testing.T) {
pluginID := model.NewId()
key := model.NewId()
value := model.NewId()
expireAt := model.GetMillis() + 15*1000
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
actualKV, err := ss.Plugin().Get(pluginID, key)
require.NoError(t, err)
require.Equal(t, kv, actualKV)
})
}
func testPluginDelete(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("no matching key value", func(t *testing.T) {
pluginID, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
key := model.NewId()
err := ss.Plugin().Delete(pluginID, key)
require.NoError(t, err)
kv, err := ss.Plugin().Get(pluginID, key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
testCases := []struct {
description string
expireAt int64
}{
{
"expired key value",
model.GetMillis() - 15*1000,
},
{
"never expiring value",
0,
},
{
"not yet expired value",
model.GetMillis() + 15*1000,
},
}
for _, testCase := range testCases {
t.Run(testCase.description, func(t *testing.T) {
pluginID, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
key := model.NewId()
value := model.NewId()
expireAt := testCase.expireAt
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(value),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
err = ss.Plugin().Delete(pluginID, key)
require.NoError(t, err)
kv, err = ss.Plugin().Get(pluginID, key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, kv)
})
}
}
func testPluginDeleteAllForPlugin(t *testing.T, rctx request.CTX, ss store.Store) {
setupKVsForDeleteAll := func(t *testing.T) (string, func()) {
pluginID := model.NewId()
otherPluginID := model.NewId()
// otherPluginKV is another key value for another plugin, and used to verify other
// keys aren't modified unintentionally.
otherPluginKV := &model.PluginKeyValue{
PluginId: otherPluginID,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(otherPluginKV)
require.NoError(t, err)
return pluginID, func() {
actualOtherPluginKV, err := ss.Plugin().Get(otherPluginKV.PluginId, otherPluginKV.Key)
require.NoError(t, err, "failed to find other key value from different plugin")
assert.Equal(t, otherPluginKV, actualOtherPluginKV)
}
}
t.Run("no keys to delete", func(t *testing.T) {
pluginID, tearDown := setupKVsForDeleteAll(t)
defer tearDown()
err := ss.Plugin().DeleteAllForPlugin(pluginID)
require.NoError(t, err)
})
t.Run("multiple keys to delete", func(t *testing.T) {
pluginID, tearDown := setupKVsForDeleteAll(t)
defer tearDown()
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
kv2 := &model.PluginKeyValue{
PluginId: pluginID,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(kv2)
require.NoError(t, err)
err = ss.Plugin().DeleteAllForPlugin(pluginID)
require.NoError(t, err)
_, err = ss.Plugin().Get(kv.PluginId, kv.Key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
_, err = ss.Plugin().Get(kv.PluginId, kv2.Key)
_, ok = err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
})
}
func testPluginDeleteAllExpired(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("no keys", func(t *testing.T) {
err := ss.Plugin().DeleteAllExpired()
require.NoError(t, err)
})
t.Run("no expiring keys to delete", func(t *testing.T) {
pluginIDA := model.NewId()
pluginIDB := model.NewId()
kvA1 := &model.PluginKeyValue{
PluginId: pluginIDA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kvA1)
require.NoError(t, err)
kvA2 := &model.PluginKeyValue{
PluginId: pluginIDA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(kvA2)
require.NoError(t, err)
kvB1 := &model.PluginKeyValue{
PluginId: pluginIDB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(kvB1)
require.NoError(t, err)
kvB2 := &model.PluginKeyValue{
PluginId: pluginIDB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err = ss.Plugin().SaveOrUpdate(kvB2)
require.NoError(t, err)
err = ss.Plugin().DeleteAllExpired()
require.NoError(t, err)
actualKVA1, err := ss.Plugin().Get(pluginIDA, kvA1.Key)
require.NoError(t, err)
assert.Equal(t, kvA1, actualKVA1)
actualKVA2, err := ss.Plugin().Get(pluginIDA, kvA2.Key)
require.NoError(t, err)
assert.Equal(t, kvA2, actualKVA2)
actualKVB1, err := ss.Plugin().Get(pluginIDB, kvB1.Key)
require.NoError(t, err)
assert.Equal(t, kvB1, actualKVB1)
actualKVB2, err := ss.Plugin().Get(pluginIDB, kvB2.Key)
require.NoError(t, err)
assert.Equal(t, kvB2, actualKVB2)
})
t.Run("no expired keys to delete", func(t *testing.T) {
pluginIDA := model.NewId()
pluginIDB := model.NewId()
kvA1 := &model.PluginKeyValue{
PluginId: pluginIDA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err := ss.Plugin().SaveOrUpdate(kvA1)
require.NoError(t, err)
kvA2 := &model.PluginKeyValue{
PluginId: pluginIDA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(kvA2)
require.NoError(t, err)
kvB1 := &model.PluginKeyValue{
PluginId: pluginIDB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(kvB1)
require.NoError(t, err)
kvB2 := &model.PluginKeyValue{
PluginId: pluginIDB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(kvB2)
require.NoError(t, err)
err = ss.Plugin().DeleteAllExpired()
require.NoError(t, err)
actualKVA1, err := ss.Plugin().Get(pluginIDA, kvA1.Key)
require.NoError(t, err)
assert.Equal(t, kvA1, actualKVA1)
actualKVA2, err := ss.Plugin().Get(pluginIDA, kvA2.Key)
require.NoError(t, err)
assert.Equal(t, kvA2, actualKVA2)
actualKVB1, err := ss.Plugin().Get(pluginIDB, kvB1.Key)
require.NoError(t, err)
assert.Equal(t, kvB1, actualKVB1)
actualKVB2, err := ss.Plugin().Get(pluginIDB, kvB2.Key)
require.NoError(t, err)
assert.Equal(t, kvB2, actualKVB2)
})
t.Run("some expired keys to delete", func(t *testing.T) {
pluginIDA := model.NewId()
pluginIDB := model.NewId()
kvA1 := &model.PluginKeyValue{
PluginId: pluginIDA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err := ss.Plugin().SaveOrUpdate(kvA1)
require.NoError(t, err)
expiredKVA2 := &model.PluginKeyValue{
PluginId: pluginIDA,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() - 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(expiredKVA2)
require.NoError(t, err)
kvB1 := &model.PluginKeyValue{
PluginId: pluginIDB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() + 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(kvB1)
require.NoError(t, err)
expiredKVB2 := &model.PluginKeyValue{
PluginId: pluginIDB,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: model.GetMillis() - 15*1000,
}
_, err = ss.Plugin().SaveOrUpdate(expiredKVB2)
require.NoError(t, err)
err = ss.Plugin().DeleteAllExpired()
require.NoError(t, err)
actualKVA1, err := ss.Plugin().Get(pluginIDA, kvA1.Key)
require.NoError(t, err)
assert.Equal(t, kvA1, actualKVA1)
actualKVA2, err := ss.Plugin().Get(pluginIDA, expiredKVA2.Key)
_, ok := err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, actualKVA2)
actualKVB1, err := ss.Plugin().Get(pluginIDB, kvB1.Key)
require.NoError(t, err)
assert.Equal(t, kvB1, actualKVB1)
actualKVB2, err := ss.Plugin().Get(pluginIDB, expiredKVB2.Key)
_, ok = err.(*store.ErrNotFound)
require.Error(t, err)
assert.True(t, ok)
assert.Nil(t, actualKVB2)
})
}
func testPluginList(t *testing.T, rctx request.CTX, ss store.Store) {
t.Run("no key values", func(t *testing.T) {
_, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
// Ignore the pluginID setup by setupKVs
pluginID := model.NewId()
keys, err := ss.Plugin().List(pluginID, 0, 100)
require.NoError(t, err)
assert.Empty(t, keys)
})
t.Run("single key", func(t *testing.T) {
_, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
// Ignore the pluginID setup by setupKVs
pluginID := model.NewId()
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: model.NewId(),
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
keys, err := ss.Plugin().List(pluginID, 0, 100)
require.NoError(t, err)
require.Len(t, keys, 1)
assert.Equal(t, kv.Key, keys[0])
})
t.Run("multiple keys", func(t *testing.T) {
_, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
// Ignore the pluginID setup by setupKVs
pluginID := model.NewId()
var keys []string
for range 150 {
key := model.NewId()
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
keys = append(keys, key)
}
sort.Strings(keys)
keys1, err := ss.Plugin().List(pluginID, 0, 100)
require.NoError(t, err)
require.Len(t, keys1, 100)
keys2, err := ss.Plugin().List(pluginID, 100, 100)
require.NoError(t, err)
require.Len(t, keys2, 50)
actualKeys := append(keys1, keys2...)
sort.Strings(actualKeys)
assert.Equal(t, keys, actualKeys)
})
t.Run("multiple keys, some expiring", func(t *testing.T) {
_, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
// Ignore the pluginID setup by setupKVs
pluginID := model.NewId()
var keys []string
now := model.GetMillis()
for i := range 150 {
key := model.NewId()
var expireAt int64
if i%10 == 0 {
// Expire keys 0, 10, 20, ...
expireAt = 1
} else if (i+5)%10 == 0 {
// Mark for future expiry keys 5, 15, 25, ...
expireAt = now + 5*60*1000
}
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(model.NewId()),
ExpireAt: expireAt,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
if expireAt == 0 || expireAt > now {
keys = append(keys, key)
}
}
sort.Strings(keys)
keys1, err := ss.Plugin().List(pluginID, 0, 100)
require.NoError(t, err)
require.Len(t, keys1, 100)
keys2, err := ss.Plugin().List(pluginID, 100, 100)
require.NoError(t, err)
require.Len(t, keys2, 35)
actualKeys := append(keys1, keys2...)
sort.Strings(actualKeys)
assert.Equal(t, keys, actualKeys)
})
t.Run("offsets and limits", func(t *testing.T) {
_, tearDown := setupKVs(t, rctx, ss)
defer tearDown()
// Ignore the pluginID setup by setupKVs
pluginID := model.NewId()
var keys []string
for range 150 {
key := model.NewId()
kv := &model.PluginKeyValue{
PluginId: pluginID,
Key: key,
Value: []byte(model.NewId()),
ExpireAt: 0,
}
_, err := ss.Plugin().SaveOrUpdate(kv)
require.NoError(t, err)
keys = append(keys, key)
}
sort.Strings(keys)
t.Run("default limit", func(t *testing.T) {
keys1, err := ss.Plugin().List(pluginID, 0, 0)
require.NoError(t, err)
require.Len(t, keys1, 10)
})
t.Run("offset 0, limit 1", func(t *testing.T) {
keys2, err := ss.Plugin().List(pluginID, 0, 1)
require.NoError(t, err)
require.Len(t, keys2, 1)
})
t.Run("offset 1, limit 1", func(t *testing.T) {
keys2, err := ss.Plugin().List(pluginID, 1, 1)
require.NoError(t, err)
require.Len(t, keys2, 1)
})
})
}