Files
grafana/pkg/storage/unified/resource/datastore_test.go
T
Will Assis 12dd3dffe0 unified-storage: sqlkv skeleton (#115176)
* implement sqlkv skeleton and include sqlkv in badgerkv tests
2025-12-15 08:56:15 -05:00

3330 lines
93 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package resource
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"testing"
"github.com/bwmarrin/snowflake"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
"github.com/stretchr/testify/require"
)
var node, _ = snowflake.NewNode(1)
func setupTestDataStore(t *testing.T) *dataStore {
kv := setupTestKV(t)
return newDataStore(kv)
}
func TestNewDataStore(t *testing.T) {
ds := setupTestDataStore(t)
require.NotNil(t, ds)
}
// nolint:unused
func setupTestDataStoreSqlKv(t *testing.T) *dataStore {
dbstore := db.InitTestDB(t)
eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil)
require.NoError(t, err)
kv, err := NewSQLKV(eDB)
require.NoError(t, err)
return newDataStore(kv)
}
func TestDataKey_String(t *testing.T) {
rv := int64(1934555792099250176)
tests := []struct {
name string
key DataKey
expected string
}{
{
name: "created key",
key: DataKey{
Group: "test-group",
Resource: "test-resource",
Namespace: "test-namespace",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
Folder: "test-folder",
},
expected: "test-group/test-resource/test-namespace/test-name/1934555792099250176~created~test-folder",
}, {
name: "updated key",
key: DataKey{
Group: "test-group",
Resource: "test-resource",
Namespace: "test-namespace",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionUpdated,
Folder: "test-folder",
},
expected: "test-group/test-resource/test-namespace/test-name/1934555792099250176~updated~test-folder",
},
{
name: "deleted key",
key: DataKey{
Group: "test-group",
Resource: "test-resource",
Namespace: "test-namespace",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionDeleted,
Folder: "test-folder",
},
expected: "test-group/test-resource/test-namespace/test-name/1934555792099250176~deleted~test-folder",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := tt.key.String()
require.Equal(t, tt.expected, actual)
})
}
}
func TestDataKey_Validate(t *testing.T) {
rv := int64(1234567890)
tests := []struct {
name string
key DataKey
expectError bool
errorMsg string
errorField string
}{
{
name: "valid key with created action",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - underscore in namespace",
key: DataKey{
Namespace: "test_namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid key with updated action",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionUpdated,
},
expectError: false,
},
{
name: "valid key with deleted action",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionDeleted,
},
expectError: false,
},
{
name: "valid - name ends with dash",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name-",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid key with minimum character lengths",
key: DataKey{
Namespace: "abc",
Group: "bcd",
Resource: "cde",
Name: "d",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid key with numbers",
key: DataKey{
Namespace: "namespace123",
Group: "group456",
Resource: "resource789",
Name: "name000",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - uppercase in name",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "Test-Name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - uppercase in namespace",
key: DataKey{
Namespace: "Test-Namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - uppercase in group",
key: DataKey{
Namespace: "test-namespace",
Group: "Test-Group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - uppercase in resource",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "Test-Resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
// Invalid cases - empty fields
{
name: "invalid - empty namespace",
key: DataKey{
Namespace: "",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorMsg: ErrNamespaceRequired,
},
{
name: "invalid - empty group",
key: DataKey{
Namespace: "test-namespace",
Group: "",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorField: "group",
},
{
name: "invalid - empty resource",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorField: "resource",
},
{
name: "invalid - empty name",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorField: "name",
},
{
name: "invalid - empty action",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: "",
},
expectError: true,
errorMsg: ErrActionRequired,
},
{
name: "invalid - all fields empty",
key: DataKey{
Namespace: "",
Group: "",
Resource: "",
Name: "",
ResourceVersion: rv,
Action: "",
},
expectError: true,
errorField: "namespace",
},
// Invalid cases - invalid characters
{
name: "invalid - key with dots and dashes",
key: DataKey{
Namespace: "test.namespace-with-dashes",
Group: "test.group-123",
Resource: "test-resource.v1",
Name: "test-name.with.dots",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorField: "namespace",
},
{
name: "invalid - space in group",
key: DataKey{
Namespace: "test-namespace",
Group: "test group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorMsg: "group 'test group' is invalid",
},
{
name: "invalid - special character in resource",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test@resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorMsg: "resource 'test@resource' is invalid",
},
// Name validation tests - K8s qualified name format
{
name: "valid - K8s format with underscores",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test_name_with_underscores",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - K8s format with dots",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test.name.with.dots",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - K8s format mixed case",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "TestName123",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - Legacy Grafana shortid format",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "a1B2c3D4e5F6g7H8",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - Legacy format with dashes and underscores",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name_with-mixed_chars123",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - Single character name",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "a",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - name starts with dash (legacy format)",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "-test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - name ends with dash (legacy format)",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name-",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - name starts with dot",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: ".test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - name ends with dot",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name.",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - name starts with underscore (legacy format)",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "_test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - name ends with underscore (legacy format)",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name_",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
// Invalid name cases
{
name: "invalid - name with slash",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test/name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorField: "name",
},
{
name: "invalid - name with spaces",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorField: "name",
},
{
name: "invalid - name with special characters",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test@name#with$special",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorField: "name",
},
{
name: "invalid - empty name",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorField: "name",
},
// Invalid cases - start/end with invalid characters
{
name: "invalid - namespace starts with dash",
key: DataKey{
Namespace: "-test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorMsg: "namespace '-test-namespace' is invalid",
},
{
name: "invalid - group ends with dot",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group.",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorMsg: "group 'test-group.' is invalid",
},
{
name: "invalid - resource starts with dot",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: ".test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorMsg: "resource '.test-resource' is invalid",
},
// Invalid cases - invalid action
{
name: "invalid - unknown action",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataAction("unknown"),
},
expectError: true,
errorMsg: "action 'unknown' is invalid",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.key.Validate()
if tt.expectError {
require.Error(t, err)
if tt.errorMsg != "" {
require.Contains(t, err.Error(), tt.errorMsg)
}
var validationErr *ValidationError
if errors.Is(err, validationErr) && tt.errorField != "" {
require.Equal(t, tt.errorField, validationErr.Field)
}
} else {
require.NoError(t, err)
}
})
}
}
func TestParseKey(t *testing.T) {
rv := node.Generate()
tests := []struct {
name string
key string
expected DataKey
expectError bool
}{
{
name: "valid normal key",
key: "test-group/test-resource/test-namespace/test-name/" + rv.String() + "~created~team-folder",
expected: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv.Int64(),
Action: DataActionCreated,
Folder: "team-folder",
},
},
{
name: "valid deleted key",
key: "test-group/test-resource/test-namespace/test-name/" + rv.String() + "~deleted~team-folder",
expected: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv.Int64(),
Action: DataActionDeleted,
Folder: "team-folder",
},
},
{
name: "invalid key - too short",
key: "test",
expectError: true,
},
{
name: "invalid key - too many slashes",
key: "test-group/test-resource/test-namespace/test-name/1934555792099250176~created~team-folder/extra-slash",
expectError: true,
},
{
name: "invalid key - invalid rv",
key: "test-group/test-resource/test-namespace/test-name/invalid-rv~team-folder",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual, err := ParseKey(tt.key)
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, tt.expected, actual)
}
})
}
}
func runDataStoreTestWith(t *testing.T, storeName string, newStoreFn func(*testing.T) *dataStore, testFn func(*testing.T, context.Context, *dataStore)) {
t.Run(storeName, func(t *testing.T) {
ctx := context.Background()
store := newStoreFn(t)
testFn(t, ctx, store)
})
}
func TestDataStore_Save_And_Get(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreSaveAndGet)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreSaveAndGet)
}
func testDataStoreSaveAndGet(t *testing.T, ctx context.Context, ds *dataStore) {
rv := node.Generate()
testKey := DataKey{
Group: "test-group",
Resource: "test-resource",
Namespace: "test-namespace",
Name: "test-name",
ResourceVersion: rv.Int64(),
Action: DataActionCreated,
}
testValue := bytes.NewReader([]byte("test-value"))
t.Run("save and get normal key", func(t *testing.T) {
err := ds.Save(ctx, testKey, testValue)
require.NoError(t, err)
result, err := ds.Get(ctx, testKey)
require.NoError(t, err)
// Read the content and compare
resultBytes, err := io.ReadAll(result)
require.NoError(t, err)
require.Equal(t, []byte("test-value"), resultBytes)
})
t.Run("save and get deleted key", func(t *testing.T) {
deletedKey := testKey
deletedKey.Action = DataActionDeleted
deletedValue := bytes.NewReader([]byte("deleted-value"))
err := ds.Save(ctx, deletedKey, deletedValue)
require.NoError(t, err)
result, err := ds.Get(ctx, deletedKey)
require.NoError(t, err)
// Read the content and compare
resultBytes, err := io.ReadAll(result)
require.NoError(t, err)
require.Equal(t, []byte("deleted-value"), resultBytes)
})
t.Run("get non-existent key", func(t *testing.T) {
rv := node.Generate()
nonExistentKey := DataKey{
Group: "test-group",
Resource: "test-resource",
Namespace: "non-existent",
Name: "test-name",
ResourceVersion: rv.Int64(),
Action: DataActionCreated,
}
_, err := ds.Get(ctx, nonExistentKey)
require.Error(t, err)
require.Equal(t, ErrNotFound, err)
})
}
func TestDataStore_Delete(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreDelete)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreDelete)
}
func testDataStoreDelete(t *testing.T, ctx context.Context, ds *dataStore) {
rv := node.Generate()
testKey := DataKey{
Group: "test-group",
Resource: "test-resource",
Namespace: "test-namespace",
Name: "test-name",
ResourceVersion: rv.Int64(),
Action: DataActionCreated,
}
testValue := bytes.NewReader([]byte("test-value"))
t.Run("delete existing key", func(t *testing.T) {
// First save the key
err := ds.Save(ctx, testKey, testValue)
require.NoError(t, err)
// Verify it exists
_, err = ds.Get(ctx, testKey)
require.NoError(t, err)
// Delete it
err = ds.Delete(ctx, testKey)
require.NoError(t, err)
// Verify it's gone
_, err = ds.Get(ctx, testKey)
require.Error(t, err)
require.Equal(t, ErrNotFound, err)
})
t.Run("delete non-existent key", func(t *testing.T) {
nonExistentKey := DataKey{
Namespace: "non-existent",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv.Int64(),
Action: DataActionCreated,
}
err := ds.Delete(ctx, nonExistentKey)
require.Error(t, err)
require.Equal(t, ErrNotFound, err)
})
}
func TestDataStore_List(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreList)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreList)
}
func testDataStoreList(t *testing.T, ctx context.Context, ds *dataStore) {
resourceKey := ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
}
// Create test data
rv1 := node.Generate()
rv2 := node.Generate()
testValue1 := bytes.NewReader([]byte("test-value-1"))
testValue2 := bytes.NewReader([]byte("test-value-2"))
dataKey1 := DataKey{
Namespace: resourceKey.Namespace,
Group: resourceKey.Group,
Resource: resourceKey.Resource,
Name: resourceKey.Name,
ResourceVersion: rv1.Int64(),
Action: DataActionCreated,
}
dataKey2 := DataKey{
Namespace: resourceKey.Namespace,
Group: resourceKey.Group,
Resource: resourceKey.Resource,
Name: resourceKey.Name,
ResourceVersion: rv2.Int64(),
Action: DataActionCreated,
}
t.Run("list multiple keys", func(t *testing.T) {
// Save test data
err := ds.Save(ctx, dataKey1, testValue1)
require.NoError(t, err)
err = ds.Save(ctx, dataKey2, testValue2)
require.NoError(t, err)
// List the data
results := make([]DataKey, 0, 2)
for key, err := range ds.Keys(ctx, resourceKey, SortOrderAsc) {
require.NoError(t, err)
results = append(results, key)
}
// Verify results
require.Len(t, results, 2)
// Check first result
result1 := results[0]
require.Equal(t, rv1.Int64(), result1.ResourceVersion)
require.Equal(t, resourceKey.Namespace, result1.Namespace)
require.Equal(t, resourceKey.Group, result1.Group)
require.Equal(t, resourceKey.Resource, result1.Resource)
require.Equal(t, DataActionCreated, result1.Action)
// Check second result
result2 := results[1]
require.Equal(t, rv2.Int64(), result2.ResourceVersion)
require.Equal(t, resourceKey.Namespace, result2.Namespace)
require.Equal(t, resourceKey.Group, result2.Group)
require.Equal(t, resourceKey.Resource, result2.Resource)
require.Equal(t, DataActionCreated, result2.Action)
})
t.Run("list empty", func(t *testing.T) {
emptyResourceKey := ListRequestKey{
Namespace: "empty-namespace",
Group: "empty-group",
Resource: "empty-resource",
Name: "empty-name",
}
results := make([]DataKey, 0, 1)
for key, err := range ds.Keys(ctx, emptyResourceKey, SortOrderAsc) {
require.NoError(t, err)
results = append(results, key)
}
require.Len(t, results, 0)
})
t.Run("list with deleted keys", func(t *testing.T) {
deletedResourceKey := ListRequestKey{
Namespace: "deleted-namespace",
Group: "deleted-group",
Resource: "deleted-resource",
Name: "deleted-name",
}
rv3 := node.Generate()
testValue3 := bytes.NewReader([]byte("deleted-value"))
deletedKey := DataKey{
Namespace: deletedResourceKey.Namespace,
Group: deletedResourceKey.Group,
Resource: deletedResourceKey.Resource,
Name: deletedResourceKey.Name,
ResourceVersion: rv3.Int64(),
Action: DataActionDeleted,
}
// Save deleted key
err := ds.Save(ctx, deletedKey, testValue3)
require.NoError(t, err)
// List should include deleted keys
results := make([]DataKey, 0, 2)
for key, err := range ds.Keys(ctx, deletedResourceKey, SortOrderAsc) {
require.NoError(t, err)
results = append(results, key)
}
require.Len(t, results, 1)
require.Equal(t, rv3.Int64(), results[0].ResourceVersion)
require.Equal(t, DataActionDeleted, results[0].Action)
})
}
func TestDataStore_Integration(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreIntegration)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreIntegration)
}
func testDataStoreIntegration(t *testing.T, ctx context.Context, ds *dataStore) {
t.Run("full lifecycle test", func(t *testing.T) {
resourceKey := ListRequestKey{
Namespace: "integration-ns",
Group: "integration-group",
Resource: "integration-resource",
Name: "integration-name",
}
rv1 := node.Generate()
rv2 := node.Generate()
rv3 := node.Generate()
// Create multiple versions
versions := []struct {
rv int64
value io.Reader
}{
{rv1.Int64(), bytes.NewReader([]byte("version-1"))},
{rv2.Int64(), bytes.NewReader([]byte("version-2"))},
{rv3.Int64(), bytes.NewReader([]byte("version-3"))},
}
// Save all versions
for _, version := range versions {
dataKey := DataKey{
Namespace: resourceKey.Namespace,
Group: resourceKey.Group,
Resource: resourceKey.Resource,
Name: resourceKey.Name,
ResourceVersion: version.rv,
Action: DataActionUpdated,
}
err := ds.Save(ctx, dataKey, version.value)
require.NoError(t, err)
}
// List all versions
results := make([]DataKey, 0, 3)
for key, err := range ds.Keys(ctx, resourceKey, SortOrderAsc) {
require.NoError(t, err)
results = append(results, key)
}
require.Len(t, results, 3)
// Delete one version
deleteKey := DataKey{
Namespace: resourceKey.Namespace,
Group: resourceKey.Group,
Resource: resourceKey.Resource,
Name: resourceKey.Name,
ResourceVersion: versions[1].rv,
Action: DataActionUpdated,
}
err := ds.Delete(ctx, deleteKey)
require.NoError(t, err)
// Verify it's gone
_, err = ds.Get(ctx, deleteKey)
require.Equal(t, ErrNotFound, err)
// List should now have 2 items
results = nil
for key, err := range ds.Keys(ctx, resourceKey, SortOrderAsc) {
require.NoError(t, err)
results = append(results, key)
}
require.Len(t, results, 2)
// Verify remaining items
remainingUUIDs := make(map[int64]bool)
for _, result := range results {
remainingUUIDs[result.ResourceVersion] = true
}
require.True(t, remainingUUIDs[versions[0].rv])
require.False(t, remainingUUIDs[versions[1].rv]) // deleted
require.True(t, remainingUUIDs[versions[2].rv])
})
}
func TestDataStore_Keys(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreKeys)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreKeys)
}
func testDataStoreKeys(t *testing.T, ctx context.Context, ds *dataStore) {
resourceKey := ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
}
// Create test data
rv1 := node.Generate()
rv2 := node.Generate()
rv3 := node.Generate()
testValue1 := bytes.NewReader([]byte("test-value-1"))
testValue2 := bytes.NewReader([]byte("test-value-2"))
testValue3 := bytes.NewReader([]byte("test-value-3"))
dataKey1 := DataKey{
Namespace: resourceKey.Namespace,
Group: resourceKey.Group,
Resource: resourceKey.Resource,
Name: resourceKey.Name,
ResourceVersion: rv1.Int64(),
Action: DataActionCreated,
}
dataKey2 := DataKey{
Namespace: resourceKey.Namespace,
Group: resourceKey.Group,
Resource: resourceKey.Resource,
Name: resourceKey.Name,
ResourceVersion: rv2.Int64(),
Action: DataActionUpdated,
}
dataKey3 := DataKey{
Namespace: resourceKey.Namespace,
Group: resourceKey.Group,
Resource: resourceKey.Resource,
Name: resourceKey.Name,
ResourceVersion: rv3.Int64(),
Action: DataActionDeleted,
}
t.Run("keys with multiple entries", func(t *testing.T) {
// Save test data
err := ds.Save(ctx, dataKey1, testValue1)
require.NoError(t, err)
err = ds.Save(ctx, dataKey2, testValue2)
require.NoError(t, err)
err = ds.Save(ctx, dataKey3, testValue3)
require.NoError(t, err)
// Get keys
keys := make([]DataKey, 0, 2)
for key, err := range ds.Keys(ctx, resourceKey, SortOrderAsc) {
require.NoError(t, err)
keys = append(keys, key)
}
// Verify results
require.Len(t, keys, 3)
// Verify all keys are present
expectedKeys := []DataKey{
dataKey1,
dataKey2,
dataKey3,
}
for _, expectedKey := range expectedKeys {
require.Contains(t, keys, expectedKey)
}
})
t.Run("keys with empty result", func(t *testing.T) {
emptyResourceKey := ListRequestKey{
Namespace: "empty-namespace",
Group: "empty-group",
Resource: "empty-resource",
Name: "empty-name",
}
keys := make([]DataKey, 0, 1)
for key, err := range ds.Keys(ctx, emptyResourceKey, SortOrderAsc) {
require.NoError(t, err)
keys = append(keys, key)
}
require.Len(t, keys, 0)
})
t.Run("keys with partial prefix matching", func(t *testing.T) {
// Create keys with different names but same namespace/group/resource
partialKey := ListRequestKey{
Namespace: resourceKey.Namespace,
Group: resourceKey.Group,
Resource: resourceKey.Resource,
// Name is empty, so it should match all names
}
rv4 := node.Generate()
dataKey4 := DataKey{
Namespace: resourceKey.Namespace,
Group: resourceKey.Group,
Resource: resourceKey.Resource,
Name: "different-name",
ResourceVersion: rv4.Int64(),
Action: DataActionCreated,
}
err := ds.Save(ctx, dataKey4, io.NopCloser(bytes.NewReader([]byte("different-value"))))
require.NoError(t, err)
keys := make([]DataKey, 0, 4)
for key, err := range ds.Keys(ctx, partialKey, SortOrderAsc) {
require.NoError(t, err)
keys = append(keys, key)
}
// Should include all keys with matching namespace/group/resource
require.Len(t, keys, 4) // 3 from previous test + 1 new one
// Verify the new key is included
require.Contains(t, keys, dataKey4)
})
t.Run("keys with group and resource only prefix", func(t *testing.T) {
groupAndResourceKey := ListRequestKey{
Group: "test-group",
Resource: "test-resource",
}
keys := make([]DataKey, 0, 4)
for key, err := range ds.Keys(ctx, groupAndResourceKey, SortOrderAsc) {
require.NoError(t, err)
keys = append(keys, key)
}
require.Len(t, keys, 4)
})
}
func TestDataStore_ValidationEnforced(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreValidationEnforced)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreValidationEnforced)
}
func testDataStoreValidationEnforced(t *testing.T, ctx context.Context, ds *dataStore) {
// Create an invalid key
invalidKey := DataKey{
Namespace: "Invalid-Namespace-$$$",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: 123456789,
Action: DataActionCreated,
}
testValue := io.NopCloser(bytes.NewReader([]byte("test-value")))
t.Run("Get with invalid key returns validation error", func(t *testing.T) {
_, err := ds.Get(ctx, invalidKey)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid data key")
var validationErr ValidationError
require.True(t, errors.As(err, &validationErr))
require.Equal(t, "namespace", validationErr.Field)
})
t.Run("Save with invalid key returns validation error", func(t *testing.T) {
err := ds.Save(ctx, invalidKey, testValue)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid data key")
var validationErr ValidationError
require.True(t, errors.As(err, &validationErr))
require.Equal(t, "namespace", validationErr.Field)
})
t.Run("Delete with invalid key returns validation error", func(t *testing.T) {
err := ds.Delete(ctx, invalidKey)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid data key")
var validationErr ValidationError
require.True(t, errors.As(err, &validationErr))
require.Equal(t, "namespace", validationErr.Field)
})
// Test another type of invalid key
emptyFieldKey := DataKey{
Namespace: "",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: 123456789,
Action: DataActionCreated,
}
t.Run("Get with empty namespace returns validation error", func(t *testing.T) {
_, err := ds.Get(ctx, emptyFieldKey)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid data key")
require.Contains(t, err.Error(), "namespace is required")
})
t.Run("Save with empty namespace returns validation error", func(t *testing.T) {
err := ds.Save(ctx, emptyFieldKey, testValue)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid data key")
require.Contains(t, err.Error(), "namespace is required")
})
t.Run("Delete with empty namespace returns validation error", func(t *testing.T) {
err := ds.Delete(ctx, emptyFieldKey)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid data key")
require.Contains(t, err.Error(), "namespace is required")
})
}
func TestListRequestKey_Validate(t *testing.T) {
tests := []struct {
name string
key ListRequestKey
expectError bool
errorMsg string
errorField string
}{
{
name: "valid - all fields provided",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
},
expectError: false,
},
{
name: "valid - uppercase in namespace",
key: ListRequestKey{
Namespace: "Test-Namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
},
expectError: false,
},
{
name: "valid - uppercase in group and resource",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "Test-Group",
Resource: "test-resource",
Name: "test-name",
},
expectError: false,
},
{
name: "valid - uppercase in resource",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "Test-Resource",
},
expectError: false,
},
{
name: "valid - underscore in namespace",
key: ListRequestKey{
Namespace: "test_namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
},
expectError: false,
},
{
name: "valid - only group and resource",
key: ListRequestKey{
Group: "test-group",
Resource: "test-resource",
},
expectError: false,
},
{
name: "valid - namespace and group and resource",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
},
expectError: false,
},
{
name: "invalid - all empty",
key: ListRequestKey{},
expectError: true,
errorField: "namespace",
},
{
name: "valid - legacy grafana uid 1",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "_4OV_5Nmz",
},
expectError: false,
},
{
name: "valid - legacy grafana uid 2",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "-Y-tnEDWk",
},
expectError: false,
},
{
name: "valid - legacy grafana uid 3",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "000000005",
},
expectError: false,
},
{
name: "valid - uppercase in name",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "Test-Name",
},
expectError: false,
},
// Invalid hierarchical cases
{
name: "invalid - group without resource",
key: ListRequestKey{
Group: "test-group",
},
expectError: true,
errorField: "resource",
},
{
name: "invalid - name without namespace",
key: ListRequestKey{
Name: "test-name",
Resource: "test-resource",
Group: "test-group",
},
expectError: true,
errorMsg: ErrNameMustBeEmptyWhenNamespaceEmpty,
},
{
name: "invalid - name without group and resource",
key: ListRequestKey{
Namespace: "test-namespace",
Name: "test-name",
},
expectError: true,
errorField: "group",
},
// Invalid naming cases
{
name: "invalid - starts with dash",
key: ListRequestKey{
Namespace: "-test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
},
expectError: true,
errorMsg: "namespace '-test-namespace' is invalid",
},
{
name: "invalid - ends with dot",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "test-group.",
Resource: "test-resource",
Name: "test-name",
},
expectError: true,
errorMsg: "group 'test-group.' is invalid",
},
{
name: "invalid - name contains invalid char",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "test-group.",
Resource: "test-resource",
Name: "test$name",
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.key.Validate()
if tt.expectError {
require.Error(t, err)
if tt.errorMsg != "" {
require.Contains(t, err.Error(), tt.errorMsg)
}
} else {
require.NoError(t, err)
}
})
}
}
func TestListRequestKey_Prefix(t *testing.T) {
tests := []struct {
name string
key ListRequestKey
expected string
}{
{
name: "all fields provided",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
},
expected: "test-group/test-resource/test-namespace/test-name/",
},
{
name: "name is empty",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "",
},
expected: "test-group/test-resource/test-namespace/",
},
{
name: "namespace is empty",
key: ListRequestKey{
Group: "test-group",
Namespace: "",
Resource: "test-resource",
Name: "",
},
expected: "test-group/test-resource/",
},
{
name: "fields with special characters",
key: ListRequestKey{
Namespace: "test-namespace-with-dashes",
Group: "test.group.with.dots",
Resource: "test-resource",
Name: "test-name-with-multiple.special-chars",
},
expected: "test.group.with.dots/test-resource/test-namespace-with-dashes/test-name-with-multiple.special-chars/",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := tt.key.Prefix()
require.Equal(t, tt.expected, actual)
})
}
}
func TestDataStore_LastResourceVersion(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreLastResourceVersion)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreLastResourceVersion)
}
func testDataStoreLastResourceVersion(t *testing.T, ctx context.Context, ds *dataStore) {
t.Run("returns last resource version for existing data", func(t *testing.T) {
resourceKey := ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
}
// Create test data with multiple versions
rv1 := node.Generate()
rv2 := node.Generate()
rv3 := node.Generate()
versions := []int64{
rv1.Int64(),
rv2.Int64(),
rv3.Int64(),
}
// Save all versions
for _, version := range versions {
dataKey := DataKey{
Namespace: resourceKey.Namespace,
Group: resourceKey.Group,
Resource: resourceKey.Resource,
Name: resourceKey.Name,
ResourceVersion: version,
Action: DataActionCreated,
}
err := ds.Save(ctx, dataKey, bytes.NewReader([]byte(fmt.Sprintf("version-%d", version))))
require.NoError(t, err)
}
// Get the last resource version
lastKey, err := ds.LastResourceVersion(ctx, resourceKey)
require.NoError(t, err)
// Verify the result
require.Equal(t, resourceKey.Namespace, lastKey.Namespace)
require.Equal(t, resourceKey.Group, lastKey.Group)
require.Equal(t, resourceKey.Resource, lastKey.Resource)
require.Equal(t, resourceKey.Name, lastKey.Name)
require.Equal(t, DataActionCreated, lastKey.Action)
require.Equal(t, rv3.Int64(), lastKey.ResourceVersion)
})
t.Run("returns error for non-existent resource", func(t *testing.T) {
nonExistentKey := ListRequestKey{
Namespace: "non-existent-namespace",
Group: "non-existent-group",
Resource: "non-existent-resource",
Name: "non-existent-name",
}
_, err := ds.LastResourceVersion(ctx, nonExistentKey)
require.Error(t, err)
require.Equal(t, ErrNotFound, err)
})
t.Run("returns error for empty required fields", func(t *testing.T) {
testCases := map[string]ListRequestKey{
"empty namespace": {
Namespace: "",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
},
"empty group": {
Namespace: "test-namespace",
Group: "",
Resource: "test-resource",
Name: "test-name",
},
"empty resource": {
Namespace: "test-namespace",
Group: "test-group",
Resource: "",
Name: "test-name",
},
"empty name": {
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "",
},
}
for name, key := range testCases {
t.Run(name, func(t *testing.T) {
_, err := ds.LastResourceVersion(ctx, key)
require.Error(t, err)
})
}
})
}
func TestDataStore_GetLatestResourceKey(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreGetLatestResourceKey)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreGetLatestResourceKey)
}
func testDataStoreGetLatestResourceKey(t *testing.T, ctx context.Context, ds *dataStore) {
key := GetRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
}
// Create multiple versions with different timestamps
rv1 := node.Generate().Int64()
rv2 := node.Generate().Int64()
rv3 := node.Generate().Int64()
// Save multiple versions (rv3 should be latest)
dataKey1 := DataKey{
Group: key.Group,
Resource: key.Resource,
Namespace: key.Namespace,
Name: key.Name,
ResourceVersion: rv1,
Action: DataActionCreated,
Folder: "test-folder",
}
dataKey2 := DataKey{
Group: key.Group,
Resource: key.Resource,
Namespace: key.Namespace,
Name: key.Name,
ResourceVersion: rv2,
Action: DataActionUpdated,
Folder: "test-folder",
}
dataKey3 := DataKey{
Group: key.Group,
Resource: key.Resource,
Namespace: key.Namespace,
Name: key.Name,
ResourceVersion: rv3,
Action: DataActionUpdated,
Folder: "test-folder",
}
err := ds.Save(ctx, dataKey1, bytes.NewReader([]byte("version1")))
require.NoError(t, err)
err = ds.Save(ctx, dataKey2, bytes.NewReader([]byte("version2")))
require.NoError(t, err)
err = ds.Save(ctx, dataKey3, bytes.NewReader([]byte("version3")))
require.NoError(t, err)
// GetLatestResourceKey should return rv3
latestKey, err := ds.GetLatestResourceKey(ctx, key)
require.NoError(t, err)
require.Equal(t, dataKey3, latestKey)
require.Equal(t, rv3, latestKey.ResourceVersion)
require.Equal(t, DataActionUpdated, latestKey.Action)
}
func TestDataStore_GetLatestResourceKey_Deleted(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreGetLatestResourceKeyDeleted)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreGetLatestResourceKeyDeleted)
}
func testDataStoreGetLatestResourceKeyDeleted(t *testing.T, ctx context.Context, ds *dataStore) {
key := GetRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
}
dataKey := DataKey{
Group: key.Group,
Resource: key.Resource,
Namespace: key.Namespace,
Name: key.Name,
ResourceVersion: node.Generate().Int64(),
Action: DataActionDeleted,
Folder: "test-folder",
}
err := ds.Save(ctx, dataKey, bytes.NewReader([]byte("deleted")))
require.NoError(t, err)
_, err = ds.GetLatestResourceKey(ctx, key)
require.Equal(t, ErrNotFound, err)
}
func TestDataStore_GetLatestResourceKey_NotFound(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreGetLatestResourceKeyNotFound)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreGetLatestResourceKeyNotFound)
}
func testDataStoreGetLatestResourceKeyNotFound(t *testing.T, ctx context.Context, ds *dataStore) {
key := GetRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "non-existent",
}
_, err := ds.GetLatestResourceKey(ctx, key)
require.Equal(t, ErrNotFound, err)
}
func TestDataStore_GetResourceKeyAtRevision(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreGetResourceKeyAtRevision)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreGetResourceKeyAtRevision)
}
func testDataStoreGetResourceKeyAtRevision(t *testing.T, ctx context.Context, ds *dataStore) {
key := GetRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
}
// Create multiple versions
rv1 := node.Generate().Int64()
rv2 := node.Generate().Int64()
rv3 := node.Generate().Int64()
dataKey1 := DataKey{
Group: key.Group,
Resource: key.Resource,
Namespace: key.Namespace,
Name: key.Name,
ResourceVersion: rv1,
Action: DataActionCreated,
Folder: "test-folder",
}
dataKey2 := DataKey{
Group: key.Group,
Resource: key.Resource,
Namespace: key.Namespace,
Name: key.Name,
ResourceVersion: rv2,
Action: DataActionUpdated,
Folder: "test-folder",
}
dataKey3 := DataKey{
Group: key.Group,
Resource: key.Resource,
Namespace: key.Namespace,
Name: key.Name,
ResourceVersion: rv3,
Action: DataActionUpdated,
Folder: "test-folder",
}
err := ds.Save(ctx, dataKey1, bytes.NewReader([]byte("version1")))
require.NoError(t, err)
err = ds.Save(ctx, dataKey2, bytes.NewReader([]byte("version2")))
require.NoError(t, err)
err = ds.Save(ctx, dataKey3, bytes.NewReader([]byte("version3")))
require.NoError(t, err)
// Get key at rv2 should return rv2
dataKey, err := ds.GetResourceKeyAtRevision(ctx, key, rv2)
require.NoError(t, err)
require.Equal(t, rv2, dataKey.ResourceVersion)
require.Equal(t, DataActionUpdated, dataKey.Action)
// Get key at rv1 should return rv1
dataKey, err = ds.GetResourceKeyAtRevision(ctx, key, rv1)
require.NoError(t, err)
require.Equal(t, rv1, dataKey.ResourceVersion)
require.Equal(t, DataActionCreated, dataKey.Action)
// Get key at revision 0 should return latest (rv3)
dataKey, err = ds.GetResourceKeyAtRevision(ctx, key, 0)
require.NoError(t, err)
require.Equal(t, rv3, dataKey.ResourceVersion)
require.Equal(t, DataActionUpdated, dataKey.Action)
}
func TestDataStore_ListLatestResourceKeys(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreListLatestResourceKeys)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreListLatestResourceKeys)
}
func testDataStoreListLatestResourceKeys(t *testing.T, ctx context.Context, ds *dataStore) {
listKey := ListRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
}
// Save multiple versions - ListLatestResourceKeys should return only the latest
rv1 := node.Generate().Int64()
rv2 := node.Generate().Int64()
dataKey1 := DataKey{
Group: listKey.Group,
Resource: listKey.Resource,
Namespace: listKey.Namespace,
Name: listKey.Name,
ResourceVersion: rv1,
Action: DataActionCreated,
Folder: "test-folder",
}
dataKey2 := DataKey{
Group: listKey.Group,
Resource: listKey.Resource,
Namespace: listKey.Namespace,
Name: listKey.Name,
ResourceVersion: rv2,
Action: DataActionUpdated,
Folder: "test-folder",
}
err := ds.Save(ctx, dataKey1, bytes.NewReader([]byte("version1")))
require.NoError(t, err)
err = ds.Save(ctx, dataKey2, bytes.NewReader([]byte("version2")))
require.NoError(t, err)
// List latest resource keys
resultKeys := make([]DataKey, 0, 1)
for dataKey, err := range ds.ListLatestResourceKeys(ctx, listKey) {
require.NoError(t, err)
resultKeys = append(resultKeys, dataKey)
}
require.Len(t, resultKeys, 1)
require.Equal(t, dataKey2, resultKeys[0])
require.Equal(t, rv2, resultKeys[0].ResourceVersion)
require.Equal(t, DataActionUpdated, resultKeys[0].Action)
}
func TestDataStore_ListLatestResourceKeys_Deleted(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreListLatestResourceKeysDeleted)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreListLatestResourceKeysDeleted)
}
func testDataStoreListLatestResourceKeysDeleted(t *testing.T, ctx context.Context, ds *dataStore) {
listKey := ListRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
}
// Save a resource and then delete it
rv1 := node.Generate().Int64()
rv2 := node.Generate().Int64()
dataKey1 := DataKey{
Group: listKey.Group,
Resource: listKey.Resource,
Namespace: listKey.Namespace,
Name: listKey.Name,
ResourceVersion: rv1,
Action: DataActionCreated,
Folder: "test-folder",
}
dataKey2 := DataKey{
Group: listKey.Group,
Resource: listKey.Resource,
Namespace: listKey.Namespace,
Name: listKey.Name,
ResourceVersion: rv2,
Action: DataActionDeleted,
Folder: "test-folder",
}
err := ds.Save(ctx, dataKey1, bytes.NewReader([]byte("version1")))
require.NoError(t, err)
err = ds.Save(ctx, dataKey2, bytes.NewReader([]byte("deleted")))
require.NoError(t, err)
// ListLatestResourceKeys should exclude deleted resources
resultKeys := make([]DataKey, 0, 1)
for dataKey, err := range ds.ListLatestResourceKeys(ctx, listKey) {
require.NoError(t, err)
resultKeys = append(resultKeys, dataKey)
}
require.Len(t, resultKeys, 0) // Should be empty because resource was deleted
}
func TestDataStore_ListLatestResourceKeys_Multiple(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreListLatestResourceKeysMultiple)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreListLatestResourceKeysMultiple)
}
func testDataStoreListLatestResourceKeysMultiple(t *testing.T, ctx context.Context, ds *dataStore) {
listKey := ListRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
}
// Save multiple resources with different names
rv1 := node.Generate().Int64()
rv2 := node.Generate().Int64()
rv3 := node.Generate().Int64()
dataKey1 := DataKey{
Group: listKey.Group,
Resource: listKey.Resource,
Namespace: listKey.Namespace,
Name: "resource-1",
ResourceVersion: rv1,
Action: DataActionCreated,
Folder: "test-folder",
}
dataKey2 := DataKey{
Group: listKey.Group,
Resource: listKey.Resource,
Namespace: listKey.Namespace,
Name: "resource-2",
ResourceVersion: rv2,
Action: DataActionCreated,
Folder: "test-folder",
}
dataKey3 := DataKey{
Group: listKey.Group,
Resource: listKey.Resource,
Namespace: listKey.Namespace,
Name: "resource-1",
ResourceVersion: rv3,
Action: DataActionUpdated,
Folder: "test-folder",
}
err := ds.Save(ctx, dataKey1, bytes.NewReader([]byte("resource-1-v1")))
require.NoError(t, err)
err = ds.Save(ctx, dataKey2, bytes.NewReader([]byte("resource-2-v1")))
require.NoError(t, err)
err = ds.Save(ctx, dataKey3, bytes.NewReader([]byte("resource-1-v2")))
require.NoError(t, err)
// List latest resource keys for all resources
resultKeys := make([]DataKey, 0, 3)
for dataKey, err := range ds.ListLatestResourceKeys(ctx, listKey) {
require.NoError(t, err)
resultKeys = append(resultKeys, dataKey)
}
require.Len(t, resultKeys, 2) // resource-1 (latest version) and resource-2
// Check we got the correct keys
names := make(map[string]int64)
for _, key := range resultKeys {
names[key.Name] = key.ResourceVersion
}
require.Equal(t, rv3, names["resource-1"]) // Should be the updated version
require.Equal(t, rv2, names["resource-2"])
}
func TestDataStore_ListResourceKeysAtRevision(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreListResourceKeysAtRevision)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreListResourceKeysAtRevision)
}
func testDataStoreListResourceKeysAtRevision(t *testing.T, ctx context.Context, ds *dataStore) {
// Create multiple resources with different versions
rv1 := node.Generate().Int64()
rv2 := node.Generate().Int64()
rv3 := node.Generate().Int64()
rv4 := node.Generate().Int64()
rv5 := node.Generate().Int64()
// Resource 1: Created at rv1, updated at rv3
key1 := DataKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "resource1",
ResourceVersion: rv1,
Action: DataActionCreated,
Folder: "test-folder",
}
err := ds.Save(ctx, key1, bytes.NewReader([]byte("resource1-v1")))
require.NoError(t, err)
key1Updated := key1
key1Updated.ResourceVersion = rv3
key1Updated.Action = DataActionUpdated
err = ds.Save(ctx, key1Updated, bytes.NewReader([]byte("resource1-v2")))
require.NoError(t, err)
// Resource 2: Created at rv2
key2 := DataKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "resource2",
ResourceVersion: rv2,
Action: DataActionCreated,
Folder: "test-folder",
}
err = ds.Save(ctx, key2, bytes.NewReader([]byte("resource2-v1")))
require.NoError(t, err)
// Resource 3: Created at rv4
key3 := DataKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "resource3",
ResourceVersion: rv4,
Action: DataActionCreated,
Folder: "test-folder",
}
err = ds.Save(ctx, key3, bytes.NewReader([]byte("resource3-v1")))
require.NoError(t, err)
// Resource 4: Created at rv2, deleted at rv5
key4 := DataKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "resource4",
ResourceVersion: rv2,
Action: DataActionCreated,
Folder: "test-folder",
}
err = ds.Save(ctx, key4, bytes.NewReader([]byte("resource4-v1")))
require.NoError(t, err)
key4Deleted := key4
key4Deleted.ResourceVersion = rv5
key4Deleted.Action = DataActionDeleted
err = ds.Save(ctx, key4Deleted, bytes.NewReader([]byte("resource4-deleted")))
require.NoError(t, err)
listKey := ListRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
}
t.Run("list at revision rv1 - should return only resource1 initial version", func(t *testing.T) {
resultKeys := make([]DataKey, 0, 2)
for dataKey, err := range ds.ListResourceKeysAtRevision(ctx, ListRequestOptions{Key: listKey, ResourceVersion: rv1}) {
require.NoError(t, err)
resultKeys = append(resultKeys, dataKey)
}
require.Len(t, resultKeys, 1)
require.Equal(t, "resource1", resultKeys[0].Name)
require.Equal(t, rv1, resultKeys[0].ResourceVersion)
require.Equal(t, DataActionCreated, resultKeys[0].Action)
})
t.Run("list at revision rv2 - should return resource1, resource2 and resource4", func(t *testing.T) {
resultKeys := make([]DataKey, 0, 3)
for dataKey, err := range ds.ListResourceKeysAtRevision(ctx, ListRequestOptions{Key: listKey, ResourceVersion: rv2}) {
require.NoError(t, err)
resultKeys = append(resultKeys, dataKey)
}
require.Len(t, resultKeys, 3) // resource1, resource2, resource4
names := make(map[string]int64)
for _, result := range resultKeys {
names[result.Name] = result.ResourceVersion
}
require.Equal(t, rv1, names["resource1"]) // Should be the original version
require.Equal(t, rv2, names["resource2"])
require.Equal(t, rv2, names["resource4"])
})
t.Run("list at revision rv3 - should return resource1, resource2 and resource4", func(t *testing.T) {
resultKeys := make([]DataKey, 0, 3)
for dataKey, err := range ds.ListResourceKeysAtRevision(ctx, ListRequestOptions{Key: listKey, ResourceVersion: rv3}) {
require.NoError(t, err)
resultKeys = append(resultKeys, dataKey)
}
require.Len(t, resultKeys, 3) // resource1 (updated), resource2, resource4
names := make(map[string]int64)
actions := make(map[string]DataAction)
for _, result := range resultKeys {
names[result.Name] = result.ResourceVersion
actions[result.Name] = result.Action
}
require.Equal(t, rv3, names["resource1"]) // Should be the updated version
require.Equal(t, DataActionUpdated, actions["resource1"])
require.Equal(t, rv2, names["resource2"])
require.Equal(t, rv2, names["resource4"])
})
t.Run("list at revision rv4 - should return all resources", func(t *testing.T) {
resultKeys := make([]DataKey, 0, 4)
for dataKey, err := range ds.ListResourceKeysAtRevision(ctx, ListRequestOptions{Key: listKey, ResourceVersion: rv4}) {
require.NoError(t, err)
resultKeys = append(resultKeys, dataKey)
}
require.Len(t, resultKeys, 4) // resource1 (updated), resource2, resource3, resource4
names := make(map[string]int64)
for _, result := range resultKeys {
names[result.Name] = result.ResourceVersion
}
require.Equal(t, rv3, names["resource1"])
require.Equal(t, rv2, names["resource2"])
require.Equal(t, rv4, names["resource3"])
require.Equal(t, rv2, names["resource4"])
})
t.Run("list at revision rv5 - should exclude deleted resource4", func(t *testing.T) {
resultKeys := make([]DataKey, 0, 3)
for dataKey, err := range ds.ListResourceKeysAtRevision(ctx, ListRequestOptions{Key: listKey, ResourceVersion: rv5}) {
require.NoError(t, err)
resultKeys = append(resultKeys, dataKey)
}
require.Len(t, resultKeys, 3) // resource1 (updated), resource2, resource3 (resource4 excluded because deleted)
names := make(map[string]bool)
for _, result := range resultKeys {
names[result.Name] = true
}
require.True(t, names["resource1"])
require.True(t, names["resource2"])
require.True(t, names["resource3"])
require.False(t, names["resource4"]) // Should be excluded because it's deleted
})
t.Run("list with specific resource name", func(t *testing.T) {
specificListKey := ListRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "resource1",
}
resultKeys := make([]DataKey, 0, 2)
for dataKey, err := range ds.ListResourceKeysAtRevision(ctx, ListRequestOptions{Key: specificListKey, ResourceVersion: rv3}) {
require.NoError(t, err)
resultKeys = append(resultKeys, dataKey)
}
require.Len(t, resultKeys, 1)
require.Equal(t, "resource1", resultKeys[0].Name)
require.Equal(t, rv3, resultKeys[0].ResourceVersion)
require.Equal(t, DataActionUpdated, resultKeys[0].Action)
})
t.Run("list at revision 0 should use MaxInt64", func(t *testing.T) {
resultKeys := make([]DataKey, 0, 4)
for dataKey, err := range ds.ListResourceKeysAtRevision(ctx, ListRequestOptions{Key: listKey, ResourceVersion: 0}) {
require.NoError(t, err)
resultKeys = append(resultKeys, dataKey)
}
// Should return all non-deleted resources at their latest versions
require.Len(t, resultKeys, 3) // resource1 (updated), resource2, resource3
names := make(map[string]bool)
for _, result := range resultKeys {
names[result.Name] = true
}
require.True(t, names["resource1"])
require.True(t, names["resource2"])
require.True(t, names["resource3"])
require.False(t, names["resource4"]) // Excluded because deleted
})
}
func TestDataStore_ListResourceKeysAtRevision_ValidationErrors(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreListResourceKeysAtRevisionValidationErrors)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreListResourceKeysAtRevisionValidationErrors)
}
func testDataStoreListResourceKeysAtRevisionValidationErrors(t *testing.T, ctx context.Context, ds *dataStore) {
tests := []struct {
name string
key ListRequestKey
}{
{
name: "missing group",
key: ListRequestKey{
Namespace: "default",
Resource: "resources",
},
},
{
name: "missing resource",
key: ListRequestKey{
Namespace: "default",
Group: "apps",
},
},
{
name: "name without namespace",
key: ListRequestKey{
Group: "apps",
Resource: "resources",
Name: "test-name",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, err := range ds.ListResourceKeysAtRevision(ctx, ListRequestOptions{Key: tt.key, ResourceVersion: 0}) {
require.Error(t, err)
return
}
})
}
}
func TestDataStore_ListResourceKeysAtRevision_EmptyResults(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreListResourceKeysAtRevisionEmptyResults)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreListResourceKeysAtRevisionEmptyResults)
}
func testDataStoreListResourceKeysAtRevisionEmptyResults(t *testing.T, ctx context.Context, ds *dataStore) {
listKey := ListRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "empty",
}
resultKeys := make([]DataKey, 0, 1)
for dataKey, err := range ds.ListResourceKeysAtRevision(ctx, ListRequestOptions{Key: listKey, ResourceVersion: 0}) {
require.NoError(t, err)
resultKeys = append(resultKeys, dataKey)
}
require.Len(t, resultKeys, 0)
}
func TestDataStore_ListResourceKeysAtRevision_ResourcesNewerThanRevision(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreListResourceKeysAtRevisionResourcesNewerThanRevision)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreListResourceKeysAtRevisionResourcesNewerThanRevision)
}
func testDataStoreListResourceKeysAtRevisionResourcesNewerThanRevision(t *testing.T, ctx context.Context, ds *dataStore) {
// Create a resource with a high resource version
rv := node.Generate().Int64()
key := DataKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "future-resource",
ResourceVersion: rv,
Action: DataActionCreated,
Folder: "test-folder",
}
err := ds.Save(ctx, key, bytes.NewReader([]byte("future-resource")))
require.NoError(t, err)
listKey := ListRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
}
// List at a revision before the resource was created
resultKeys := make([]DataKey, 0, 1)
for dataKey, err := range ds.ListResourceKeysAtRevision(ctx, ListRequestOptions{Key: listKey, ResourceVersion: rv - 1000}) {
require.NoError(t, err)
resultKeys = append(resultKeys, dataKey)
}
// Should return no results since the resource is newer than the target revision
require.Len(t, resultKeys, 0)
}
func TestDataKey_Equals(t *testing.T) {
baseKey := DataKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
ResourceVersion: 123,
Action: DataActionCreated,
Folder: "test-folder",
}
tests := []struct {
name string
key1 DataKey
key2 DataKey
expected bool
}{
{
name: "identical keys",
key1: baseKey,
key2: baseKey,
expected: true,
},
{
name: "different resource version",
key1: baseKey,
key2: DataKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
ResourceVersion: 456,
Action: DataActionCreated,
Folder: "test-folder",
},
expected: false,
},
{
name: "different action",
key1: baseKey,
key2: DataKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
ResourceVersion: 123,
Action: DataActionUpdated,
Folder: "test-folder",
},
expected: false,
},
{
name: "different folder",
key1: baseKey,
key2: DataKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
ResourceVersion: 123,
Action: DataActionCreated,
Folder: "other-folder",
},
expected: false,
},
{
name: "different namespace",
key1: baseKey,
key2: DataKey{
Group: "apps",
Resource: "resources",
Namespace: "other-namespace",
Name: "test-resource",
ResourceVersion: 123,
Action: DataActionCreated,
Folder: "test-folder",
},
expected: false,
},
{
name: "different group",
key1: baseKey,
key2: DataKey{
Group: "extensions",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
ResourceVersion: 123,
Action: DataActionCreated,
Folder: "test-folder",
},
expected: false,
},
{
name: "different resource",
key1: baseKey,
key2: DataKey{
Group: "apps",
Resource: "services",
Namespace: "default",
Name: "test-resource",
ResourceVersion: 123,
Action: DataActionCreated,
Folder: "test-folder",
},
expected: false,
},
{
name: "different name",
key1: baseKey,
key2: DataKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "other-deployment",
ResourceVersion: 123,
Action: DataActionCreated,
Folder: "test-folder",
},
expected: false,
},
{
name: "empty keys",
key1: DataKey{},
key2: DataKey{},
expected: true,
},
{
name: "one empty key",
key1: baseKey,
key2: DataKey{},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.key1.Equals(tt.key2)
require.Equal(t, tt.expected, result)
// Test symmetry: Equals should be commutative
reverseResult := tt.key2.Equals(tt.key1)
require.Equal(t, result, reverseResult, "Equals method should be commutative")
})
}
}
func TestDataKey_SameResource(t *testing.T) {
baseKey := DataKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
ResourceVersion: 123,
Action: DataActionCreated,
Folder: "test-folder",
}
tests := []struct {
name string
key1 DataKey
key2 DataKey
expected bool
}{
{
name: "identical keys",
key1: baseKey,
key2: baseKey,
expected: true,
},
{
name: "same identifying fields, different resource version",
key1: baseKey,
key2: DataKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
ResourceVersion: 456, // Different resource version
Action: DataActionUpdated,
Folder: "other-folder",
},
expected: true, // Should still be equal as ResourceVersion, Action, and Folder don't matter
},
{
name: "different namespace",
key1: baseKey,
key2: DataKey{
Group: "apps",
Resource: "resources",
Namespace: "other-namespace",
Name: "test-resource",
ResourceVersion: 123,
Action: DataActionCreated,
Folder: "test-folder",
},
expected: false,
},
{
name: "different group",
key1: baseKey,
key2: DataKey{
Group: "extensions",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
ResourceVersion: 123,
Action: DataActionCreated,
Folder: "test-folder",
},
expected: false,
},
{
name: "different resource",
key1: baseKey,
key2: DataKey{
Group: "apps",
Resource: "services",
Namespace: "default",
Name: "test-resource",
ResourceVersion: 123,
Action: DataActionCreated,
Folder: "test-folder",
},
expected: false,
},
{
name: "different name",
key1: baseKey,
key2: DataKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "other-deployment",
ResourceVersion: 123,
Action: DataActionCreated,
Folder: "test-folder",
},
expected: false,
},
{
name: "empty keys",
key1: DataKey{},
key2: DataKey{},
expected: true,
},
{
name: "one empty key",
key1: baseKey,
key2: DataKey{},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.key1.SameResource(tt.key2)
require.Equal(t, tt.expected, result)
// Test symmetry: SameResource should be commutative
reverseResult := tt.key2.SameResource(tt.key1)
require.Equal(t, result, reverseResult, "SameResource method should be commutative")
})
}
}
func TestGetRequestKey_Validate(t *testing.T) {
tests := []struct {
name string
key GetRequestKey
expectErr bool
wantError string
errorField string
}{
{
name: "valid key",
key: GetRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
},
expectErr: false,
},
{
name: "valid key with dots and dashes",
key: GetRequestKey{
Group: "apps.v1",
Resource: "deployment-configs",
Namespace: "default-ns",
Name: "test-resource.v1",
},
expectErr: false,
},
{
name: "valid grafana name - ends with dot",
key: GetRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: ".123_hello",
},
expectErr: false,
},
{
name: "missing group",
key: GetRequestKey{
Resource: "resources",
Namespace: "default",
Name: "test-resource",
},
expectErr: true,
errorField: "group",
},
{
name: "missing resource",
key: GetRequestKey{
Group: "apps",
Namespace: "default",
Name: "test-resource",
},
expectErr: true,
errorField: "resource",
},
{
name: "missing namespace",
key: GetRequestKey{
Group: "apps",
Resource: "resources",
Name: "test-resource",
},
expectErr: true,
errorField: "namespace",
},
{
name: "missing name",
key: GetRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
},
expectErr: true,
errorField: "name",
},
{
name: "invalid group - underscore at start",
key: GetRequestKey{
Group: "_apps_v1",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
},
expectErr: true,
errorField: "group",
},
{
name: "invalid resource - starts with dash",
key: GetRequestKey{
Group: "apps",
Resource: "-resources",
Namespace: "default",
Name: "test-resource",
},
expectErr: true,
errorField: "resource",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.key.Validate()
if tt.expectErr {
require.Error(t, err)
if tt.wantError != "" {
require.Contains(t, err.Error(), tt.wantError)
}
var validationErr *ValidationError
if errors.Is(err, validationErr) && tt.errorField != "" {
require.Equal(t, tt.errorField, validationErr.Field)
}
} else {
require.NoError(t, err)
}
})
}
}
func TestGetRequestKey_Prefix(t *testing.T) {
tests := []struct {
name string
key GetRequestKey
expectedPrefix string
}{
{
name: "standard key",
key: GetRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
},
expectedPrefix: "apps/resources/default/test-resource/",
},
{
name: "key with special characters",
key: GetRequestKey{
Group: "apps.v1",
Resource: "deployment-configs",
Namespace: "system-namespace",
Name: "my-app.v2",
},
expectedPrefix: "apps.v1/deployment-configs/system-namespace/my-app.v2/",
},
{
name: "key with single character fields",
key: GetRequestKey{
Group: "a",
Resource: "b",
Namespace: "c",
Name: "d",
},
expectedPrefix: "a/b/c/d/",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prefix := tt.key.Prefix()
require.Equal(t, tt.expectedPrefix, prefix)
})
}
}
func TestDataStore_GetResourceStats_Comprehensive(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreGetResourceStatsComprehensive)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreGetResourceStatsComprehensive)
}
func testDataStoreGetResourceStatsComprehensive(t *testing.T, ctx context.Context, ds *dataStore) {
// Test setup: 3 namespaces × 3 groups × 3 resources × 3 names × 3 versions = 243 total entries
// But each name will have only 1 latest version that counts, so 3 × 3 × 3 × 3 = 81 non-deleted resources
namespaces := []string{"ns1", "ns2", "ns3"}
groups := []string{"apps", "extensions", "networking"}
resources := []string{"deployments", "services", "ingresses"}
names := []string{"item1", "item2", "item3"}
// Create all the test data
totalEntries := 0
for _, ns := range namespaces {
for _, group := range groups {
for _, resource := range resources {
for _, name := range names {
// Create 3 versions for each resource name
for version := 1; version <= 3; version++ {
rv := node.Generate().Int64()
var action DataAction
switch version {
case 1:
action = DataActionCreated
case 2, 3:
action = DataActionUpdated
}
dataKey := DataKey{
Namespace: ns,
Group: group,
Resource: resource,
Name: name,
ResourceVersion: rv,
Action: action,
Folder: "test-folder",
}
content := fmt.Sprintf("%s/%s/%s/%s-v%d", ns, group, resource, name, version)
err := ds.Save(ctx, dataKey, bytes.NewReader([]byte(content)))
require.NoError(t, err)
totalEntries++
}
}
}
}
}
// Verify we created the expected number of entries
require.Equal(t, 243, totalEntries) // 3×3×3×3×3 = 243 total entries
t.Run("get stats for all namespaces", func(t *testing.T) {
stats, err := ds.GetResourceStats(ctx, "", 0)
require.NoError(t, err)
// Should have 27 resource types (3 namespaces × 3 groups × 3 resources)
require.Len(t, stats, 27)
// Each resource type should have exactly 3 items (3 names per resource type)
for _, stat := range stats {
require.Equal(t, int64(3), stat.Count, "Resource %s/%s/%s should have 3 items", stat.Namespace, stat.Group, stat.Resource)
require.Greater(t, stat.ResourceVersion, int64(0), "ResourceVersion should be positive")
}
// Verify all expected combinations are present
expectedCombinations := make(map[string]bool)
for _, ns := range namespaces {
for _, group := range groups {
for _, resource := range resources {
key := fmt.Sprintf("%s/%s/%s", ns, group, resource)
expectedCombinations[key] = false
}
}
}
for _, stat := range stats {
key := fmt.Sprintf("%s/%s/%s", stat.Namespace, stat.Group, stat.Resource)
expectedCombinations[key] = true
}
for key, found := range expectedCombinations {
require.True(t, found, "Expected combination not found: %s", key)
}
})
t.Run("get stats for specific namespace ns1", func(t *testing.T) {
stats, err := ds.GetResourceStats(ctx, "ns1", 0)
require.NoError(t, err)
// Should have 9 resource types (3 groups × 3 resources for ns1)
require.Len(t, stats, 9)
// All stats should be for ns1
for _, stat := range stats {
require.Equal(t, "ns1", stat.Namespace)
require.Equal(t, int64(3), stat.Count) // 3 names per resource type
}
// Verify we have all expected groups and resources for ns1
foundCombinations := make(map[string]bool)
for _, stat := range stats {
key := fmt.Sprintf("%s/%s", stat.Group, stat.Resource)
foundCombinations[key] = true
}
expectedCount := len(groups) * len(resources) // 3×3=9
require.Equal(t, expectedCount, len(foundCombinations))
})
t.Run("get stats for specific namespace ns2", func(t *testing.T) {
stats, err := ds.GetResourceStats(ctx, "ns2", 0)
require.NoError(t, err)
// Should have 9 resource types (3 groups × 3 resources for ns2)
require.Len(t, stats, 9)
// All stats should be for ns2
for _, stat := range stats {
require.Equal(t, "ns2", stat.Namespace)
require.Equal(t, int64(3), stat.Count)
}
})
t.Run("get stats with minCount filter", func(t *testing.T) {
// With minCount=0, all resources should be included (each has 3 items > 0)
stats, err := ds.GetResourceStats(ctx, "", 0)
require.NoError(t, err)
require.Len(t, stats, 27) // All 27 resource types should be included
// With minCount=2, all resources should still be included (each has 3 items > 2)
stats, err = ds.GetResourceStats(ctx, "", 2)
require.NoError(t, err)
require.Len(t, stats, 27) // All 27 resource types should still be included
// With minCount=3, no resources should be included (each has exactly 3 items, not > 3)
stats, err = ds.GetResourceStats(ctx, "", 3)
require.NoError(t, err)
require.Len(t, stats, 0)
// With minCount=4, no resources should be included (each has only 3 items < 4)
stats, err = ds.GetResourceStats(ctx, "", 4)
require.NoError(t, err)
require.Len(t, stats, 0)
})
t.Run("get stats for non-existent namespace", func(t *testing.T) {
stats, err := ds.GetResourceStats(ctx, "non-existent", 0)
require.NoError(t, err)
require.Len(t, stats, 0)
})
t.Run("add deleted resources and verify counts", func(t *testing.T) {
// Delete one resource from ns1/apps/deployments/item1
rv := node.Generate().Int64()
deletedKey := DataKey{
Namespace: "ns1",
Group: "apps",
Resource: "deployments",
Name: "item1",
ResourceVersion: rv,
Action: DataActionDeleted,
Folder: "test-folder",
}
err := ds.Save(ctx, deletedKey, bytes.NewReader([]byte("deleted")))
require.NoError(t, err)
// Get stats for ns1 - apps/deployments should now have 2 items instead of 3
stats, err := ds.GetResourceStats(ctx, "ns1", 0)
require.NoError(t, err)
// Find the apps/deployments stat
var appsDeploymentsCount int64 = -1
for _, stat := range stats {
if stat.Group == "apps" && stat.Resource == "deployments" {
appsDeploymentsCount = stat.Count
break
}
}
require.Equal(t, int64(2), appsDeploymentsCount, "apps/deployments should have 2 items after deletion")
// Other resource types in ns1 should still have 3 items
otherResourceCount := 0
for _, stat := range stats {
if stat.Group != "apps" || stat.Resource != "deployments" {
require.Equal(t, int64(3), stat.Count, "Other resources should still have 3 items")
otherResourceCount++
}
}
require.Equal(t, 8, otherResourceCount) // 9 total - 1 apps/deployments = 8
})
t.Run("verify resource versions are meaningful", func(t *testing.T) {
stats, err := ds.GetResourceStats(ctx, "ns1", 0)
require.NoError(t, err)
// All ResourceVersions should be positive and reasonable
for _, stat := range stats {
require.Greater(t, stat.ResourceVersion, int64(0))
// ResourceVersion should be a snowflake ID, so it should be quite large
require.Greater(t, stat.ResourceVersion, int64(1000000))
}
})
}
func TestDataStore_getGroupResources(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreGetGroupResources)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreGetGroupResources)
}
func testDataStoreGetGroupResources(t *testing.T, ctx context.Context, ds *dataStore) {
// Create test data with multiple group/resource combinations
testData := []struct {
group string
resource string
namespace string
name string
}{
{"apps", "deployments", "default", "web-app"},
{"apps", "deployments", "test", "api-server"},
{"apps", "services", "default", "web-svc"},
{"networking", "ingresses", "default", "web-ingress"},
{"batch", "jobs", "default", "cleanup-job"},
{"batch", "jobs", "test", "migration-job"},
}
// Save all test data
for i, data := range testData {
rv := node.Generate().Int64()
dataKey := DataKey{
Namespace: data.namespace,
Group: data.group,
Resource: data.resource,
Name: data.name,
ResourceVersion: rv,
Action: DataActionCreated,
Folder: "test-folder",
}
err := ds.Save(ctx, dataKey, bytes.NewReader([]byte(fmt.Sprintf("content-%d", i))))
require.NoError(t, err)
}
// Test GetGroupResources
results, err := ds.getGroupResources(ctx)
require.NoError(t, err)
// Should find exactly 4 unique group/resource combinations
expectedCombinations := []string{
"apps/deployments",
"apps/services",
"networking/ingresses",
"batch/jobs",
}
require.Len(t, results, len(expectedCombinations))
// Verify all expected combinations are present and no duplicates
foundCombinations := make(map[string]bool)
for _, result := range results {
key := fmt.Sprintf("%s/%s", result.Group, result.Resource)
require.False(t, foundCombinations[key], "Duplicate group/resource found: %s", key)
foundCombinations[key] = true
}
for _, expected := range expectedCombinations {
require.True(t, foundCombinations[expected], "Expected combination not found: %s", expected)
}
}
func TestDataStore_BatchDelete(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreBatchDelete)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreBatchDelete)
}
func testDataStoreBatchDelete(t *testing.T, ctx context.Context, ds *dataStore) {
keys := make([]DataKey, 95)
for i := 0; i < 95; i++ {
rv := node.Generate().Int64()
keys[i] = DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: fmt.Sprintf("test-name-%d", i),
ResourceVersion: rv,
Action: DataActionCreated,
Folder: "test-folder",
}
content := fmt.Sprintf("test-value-%d", i)
err := ds.Save(ctx, keys[i], bytes.NewReader([]byte(content)))
require.NoError(t, err)
}
err := ds.batchDelete(ctx, keys)
require.NoError(t, err)
// Verify all events were deleted
for i := 0; i < 95; i++ {
_, err := ds.Get(ctx, DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: fmt.Sprintf("test-name-%d", i),
})
require.Error(t, err, "Resource should have been deleted")
}
}
func TestDataStore_BatchGet(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreBatchGet)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreBatchGet)
}
func testDataStoreBatchGet(t *testing.T, ctx context.Context, ds *dataStore) {
t.Run("batch get multiple existing keys", func(t *testing.T) {
// Create test data
keys := make([]DataKey, 5)
expectedContent := make(map[string]string)
for i := 0; i < 5; i++ {
rv := node.Generate().Int64()
keys[i] = DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: fmt.Sprintf("test-name-%d", i),
ResourceVersion: rv,
Action: DataActionCreated,
Folder: "test-folder",
}
content := fmt.Sprintf("test-value-%d", i)
expectedContent[keys[i].Name] = content
err := ds.Save(ctx, keys[i], bytes.NewReader([]byte(content)))
require.NoError(t, err)
}
// Batch get all keys
results := make([]DataObj, 0, 5)
for obj, err := range ds.BatchGet(ctx, keys) {
require.NoError(t, err)
results = append(results, obj)
}
// Verify all keys were returned
require.Len(t, results, 5)
// Verify content matches
for _, result := range results {
resultBytes, err := io.ReadAll(result.Value)
require.NoError(t, err)
expectedValue, ok := expectedContent[result.Key.Name]
require.True(t, ok, "Unexpected key in results: %s", result.Key.Name)
require.Equal(t, expectedValue, string(resultBytes))
}
})
t.Run("batch get with some non-existent keys", func(t *testing.T) {
// Create 3 existing keys
existingKeys := make([]DataKey, 3)
for i := 0; i < 3; i++ {
rv := node.Generate().Int64()
existingKeys[i] = DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: fmt.Sprintf("existing-%d", i),
ResourceVersion: rv,
Action: DataActionCreated,
Folder: "test-folder",
}
err := ds.Save(ctx, existingKeys[i], bytes.NewReader([]byte(fmt.Sprintf("value-%d", i))))
require.NoError(t, err)
}
// Create 2 non-existent keys (not saved to datastore)
nonExistentKeys := make([]DataKey, 2)
for i := 0; i < 2; i++ {
rv := node.Generate().Int64()
nonExistentKeys[i] = DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: fmt.Sprintf("non-existent-%d", i),
ResourceVersion: rv,
Action: DataActionCreated,
Folder: "test-folder",
}
}
// Combine existing and non-existent keys
allKeys := append(existingKeys, nonExistentKeys...)
// Batch get all keys
results := make([]DataObj, 0, 3)
for obj, err := range ds.BatchGet(ctx, allKeys) {
require.NoError(t, err)
results = append(results, obj)
}
// Should only return the 3 existing keys
require.Len(t, results, 3)
// Verify only existing keys are returned
for _, result := range results {
require.Contains(t, result.Key.Name, "existing-")
// Verify content
resultBytes, err := io.ReadAll(result.Value)
require.NoError(t, err)
require.NotEmpty(t, resultBytes)
}
})
t.Run("batch get with large number of keys to test batching", func(t *testing.T) {
numKeys := 150
keys := make([]DataKey, numKeys)
expectedContent := make(map[string]string)
for i := 0; i < numKeys; i++ {
rv := node.Generate().Int64()
keys[i] = DataKey{
Namespace: "batch-test",
Group: "test-group",
Resource: "test-resource",
Name: fmt.Sprintf("item-%d", i),
ResourceVersion: rv,
Action: DataActionCreated,
Folder: "test-folder",
}
content := fmt.Sprintf("content-%d", i)
expectedContent[keys[i].Name] = content
err := ds.Save(ctx, keys[i], bytes.NewReader([]byte(content)))
require.NoError(t, err)
}
// Batch get all keys
results := make([]DataObj, 0, numKeys)
for obj, err := range ds.BatchGet(ctx, keys) {
require.NoError(t, err)
results = append(results, obj)
}
// Verify all keys were returned
require.Len(t, results, numKeys)
// Verify content matches for all keys
for _, result := range results {
resultBytes, err := io.ReadAll(result.Value)
require.NoError(t, err)
expectedValue, ok := expectedContent[result.Key.Name]
require.True(t, ok, "Unexpected key in results: %s", result.Key.Name)
require.Equal(t, expectedValue, string(resultBytes))
}
})
}
func TestDataStore_GetLatestAndPredecessor(t *testing.T) {
runDataStoreTestWith(t, "badger", setupTestDataStore, testDataStoreGetLatestAndPredecessor)
// enable this when sqlkv is ready
// runDataStoreTestWith(t, "sqlkv", setupTestDataStoreSqlKv, testDataStoreGetLatestAndPredecessor)
}
func testDataStoreGetLatestAndPredecessor(t *testing.T, ctx context.Context, ds *dataStore) {
resourceKey := ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
}
t.Run("returns latest and predecessor when multiple versions exist", func(t *testing.T) {
// Create test data with multiple versions
rv1 := node.Generate().Int64()
rv2 := node.Generate().Int64()
rv3 := node.Generate().Int64()
versions := []int64{rv1, rv2, rv3}
// Save all versions
for _, version := range versions {
dataKey := DataKey{
Namespace: resourceKey.Namespace,
Group: resourceKey.Group,
Resource: resourceKey.Resource,
Name: resourceKey.Name,
ResourceVersion: version,
Action: DataActionCreated,
}
err := ds.Save(ctx, dataKey, bytes.NewReader([]byte(fmt.Sprintf("version-%d", version))))
require.NoError(t, err)
}
// Get latest and predecessor
latest, predecessor, err := ds.GetLatestAndPredecessor(ctx, resourceKey)
require.NoError(t, err)
// Verify latest is rv3 (highest)
require.Equal(t, rv3, latest.ResourceVersion)
require.Equal(t, resourceKey.Namespace, latest.Namespace)
require.Equal(t, resourceKey.Group, latest.Group)
require.Equal(t, resourceKey.Resource, latest.Resource)
require.Equal(t, resourceKey.Name, latest.Name)
// Verify predecessor is rv2 (second highest)
require.Equal(t, rv2, predecessor.ResourceVersion)
require.Equal(t, resourceKey.Namespace, predecessor.Namespace)
require.Equal(t, resourceKey.Group, predecessor.Group)
require.Equal(t, resourceKey.Resource, predecessor.Resource)
require.Equal(t, resourceKey.Name, predecessor.Name)
})
t.Run("returns latest with empty predecessor when only one version exists", func(t *testing.T) {
singleResourceKey := ListRequestKey{
Namespace: "single-namespace",
Group: "single-group",
Resource: "single-resource",
Name: "single-name",
}
rv := node.Generate().Int64()
dataKey := DataKey{
Namespace: singleResourceKey.Namespace,
Group: singleResourceKey.Group,
Resource: singleResourceKey.Resource,
Name: singleResourceKey.Name,
ResourceVersion: rv,
Action: DataActionCreated,
}
err := ds.Save(ctx, dataKey, bytes.NewReader([]byte("single-version")))
require.NoError(t, err)
// Get latest and predecessor
latest, predecessor, err := ds.GetLatestAndPredecessor(ctx, singleResourceKey)
require.NoError(t, err)
// Verify latest is correct
require.Equal(t, rv, latest.ResourceVersion)
require.Equal(t, singleResourceKey.Namespace, latest.Namespace)
require.Equal(t, singleResourceKey.Group, latest.Group)
require.Equal(t, singleResourceKey.Resource, latest.Resource)
require.Equal(t, singleResourceKey.Name, latest.Name)
// Verify predecessor is empty (ResourceVersion == 0)
require.Equal(t, int64(0), predecessor.ResourceVersion)
require.Empty(t, predecessor.Namespace)
require.Empty(t, predecessor.Group)
require.Empty(t, predecessor.Resource)
require.Empty(t, predecessor.Name)
})
t.Run("returns error for non-existent resource", func(t *testing.T) {
nonExistentKey := ListRequestKey{
Namespace: "non-existent-namespace",
Group: "non-existent-group",
Resource: "non-existent-resource",
Name: "non-existent-name",
}
_, _, err := ds.GetLatestAndPredecessor(ctx, nonExistentKey)
require.Error(t, err)
require.Equal(t, ErrNotFound, err)
})
}