Add public existence-check methods across all store layers (Store, ScopedStore, StoreTransaction, ScopedStoreTransaction) so callers can test key/group presence declaratively without Get+error-type checking. Add Workspace.Count for total entry count. Full test coverage with Good/Bad/Ugly naming, race-clean. Co-Authored-By: Virgil <virgil@lethean.io>
413 lines
12 KiB
Go
413 lines
12 KiB
Go
package store
|
|
|
|
import (
|
|
"iter"
|
|
"testing"
|
|
"time"
|
|
|
|
core "dappco.re/go/core"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestTransaction_Transaction_Good_CommitsMultipleWrites(t *testing.T) {
|
|
storeInstance, _ := New(":memory:")
|
|
defer storeInstance.Close()
|
|
|
|
events := storeInstance.Watch("*")
|
|
defer storeInstance.Unwatch("*", events)
|
|
|
|
err := storeInstance.Transaction(func(transaction *StoreTransaction) error {
|
|
if err := transaction.Set("alpha", "first", "1"); err != nil {
|
|
return err
|
|
}
|
|
if err := transaction.Set("beta", "second", "2"); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
firstValue, err := storeInstance.Get("alpha", "first")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "1", firstValue)
|
|
|
|
secondValue, err := storeInstance.Get("beta", "second")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "2", secondValue)
|
|
|
|
received := drainEvents(events, 2, time.Second)
|
|
require.Len(t, received, 2)
|
|
assert.Equal(t, EventSet, received[0].Type)
|
|
assert.Equal(t, "alpha", received[0].Group)
|
|
assert.Equal(t, "first", received[0].Key)
|
|
assert.Equal(t, EventSet, received[1].Type)
|
|
assert.Equal(t, "beta", received[1].Group)
|
|
assert.Equal(t, "second", received[1].Key)
|
|
}
|
|
|
|
func TestTransaction_Transaction_Good_RollbackOnError(t *testing.T) {
|
|
storeInstance, _ := New(":memory:")
|
|
defer storeInstance.Close()
|
|
|
|
err := storeInstance.Transaction(func(transaction *StoreTransaction) error {
|
|
if err := transaction.Set("alpha", "first", "1"); err != nil {
|
|
return err
|
|
}
|
|
return core.E("test", "force rollback", nil)
|
|
})
|
|
require.Error(t, err)
|
|
|
|
_, err = storeInstance.Get("alpha", "first")
|
|
assert.ErrorIs(t, err, NotFoundError)
|
|
}
|
|
|
|
func TestTransaction_Transaction_Good_DeletesAtomically(t *testing.T) {
|
|
storeInstance, _ := New(":memory:")
|
|
defer storeInstance.Close()
|
|
|
|
require.NoError(t, storeInstance.Set("alpha", "first", "1"))
|
|
require.NoError(t, storeInstance.Set("beta", "second", "2"))
|
|
|
|
err := storeInstance.Transaction(func(transaction *StoreTransaction) error {
|
|
if err := transaction.DeletePrefix(""); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = storeInstance.Get("alpha", "first")
|
|
assert.ErrorIs(t, err, NotFoundError)
|
|
_, err = storeInstance.Get("beta", "second")
|
|
assert.ErrorIs(t, err, NotFoundError)
|
|
}
|
|
|
|
func TestTransaction_Transaction_Good_ReadHelpersSeePendingWrites(t *testing.T) {
|
|
storeInstance, _ := New(":memory:")
|
|
defer storeInstance.Close()
|
|
|
|
err := storeInstance.Transaction(func(transaction *StoreTransaction) error {
|
|
if err := transaction.Set("config", "colour", "blue"); err != nil {
|
|
return err
|
|
}
|
|
if err := transaction.Set("config", "hosts", "alpha beta"); err != nil {
|
|
return err
|
|
}
|
|
if err := transaction.Set("audit", "enabled", "true"); err != nil {
|
|
return err
|
|
}
|
|
|
|
entriesByKey, err := transaction.GetAll("config")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, map[string]string{"colour": "blue", "hosts": "alpha beta"}, entriesByKey)
|
|
|
|
count, err := transaction.CountAll("")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 3, count)
|
|
|
|
groupNames, err := transaction.Groups()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"audit", "config"}, groupNames)
|
|
|
|
renderedTemplate, err := transaction.Render("{{ .colour }} / {{ .hosts }}", "config")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "blue / alpha beta", renderedTemplate)
|
|
|
|
splitParts, err := transaction.GetSplit("config", "hosts", " ")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"alpha", "beta"}, collectSeq(t, splitParts))
|
|
|
|
fieldParts, err := transaction.GetFields("config", "hosts")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"alpha", "beta"}, collectSeq(t, fieldParts))
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestTransaction_Transaction_Good_PurgeExpired(t *testing.T) {
|
|
storeInstance, _ := New(":memory:")
|
|
defer storeInstance.Close()
|
|
|
|
require.NoError(t, storeInstance.SetWithTTL("alpha", "ephemeral", "gone", 1*time.Millisecond))
|
|
time.Sleep(5 * time.Millisecond)
|
|
|
|
err := storeInstance.Transaction(func(transaction *StoreTransaction) error {
|
|
removedRows, err := transaction.PurgeExpired()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), removedRows)
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = storeInstance.Get("alpha", "ephemeral")
|
|
assert.ErrorIs(t, err, NotFoundError)
|
|
}
|
|
|
|
func TestTransaction_Transaction_Good_Exists(t *testing.T) {
|
|
storeInstance, _ := New(":memory:")
|
|
defer storeInstance.Close()
|
|
|
|
require.NoError(t, storeInstance.Set("config", "colour", "blue"))
|
|
|
|
err := storeInstance.Transaction(func(transaction *StoreTransaction) error {
|
|
exists, err := transaction.Exists("config", "colour")
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
|
|
exists, err = transaction.Exists("config", "missing")
|
|
require.NoError(t, err)
|
|
assert.False(t, exists)
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestTransaction_Transaction_Good_ExistsSeesPendingWrites(t *testing.T) {
|
|
storeInstance, _ := New(":memory:")
|
|
defer storeInstance.Close()
|
|
|
|
err := storeInstance.Transaction(func(transaction *StoreTransaction) error {
|
|
exists, err := transaction.Exists("config", "colour")
|
|
require.NoError(t, err)
|
|
assert.False(t, exists)
|
|
|
|
if err := transaction.Set("config", "colour", "blue"); err != nil {
|
|
return err
|
|
}
|
|
|
|
exists, err = transaction.Exists("config", "colour")
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestTransaction_Transaction_Good_GroupExists(t *testing.T) {
|
|
storeInstance, _ := New(":memory:")
|
|
defer storeInstance.Close()
|
|
|
|
err := storeInstance.Transaction(func(transaction *StoreTransaction) error {
|
|
exists, err := transaction.GroupExists("config")
|
|
require.NoError(t, err)
|
|
assert.False(t, exists)
|
|
|
|
if err := transaction.Set("config", "colour", "blue"); err != nil {
|
|
return err
|
|
}
|
|
|
|
exists, err = transaction.GroupExists("config")
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestTransaction_ScopedStoreTransaction_Good_ExistsAndGroupExists(t *testing.T) {
|
|
storeInstance, _ := New(":memory:")
|
|
defer storeInstance.Close()
|
|
|
|
scopedStore, err := NewScoped(storeInstance, "tenant-a")
|
|
require.NoError(t, err)
|
|
|
|
err = scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error {
|
|
exists, err := transaction.Exists("colour")
|
|
require.NoError(t, err)
|
|
assert.False(t, exists)
|
|
|
|
if err := transaction.Set("colour", "blue"); err != nil {
|
|
return err
|
|
}
|
|
|
|
exists, err = transaction.Exists("colour")
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
|
|
exists, err = transaction.ExistsIn("other", "colour")
|
|
require.NoError(t, err)
|
|
assert.False(t, exists)
|
|
|
|
if err := transaction.SetIn("config", "theme", "dark"); err != nil {
|
|
return err
|
|
}
|
|
|
|
groupExists, err := transaction.GroupExists("config")
|
|
require.NoError(t, err)
|
|
assert.True(t, groupExists)
|
|
|
|
groupExists, err = transaction.GroupExists("missing-group")
|
|
require.NoError(t, err)
|
|
assert.False(t, groupExists)
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestTransaction_ScopedStoreTransaction_Good_GetPage(t *testing.T) {
|
|
storeInstance, _ := New(":memory:")
|
|
defer storeInstance.Close()
|
|
|
|
scopedStore, err := NewScoped(storeInstance, "tenant-a")
|
|
require.NoError(t, err)
|
|
|
|
err = scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error {
|
|
if err := transaction.SetIn("items", "charlie", "3"); err != nil {
|
|
return err
|
|
}
|
|
if err := transaction.SetIn("items", "alpha", "1"); err != nil {
|
|
return err
|
|
}
|
|
if err := transaction.SetIn("items", "bravo", "2"); err != nil {
|
|
return err
|
|
}
|
|
|
|
page, err := transaction.GetPage("items", 1, 1)
|
|
require.NoError(t, err)
|
|
require.Len(t, page, 1)
|
|
assert.Equal(t, KeyValue{Key: "bravo", Value: "2"}, page[0])
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestTransaction_ScopedStoreTransaction_Good_CommitsNamespacedWrites(t *testing.T) {
|
|
storeInstance, _ := New(":memory:")
|
|
defer storeInstance.Close()
|
|
|
|
scopedStore, err := NewScopedConfigured(storeInstance, ScopedStoreConfig{
|
|
Namespace: "tenant-a",
|
|
Quota: QuotaConfig{MaxKeys: 4, MaxGroups: 2},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error {
|
|
if err := transaction.Set("theme", "dark"); err != nil {
|
|
return err
|
|
}
|
|
if err := transaction.SetIn("preferences", "locale", "en-GB"); err != nil {
|
|
return err
|
|
}
|
|
|
|
themeValue, err := transaction.Get("theme")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "dark", themeValue)
|
|
|
|
localeValue, err := transaction.GetFrom("preferences", "locale")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "en-GB", localeValue)
|
|
|
|
groupNames, err := transaction.Groups()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"default", "preferences"}, groupNames)
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
themeValue, err := storeInstance.Get("tenant-a:default", "theme")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "dark", themeValue)
|
|
|
|
localeValue, err := storeInstance.Get("tenant-a:preferences", "locale")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "en-GB", localeValue)
|
|
}
|
|
|
|
func TestTransaction_ScopedStoreTransaction_Good_PurgeExpired(t *testing.T) {
|
|
storeInstance, _ := New(":memory:")
|
|
defer storeInstance.Close()
|
|
|
|
scopedStore, err := NewScoped(storeInstance, "tenant-a")
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, scopedStore.SetWithTTL("session", "token", "abc123", 1*time.Millisecond))
|
|
time.Sleep(5 * time.Millisecond)
|
|
|
|
err = scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error {
|
|
removedRows, err := transaction.PurgeExpired()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), removedRows)
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = scopedStore.GetFrom("session", "token")
|
|
assert.ErrorIs(t, err, NotFoundError)
|
|
}
|
|
|
|
func TestTransaction_ScopedStoreTransaction_Good_QuotaUsesPendingWrites(t *testing.T) {
|
|
storeInstance, _ := New(":memory:")
|
|
defer storeInstance.Close()
|
|
|
|
scopedStore, err := NewScopedConfigured(storeInstance, ScopedStoreConfig{
|
|
Namespace: "tenant-a",
|
|
Quota: QuotaConfig{MaxKeys: 2, MaxGroups: 2},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
err = scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error {
|
|
require.NoError(t, transaction.SetIn("group-1", "first", "1"))
|
|
require.NoError(t, transaction.SetIn("group-2", "second", "2"))
|
|
|
|
err := transaction.SetIn("group-2", "third", "3")
|
|
require.Error(t, err)
|
|
assert.True(t, core.Is(err, QuotaExceededError))
|
|
return err
|
|
})
|
|
require.Error(t, err)
|
|
assert.True(t, core.Is(err, QuotaExceededError))
|
|
|
|
_, getErr := storeInstance.Get("tenant-a:group-1", "first")
|
|
assert.True(t, core.Is(getErr, NotFoundError))
|
|
}
|
|
|
|
func TestTransaction_ScopedStoreTransaction_Good_DeletePrefix(t *testing.T) {
|
|
storeInstance, _ := New(":memory:")
|
|
defer storeInstance.Close()
|
|
|
|
scopedStore, err := NewScoped(storeInstance, "tenant-a")
|
|
require.NoError(t, err)
|
|
otherScopedStore, err := NewScoped(storeInstance, "tenant-b")
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, scopedStore.SetIn("cache", "theme", "dark"))
|
|
require.NoError(t, scopedStore.SetIn("cache-warm", "status", "ready"))
|
|
require.NoError(t, scopedStore.SetIn("config", "colour", "blue"))
|
|
require.NoError(t, otherScopedStore.SetIn("cache", "theme", "keep"))
|
|
|
|
err = scopedStore.Transaction(func(transaction *ScopedStoreTransaction) error {
|
|
return transaction.DeletePrefix("cache")
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, getErr := scopedStore.GetFrom("cache", "theme")
|
|
assert.True(t, core.Is(getErr, NotFoundError))
|
|
_, getErr = scopedStore.GetFrom("cache-warm", "status")
|
|
assert.True(t, core.Is(getErr, NotFoundError))
|
|
|
|
colourValue, getErr := scopedStore.GetFrom("config", "colour")
|
|
require.NoError(t, getErr)
|
|
assert.Equal(t, "blue", colourValue)
|
|
|
|
otherValue, getErr := otherScopedStore.GetFrom("cache", "theme")
|
|
require.NoError(t, getErr)
|
|
assert.Equal(t, "keep", otherValue)
|
|
}
|
|
|
|
func collectSeq[T any](t *testing.T, sequence iter.Seq[T]) []T {
|
|
t.Helper()
|
|
|
|
values := make([]T, 0)
|
|
for value := range sequence {
|
|
values = append(values, value)
|
|
}
|
|
return values
|
|
}
|