12dd3dffe0
* implement sqlkv skeleton and include sqlkv in badgerkv tests
856 lines
25 KiB
Go
856 lines
25 KiB
Go
package resource
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/bwmarrin/snowflake"
|
|
"github.com/grafana/grafana/pkg/infra/db"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
|
|
"github.com/grafana/grafana/pkg/tests/testsuite"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func setupTestEventStore(t *testing.T) *eventStore {
|
|
db := setupTestBadgerDB(t)
|
|
t.Cleanup(func() {
|
|
err := db.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
kv := NewBadgerKV(db)
|
|
return newEventStore(kv)
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
testsuite.Run(m)
|
|
}
|
|
|
|
// nolint:unused
|
|
func setupTestEventStoreSqlKv(t *testing.T) *eventStore {
|
|
dbstore := db.InitTestDB(t)
|
|
eDB, err := dbimpl.ProvideResourceDB(dbstore, setting.NewCfg(), nil)
|
|
require.NoError(t, err)
|
|
kv, err := NewSQLKV(eDB)
|
|
require.NoError(t, err)
|
|
return newEventStore(kv)
|
|
}
|
|
|
|
func TestNewEventStore(t *testing.T) {
|
|
store := setupTestEventStore(t)
|
|
assert.NotNil(t, store.kv)
|
|
}
|
|
|
|
func TestEventKey_String(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
eventKey EventKey
|
|
expected string
|
|
}{
|
|
{
|
|
name: "basic event key",
|
|
eventKey: EventKey{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-resource",
|
|
ResourceVersion: 1000,
|
|
Action: "created",
|
|
Folder: "test-folder",
|
|
},
|
|
expected: "1000~default~apps~resource~test-resource~created~test-folder",
|
|
},
|
|
{
|
|
name: "empty namespace",
|
|
eventKey: EventKey{
|
|
Namespace: "",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-resource",
|
|
ResourceVersion: 2000,
|
|
Action: "updated",
|
|
Folder: "test-folder",
|
|
},
|
|
expected: "2000~~apps~resource~test-resource~updated~test-folder",
|
|
},
|
|
{
|
|
name: "special characters in name",
|
|
eventKey: EventKey{
|
|
Namespace: "test-ns",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-resource-with-dashes",
|
|
ResourceVersion: 3000,
|
|
Action: "deleted",
|
|
Folder: "test-folder",
|
|
},
|
|
expected: "3000~test-ns~apps~resource~test-resource-with-dashes~deleted~test-folder",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.eventKey.String()
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEventKey_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
expected EventKey
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "valid key",
|
|
key: "1000~default~apps~resource~test-resource~created~test-folder",
|
|
expected: EventKey{
|
|
ResourceVersion: 1000,
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-resource",
|
|
Action: "created",
|
|
Folder: "test-folder",
|
|
},
|
|
},
|
|
{
|
|
name: "empty namespace",
|
|
key: "2000~~apps~resource~test-resource~updated~",
|
|
expected: EventKey{
|
|
ResourceVersion: 2000,
|
|
Namespace: "",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-resource",
|
|
Action: "updated",
|
|
},
|
|
},
|
|
{
|
|
name: "special characters in name",
|
|
key: "3000~test-ns~apps~resource~test-resource-with-dashes~updated~",
|
|
expected: EventKey{
|
|
ResourceVersion: 3000,
|
|
Namespace: "test-ns",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-resource-with-dashes",
|
|
Action: "updated",
|
|
},
|
|
},
|
|
{
|
|
name: "invalid key - too few parts",
|
|
key: "1000~default~apps~resource~",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "invalid key - too many parts",
|
|
key: "1000~default~apps~resource~test~extra~parts~",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "invalid resource version",
|
|
key: "invalid~default~apps~resource~test~cerated~",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "empty key",
|
|
key: "",
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, err := ParseEventKey(tt.key)
|
|
|
|
if tt.expectError {
|
|
assert.Error(t, err)
|
|
assert.Equal(t, EventKey{}, result)
|
|
} else {
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEventStore_ParseEventKey(t *testing.T) {
|
|
originalKey := EventKey{
|
|
ResourceVersion: 1234567890,
|
|
Namespace: "test-namespace",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-resource",
|
|
Action: "created",
|
|
}
|
|
|
|
// Convert to string and back
|
|
keyString := originalKey.String()
|
|
parsedKey, err := ParseEventKey(keyString)
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, originalKey, parsedKey)
|
|
}
|
|
|
|
func runEventStoreTestWith(t *testing.T, storeName string, newStoreFn func(*testing.T) *eventStore, testFn func(*testing.T, context.Context, *eventStore)) {
|
|
t.Run(storeName, func(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newStoreFn(t)
|
|
testFn(t, ctx, store)
|
|
})
|
|
}
|
|
|
|
func TestEventStore_Save_Get(t *testing.T) {
|
|
runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreSaveGet)
|
|
// enable this when sqlkv is ready
|
|
// runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreSaveGet)
|
|
}
|
|
|
|
func testEventStoreSaveGet(t *testing.T, ctx context.Context, store *eventStore) {
|
|
event := Event{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-resource",
|
|
ResourceVersion: 1000,
|
|
Action: DataActionCreated,
|
|
Folder: "test-folder",
|
|
PreviousRV: 999,
|
|
}
|
|
|
|
// Save the event
|
|
err := store.Save(ctx, event)
|
|
require.NoError(t, err)
|
|
|
|
// Get the event back
|
|
eventKey := EventKey{
|
|
Namespace: event.Namespace,
|
|
Group: event.Group,
|
|
Resource: event.Resource,
|
|
Name: event.Name,
|
|
ResourceVersion: event.ResourceVersion,
|
|
Action: event.Action,
|
|
Folder: event.Folder,
|
|
}
|
|
|
|
retrievedEvent, err := store.Get(ctx, eventKey)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, event, retrievedEvent)
|
|
}
|
|
|
|
func TestEventStore_Get_NotFound(t *testing.T) {
|
|
runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreGetNotFound)
|
|
// enable this when sqlkv is ready
|
|
// runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreGetNotFound)
|
|
}
|
|
|
|
func testEventStoreGetNotFound(t *testing.T, ctx context.Context, store *eventStore) {
|
|
nonExistentKey := EventKey{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "non-existent",
|
|
ResourceVersion: 9999,
|
|
Action: "created",
|
|
}
|
|
|
|
_, err := store.Get(ctx, nonExistentKey)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestEventStore_LastEventKey(t *testing.T) {
|
|
runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreLastEventKey)
|
|
// enable this when sqlkv is ready
|
|
// runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreLastEventKey)
|
|
}
|
|
|
|
func testEventStoreLastEventKey(t *testing.T, ctx context.Context, store *eventStore) {
|
|
// Test when no events exist
|
|
_, err := store.LastEventKey(ctx)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, ErrNotFound, err)
|
|
|
|
// Add some events with different resource versions
|
|
events := []Event{
|
|
{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-1",
|
|
ResourceVersion: 1000,
|
|
Action: DataActionCreated,
|
|
},
|
|
{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-2",
|
|
ResourceVersion: 3000, // highest
|
|
Action: DataActionCreated,
|
|
},
|
|
{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-3",
|
|
ResourceVersion: 2000,
|
|
Action: DataActionCreated,
|
|
},
|
|
}
|
|
|
|
// Save all events
|
|
for _, event := range events {
|
|
err := store.Save(ctx, event)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Get the last event key (should be the one with highest RV)
|
|
lastKey, err := store.LastEventKey(ctx)
|
|
require.NoError(t, err)
|
|
|
|
expectedKey := EventKey{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-2",
|
|
ResourceVersion: 3000,
|
|
Action: "created",
|
|
}
|
|
|
|
assert.Equal(t, expectedKey, lastKey)
|
|
}
|
|
|
|
func TestEventStore_ListKeysSince(t *testing.T) {
|
|
runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreListKeysSince)
|
|
// enable this when sqlkv is ready
|
|
// runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreListKeysSince)
|
|
}
|
|
|
|
func testEventStoreListKeysSince(t *testing.T, ctx context.Context, store *eventStore) {
|
|
// Add events with different resource versions
|
|
events := []Event{
|
|
{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-1",
|
|
ResourceVersion: 1000,
|
|
Action: DataActionCreated,
|
|
},
|
|
{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-2",
|
|
ResourceVersion: 2000,
|
|
Action: DataActionUpdated,
|
|
},
|
|
{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-3",
|
|
ResourceVersion: 3000,
|
|
Action: DataActionDeleted,
|
|
},
|
|
}
|
|
|
|
// Save all events
|
|
for _, event := range events {
|
|
err := store.Save(ctx, event)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// List events since RV 1500 (should get events with RV 2000 and 3000)
|
|
retrievedEvents := make([]string, 0, 2)
|
|
for eventKey, err := range store.ListKeysSince(ctx, 1500) {
|
|
require.NoError(t, err)
|
|
retrievedEvents = append(retrievedEvents, eventKey)
|
|
}
|
|
|
|
// Should return events in ascending order of resource version
|
|
require.Len(t, retrievedEvents, 2)
|
|
evt1, err := ParseEventKey(retrievedEvents[0])
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(2000), evt1.ResourceVersion)
|
|
assert.Equal(t, "test-2", evt1.Name)
|
|
evt2, err := ParseEventKey(retrievedEvents[1])
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(3000), evt2.ResourceVersion)
|
|
assert.Equal(t, "test-3", evt2.Name)
|
|
}
|
|
|
|
func TestEventStore_ListSince(t *testing.T) {
|
|
runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreListSince)
|
|
// enable this when sqlkv is ready
|
|
// runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreListSince)
|
|
}
|
|
|
|
func testEventStoreListSince(t *testing.T, ctx context.Context, store *eventStore) {
|
|
// Add events with different resource versions
|
|
events := []Event{
|
|
{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-1",
|
|
ResourceVersion: 1000,
|
|
Action: DataActionCreated,
|
|
},
|
|
{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-2",
|
|
ResourceVersion: 2000,
|
|
Action: DataActionUpdated,
|
|
},
|
|
{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-3",
|
|
ResourceVersion: 3000,
|
|
Action: DataActionDeleted,
|
|
},
|
|
}
|
|
|
|
// Save all events
|
|
for _, event := range events {
|
|
err := store.Save(ctx, event)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// List events since RV 1500 (should get events with RV 2000 and 3000)
|
|
retrievedEvents := make([]Event, 0, 2)
|
|
for event, err := range store.ListSince(ctx, 1500) {
|
|
require.NoError(t, err)
|
|
retrievedEvents = append(retrievedEvents, event)
|
|
}
|
|
|
|
// Should return events in descending order of resource version
|
|
require.Len(t, retrievedEvents, 2)
|
|
assert.Equal(t, int64(2000), retrievedEvents[0].ResourceVersion)
|
|
assert.Equal(t, "test-2", retrievedEvents[0].Name)
|
|
assert.Equal(t, DataActionUpdated, retrievedEvents[0].Action)
|
|
assert.Equal(t, int64(3000), retrievedEvents[1].ResourceVersion)
|
|
assert.Equal(t, "test-3", retrievedEvents[1].Name)
|
|
assert.Equal(t, DataActionDeleted, retrievedEvents[1].Action)
|
|
}
|
|
|
|
func TestEventStore_ListSince_Empty(t *testing.T) {
|
|
runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreListSinceEmpty)
|
|
// enable this when sqlkv is ready
|
|
// runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreListSinceEmpty)
|
|
}
|
|
|
|
func testEventStoreListSinceEmpty(t *testing.T, ctx context.Context, store *eventStore) {
|
|
// List events when store is empty
|
|
retrievedEvents := make([]Event, 0)
|
|
for event, err := range store.ListSince(ctx, 0) {
|
|
require.NoError(t, err)
|
|
retrievedEvents = append(retrievedEvents, event)
|
|
}
|
|
|
|
assert.Empty(t, retrievedEvents)
|
|
}
|
|
|
|
func TestEvent_JSONSerialization(t *testing.T) {
|
|
event := Event{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-resource",
|
|
ResourceVersion: 1000,
|
|
Action: DataActionCreated,
|
|
Folder: "test-folder",
|
|
PreviousRV: 999,
|
|
}
|
|
|
|
// Serialize to JSON
|
|
data, err := json.Marshal(event)
|
|
require.NoError(t, err)
|
|
|
|
// Deserialize from JSON
|
|
var deserializedEvent Event
|
|
err = json.Unmarshal(data, &deserializedEvent)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, event, deserializedEvent)
|
|
}
|
|
|
|
func TestEventKey_Struct(t *testing.T) {
|
|
key := EventKey{
|
|
Namespace: "test-namespace",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-resource",
|
|
ResourceVersion: 1234567890,
|
|
Action: "created",
|
|
}
|
|
|
|
assert.Equal(t, "test-namespace", key.Namespace)
|
|
assert.Equal(t, "apps", key.Group)
|
|
assert.Equal(t, "resource", key.Resource)
|
|
assert.Equal(t, "test-resource", key.Name)
|
|
assert.Equal(t, int64(1234567890), key.ResourceVersion)
|
|
}
|
|
|
|
func TestEventStore_Save_InvalidJSON(t *testing.T) {
|
|
runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreSaveInvalidJSON)
|
|
// enable this when sqlkv is ready
|
|
// runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreSaveInvalidJSON)
|
|
}
|
|
|
|
func testEventStoreSaveInvalidJSON(t *testing.T, ctx context.Context, store *eventStore) {
|
|
// This should work fine as the Event struct should be serializable
|
|
event := Event{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test",
|
|
ResourceVersion: 1000,
|
|
Action: DataActionCreated,
|
|
}
|
|
|
|
err := store.Save(ctx, event)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestEventStore_CleanupOldEvents(t *testing.T) {
|
|
runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreCleanupOldEvents)
|
|
// enable this when sqlkv is ready
|
|
// runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreCleanupOldEvents)
|
|
}
|
|
|
|
func testEventStoreCleanupOldEvents(t *testing.T, ctx context.Context, store *eventStore) {
|
|
now := time.Now()
|
|
oldRV := snowflakeFromTime(now.Add(-48 * time.Hour)) // 48 hours ago
|
|
recentRV := snowflakeFromTime(now.Add(-1 * time.Hour)) // 1 hour ago
|
|
|
|
oldEvent := Event{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "old-resource",
|
|
ResourceVersion: oldRV,
|
|
Action: DataActionCreated,
|
|
Folder: "test-folder",
|
|
PreviousRV: 999,
|
|
}
|
|
|
|
recentEvent := Event{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "recent-resource",
|
|
ResourceVersion: recentRV,
|
|
Action: DataActionCreated,
|
|
Folder: "test-folder",
|
|
PreviousRV: 999,
|
|
}
|
|
|
|
// Save both events
|
|
err := store.Save(ctx, oldEvent)
|
|
require.NoError(t, err)
|
|
err = store.Save(ctx, recentEvent)
|
|
require.NoError(t, err)
|
|
|
|
// Verify both events exist
|
|
_, err = store.Get(ctx, EventKey{
|
|
Namespace: oldEvent.Namespace,
|
|
Group: oldEvent.Group,
|
|
Resource: oldEvent.Resource,
|
|
Name: oldEvent.Name,
|
|
ResourceVersion: oldEvent.ResourceVersion,
|
|
Action: oldEvent.Action,
|
|
Folder: oldEvent.Folder,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
_, err = store.Get(ctx, EventKey{
|
|
Namespace: recentEvent.Namespace,
|
|
Group: recentEvent.Group,
|
|
Resource: recentEvent.Resource,
|
|
Name: recentEvent.Name,
|
|
ResourceVersion: recentEvent.ResourceVersion,
|
|
Action: recentEvent.Action,
|
|
Folder: recentEvent.Folder,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Clean up events older than 24 hours
|
|
deletedCount, err := store.CleanupOldEvents(ctx, time.Now().Add(-24*time.Hour))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, deletedCount, "Should have deleted 1 old event")
|
|
|
|
// Verify old event was deleted
|
|
_, err = store.Get(ctx, EventKey{
|
|
Namespace: oldEvent.Namespace,
|
|
Group: oldEvent.Group,
|
|
Resource: oldEvent.Resource,
|
|
Name: oldEvent.Name,
|
|
ResourceVersion: oldEvent.ResourceVersion,
|
|
Action: oldEvent.Action,
|
|
})
|
|
assert.Error(t, err, "Old event should have been deleted")
|
|
|
|
// Verify recent event still exists
|
|
_, err = store.Get(ctx, EventKey{
|
|
Namespace: recentEvent.Namespace,
|
|
Group: recentEvent.Group,
|
|
Resource: recentEvent.Resource,
|
|
Name: recentEvent.Name,
|
|
ResourceVersion: recentEvent.ResourceVersion,
|
|
Action: recentEvent.Action,
|
|
Folder: recentEvent.Folder,
|
|
})
|
|
require.NoError(t, err, "Recent event should still exist")
|
|
}
|
|
|
|
func TestEventStore_CleanupOldEvents_NoOldEvents(t *testing.T) {
|
|
runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreCleanupOldEventsNoOldEvents)
|
|
// enable this when sqlkv is ready
|
|
// runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreCleanupOldEventsNoOldEvents)
|
|
}
|
|
|
|
func testEventStoreCleanupOldEventsNoOldEvents(t *testing.T, ctx context.Context, store *eventStore) {
|
|
// Create an event 1 hour old
|
|
rv := snowflakeFromTime(time.Now().Add(-1 * time.Hour))
|
|
event := Event{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "recent-resource",
|
|
ResourceVersion: rv,
|
|
Action: DataActionCreated,
|
|
Folder: "test-folder",
|
|
PreviousRV: 999,
|
|
}
|
|
|
|
err := store.Save(ctx, event)
|
|
require.NoError(t, err)
|
|
|
|
// Clean up events older than 24 hours
|
|
deletedCount, err := store.CleanupOldEvents(ctx, time.Now().Add(-24*time.Hour))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, deletedCount, "Should not have deleted any events")
|
|
|
|
// Verify event still exists
|
|
_, err = store.Get(ctx, EventKey{
|
|
Namespace: event.Namespace,
|
|
Group: event.Group,
|
|
Resource: event.Resource,
|
|
Name: event.Name,
|
|
ResourceVersion: event.ResourceVersion,
|
|
Action: event.Action,
|
|
Folder: event.Folder,
|
|
})
|
|
require.NoError(t, err, "Recent event should still exist")
|
|
}
|
|
|
|
func TestEventStore_CleanupOldEvents_EmptyStore(t *testing.T) {
|
|
runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreCleanupOldEventsEmptyStore)
|
|
// enable this when sqlkv is ready
|
|
// runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreCleanupOldEventsEmptyStore)
|
|
}
|
|
|
|
func testEventStoreCleanupOldEventsEmptyStore(t *testing.T, ctx context.Context, store *eventStore) {
|
|
// Clean up events from empty store
|
|
deletedCount, err := store.CleanupOldEvents(ctx, time.Now().Add(-24*time.Hour))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 0, deletedCount, "Should not have deleted any events from empty store")
|
|
}
|
|
|
|
func TestEventStore_BatchDelete(t *testing.T) {
|
|
runEventStoreTestWith(t, "badger", setupTestEventStore, testEventStoreBatchDelete)
|
|
// enable this when sqlkv is ready
|
|
// runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testEventStoreBatchDelete)
|
|
}
|
|
|
|
func testEventStoreBatchDelete(t *testing.T, ctx context.Context, store *eventStore) {
|
|
// Create multiple events (more than batch size to test batching)
|
|
eventKeys := make([]string, 75)
|
|
for i := 0; i < 75; i++ {
|
|
event := Event{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "deployments",
|
|
Name: "test-deployment",
|
|
ResourceVersion: int64(1000 + i),
|
|
Action: DataActionCreated,
|
|
Folder: "test-folder",
|
|
PreviousRV: int64(999 + i),
|
|
}
|
|
err := store.Save(ctx, event)
|
|
require.NoError(t, err)
|
|
|
|
eventKeys[i] = EventKey{
|
|
Namespace: event.Namespace,
|
|
Group: event.Group,
|
|
Resource: event.Resource,
|
|
Name: event.Name,
|
|
ResourceVersion: event.ResourceVersion,
|
|
Action: event.Action,
|
|
Folder: event.Folder,
|
|
}.String()
|
|
}
|
|
|
|
// Batch delete all events
|
|
err := store.batchDelete(ctx, eventKeys)
|
|
require.NoError(t, err)
|
|
|
|
// Verify all events were deleted
|
|
for i := 0; i < 75; i++ {
|
|
_, err := store.Get(ctx, EventKey{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "deployments",
|
|
Name: "test-deployment",
|
|
ResourceVersion: int64(1000 + i),
|
|
Action: DataActionCreated,
|
|
Folder: "test-folder",
|
|
})
|
|
require.Error(t, err, "Event should have been deleted")
|
|
}
|
|
}
|
|
|
|
func TestSubtractDurationFromSnowflake(t *testing.T) {
|
|
baseTime := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
|
|
tests := []struct {
|
|
name string
|
|
addTime time.Duration
|
|
}{
|
|
{
|
|
name: "subtract 1 hour",
|
|
addTime: -1 * time.Hour,
|
|
},
|
|
{
|
|
name: "subtract 2 hours",
|
|
addTime: -2 * time.Hour,
|
|
},
|
|
{
|
|
name: "subtract 24 hours",
|
|
addTime: -24 * time.Hour,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Generate a snowflake from the base time
|
|
baseSnowflake := snowflakeFromTime(baseTime)
|
|
|
|
// Subtract the duration
|
|
resultSnowflake := subtractDurationFromSnowflake(baseSnowflake, tt.addTime)
|
|
|
|
// Convert back to timestamp and verify
|
|
// Extract timestamp from the result snowflake
|
|
timestamp := snowflake.ID(resultSnowflake).Time()
|
|
resultTime := time.Unix(0, timestamp*int64(time.Millisecond))
|
|
|
|
// Compare with expected time (allowing for small differences due to snowflake precision)
|
|
expectedMillis := baseTime.Add(-tt.addTime).UnixMilli()
|
|
resultMillis := resultTime.UnixMilli()
|
|
assert.InDelta(t, expectedMillis, resultMillis, 1,
|
|
"Expected time %v, got %v (diff: %d ms)",
|
|
baseTime, resultTime, expectedMillis-resultMillis)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSnowflakeFromTime(t *testing.T) {
|
|
testTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
snowflakeID := snowflakeFromTime(testTime)
|
|
|
|
// Extract timestamp and verify it matches
|
|
timestamp := snowflake.ID(snowflakeID).Time()
|
|
reconstructedTime := time.Unix(0, timestamp*int64(time.Millisecond))
|
|
|
|
// The times should match at millisecond precision
|
|
expectedMillis := testTime.UnixMilli()
|
|
resultMillis := reconstructedTime.UnixMilli()
|
|
|
|
assert.Equal(t, expectedMillis, resultMillis, "Snowflake timestamp should match original time at millisecond precision")
|
|
}
|
|
|
|
func TestListKeysSince_WithSnowflakeTime(t *testing.T) {
|
|
runEventStoreTestWith(t, "badger", setupTestEventStore, testListKeysSinceWithSnowflakeTime)
|
|
// enable this when sqlkv is ready
|
|
// runEventStoreTestWith(t, "sqlkv", setupTestEventStoreSqlKv, testListKeysSinceWithSnowflakeTime)
|
|
}
|
|
|
|
func testListKeysSinceWithSnowflakeTime(t *testing.T, ctx context.Context, store *eventStore) {
|
|
// Create events with snowflake-based resource versions at different times
|
|
now := time.Now()
|
|
events := []Event{
|
|
{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-1",
|
|
ResourceVersion: snowflakeFromTime(now.Add(-2 * time.Hour)),
|
|
Action: DataActionCreated,
|
|
},
|
|
{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-2",
|
|
ResourceVersion: snowflakeFromTime(now.Add(-1 * time.Hour)),
|
|
Action: DataActionUpdated,
|
|
},
|
|
{
|
|
Namespace: "default",
|
|
Group: "apps",
|
|
Resource: "resource",
|
|
Name: "test-3",
|
|
ResourceVersion: snowflakeFromTime(now.Add(-30 * time.Minute)),
|
|
Action: DataActionDeleted,
|
|
},
|
|
}
|
|
|
|
// Save all events
|
|
for _, event := range events {
|
|
err := store.Save(ctx, event)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// List events since 90 minutes ago using subtractDurationFromSnowflake
|
|
sinceRV := subtractDurationFromSnowflake(snowflakeFromTime(now), 90*time.Minute)
|
|
retrievedEvents := make([]string, 0)
|
|
for eventKey, err := range store.ListKeysSince(ctx, sinceRV) {
|
|
require.NoError(t, err)
|
|
retrievedEvents = append(retrievedEvents, eventKey)
|
|
}
|
|
|
|
// Should return events from the last hour and 30 minutes
|
|
require.Len(t, retrievedEvents, 2)
|
|
evt1, err := ParseEventKey(retrievedEvents[0])
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test-2", evt1.Name)
|
|
evt2, err := ParseEventKey(retrievedEvents[1])
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test-3", evt2.Name)
|
|
|
|
// List events since 30 minutes ago using subtractDurationFromSnowflake
|
|
sinceRV = subtractDurationFromSnowflake(snowflakeFromTime(now), 30*time.Minute)
|
|
retrievedEvents = make([]string, 0)
|
|
for eventKey, err := range store.ListKeysSince(ctx, sinceRV) {
|
|
require.NoError(t, err)
|
|
retrievedEvents = append(retrievedEvents, eventKey)
|
|
}
|
|
|
|
// Should return events from the last hour and 30 minutes
|
|
require.Len(t, retrievedEvents, 1)
|
|
evt, err := ParseEventKey(retrievedEvents[0])
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test-3", evt.Name)
|
|
}
|