mirror of https://github.com/grafana/grafana.git
2952 lines
77 KiB
Go
2952 lines
77 KiB
Go
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)
|
||
}
|
||
}
|