grafana/pkg/storage/unified/resource/datastore_test.go

2952 lines
77 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/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)
}
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 TestDataStore_Save_And_Get(t *testing.T) {
ds := setupTestDataStore(t)
ctx := context.Background()
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) {
ds := setupTestDataStore(t)
ctx := context.Background()
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) {
ds := setupTestDataStore(t)
ctx := context.Background()
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) {
ds := setupTestDataStore(t)
ctx := context.Background()
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) {
ds := setupTestDataStore(t)
ctx := context.Background()
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) {
ds := setupTestDataStore(t)
ctx := context.Background()
// 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) {
ds := setupTestDataStore(t)
ctx := context.Background()
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) {
ds := setupTestDataStore(t)
ctx := context.Background()
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) {
ds := setupTestDataStore(t)
ctx := context.Background()
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) {
ds := setupTestDataStore(t)
ctx := context.Background()
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) {
ds := setupTestDataStore(t)
ctx := context.Background()
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) {
ds := setupTestDataStore(t)
ctx := context.Background()
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) {
ds := setupTestDataStore(t)
ctx := context.Background()
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) {
ds := setupTestDataStore(t)
ctx := context.Background()
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) {
ds := setupTestDataStore(t)
ctx := context.Background()
// 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, listKey, 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, listKey, 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, listKey, 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, listKey, 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, listKey, 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, specificListKey, 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, listKey, 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) {
ds := setupTestDataStore(t)
ctx := context.Background()
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, tt.key, 0) {
require.Error(t, err)
return
}
})
}
}
func TestDataStore_ListResourceKeysAtRevision_EmptyResults(t *testing.T) {
ds := setupTestDataStore(t)
ctx := context.Background()
listKey := ListRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "empty",
}
resultKeys := make([]DataKey, 0, 1)
for dataKey, err := range ds.ListResourceKeysAtRevision(ctx, listKey, 0) {
require.NoError(t, err)
resultKeys = append(resultKeys, dataKey)
}
require.Len(t, resultKeys, 0)
}
func TestDataStore_ListResourceKeysAtRevision_ResourcesNewerThanRevision(t *testing.T) {
ds := setupTestDataStore(t)
ctx := context.Background()
// 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, listKey, 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) {
ds := setupTestDataStore(t)
ctx := context.Background()
// 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) {
ds := setupTestDataStore(t)
ctx := context.Background()
// 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)
}
}