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) }) }