package cache import ( "sync" "testing" "time" "git.nakama.town/fmartingr/gotoolkit/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewMemoryCache(t *testing.T) { cache := NewMemoryCache() require.NotNil(t, cache, "NewMemoryCache returned nil") require.NotNil(t, cache.data, "Cache data map was not initialized") } func TestMemoryCache_Set(t *testing.T) { cache := NewMemoryCache() // Test basic set err := cache.Set("key1", "value1") require.NoError(t, err, "Set returned unexpected error") // Verify the item was stored correctly cache.dataMu.RLock() item, exists := cache.data["key1"] cache.dataMu.RUnlock() require.True(t, exists, "Set did not store the item in the cache") assert.Equal(t, "key1", item.Key, "Wrong key stored") assert.Equal(t, "value1", item.Value, "Wrong value stored") assert.Nil(t, item.TTL, "TTL should be nil") // Test set with TTL withTTL := func(item *model.CacheItem) { expiry := time.Now().Add(10 * time.Minute) item.TTL = &expiry } err = cache.Set("key2", "value2", withTTL) require.NoError(t, err, "Set with TTL returned unexpected error") // Verify the item with TTL was stored correctly cache.dataMu.RLock() itemWithTTL, exists := cache.data["key2"] cache.dataMu.RUnlock() require.True(t, exists, "Set did not store the item with TTL in the cache") assert.NotNil(t, itemWithTTL.TTL, "TTL option was not applied") } func TestMemoryCache_SetTableDriven(t *testing.T) { tests := []struct { name string key string value interface{} ttl *time.Duration }{ { name: "string value without TTL", key: "string-key", value: "string-value", ttl: nil, }, { name: "int value without TTL", key: "int-key", value: 42, ttl: nil, }, { name: "struct value without TTL", key: "struct-key", value: struct{ Name string }{"test"}, ttl: nil, }, { name: "string value with TTL", key: "string-key-ttl", value: "string-value-ttl", ttl: ptrDuration(5 * time.Minute), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cache := NewMemoryCache() var opts []model.CacheOption if tt.ttl != nil { opts = append(opts, func(item *model.CacheItem) { expiry := time.Now().Add(*tt.ttl) item.TTL = &expiry }) } err := cache.Set(tt.key, tt.value, opts...) assert.NoError(t, err) cache.dataMu.RLock() item, exists := cache.data[tt.key] cache.dataMu.RUnlock() assert.True(t, exists) assert.Equal(t, tt.key, item.Key) assert.Equal(t, tt.value, item.Value) if tt.ttl != nil { assert.NotNil(t, item.TTL) } else { assert.Nil(t, item.TTL) } }) } } func TestMemoryCache_Get(t *testing.T) { cache := NewMemoryCache() // Test getting non-existent key _, err := cache.Get("nonexistent") assert.ErrorIs(t, err, model.ErrCacheKeyDontExist, "Wrong error returned for non-existent key") // Test getting existing key require.NoError(t, cache.Set("key1", "value1")) value, err := cache.Get("key1") require.NoError(t, err, "Get returned unexpected error") assert.Equal(t, "value1", value, "Wrong value returned") // Test getting expired key expiredTTL := func(item *model.CacheItem) { expiry := time.Now().Add(-1 * time.Minute) // Past time item.TTL = &expiry } require.NoError(t, cache.Set("expired", "value", expiredTTL)) _, err = cache.Get("expired") assert.ErrorIs(t, err, model.ErrCacheKeyDontExist, "Wrong error returned for expired item") // Verify the expired key was deleted cache.dataMu.RLock() _, exists := cache.data["expired"] cache.dataMu.RUnlock() assert.False(t, exists, "Expired item was not deleted during Get") } func TestMemoryCache_GetTableDriven(t *testing.T) { tests := []struct { name string setupKey string setupValue any setupTTL *time.Duration getKey string expectValue any expectError error }{ { name: "get existing key", setupKey: "test-key", setupValue: "test-value", getKey: "test-key", expectValue: "test-value", expectError: nil, }, { name: "get non-existent key", setupKey: "test-key", setupValue: "test-value", getKey: "different-key", expectValue: nil, expectError: model.ErrCacheKeyDontExist, }, { name: "get expired key", setupKey: "expired-key", setupValue: "expired-value", setupTTL: ptrDuration(-1 * time.Minute), // Negative to create an expired key getKey: "expired-key", expectValue: nil, expectError: model.ErrCacheKeyDontExist, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cache := NewMemoryCache() // Setup var opts []model.CacheOption if tt.setupTTL != nil { opts = append(opts, func(item *model.CacheItem) { expiry := time.Now().Add(*tt.setupTTL) item.TTL = &expiry }) } if tt.setupKey != "" { err := cache.Set(tt.setupKey, tt.setupValue, opts...) require.NoError(t, err) } // Test value, err := cache.Get(tt.getKey) if tt.expectError != nil { assert.ErrorIs(t, err, tt.expectError) } else { assert.NoError(t, err) } assert.Equal(t, tt.expectValue, value) // If it was an expired key, ensure it's deleted if tt.setupTTL != nil && *tt.setupTTL < 0 { cache.dataMu.RLock() _, exists := cache.data[tt.setupKey] cache.dataMu.RUnlock() assert.False(t, exists, "Expired item was not deleted during Get") } }) } } func TestMemoryCache_Delete(t *testing.T) { cache := NewMemoryCache() // Setup test data require.NoError(t, cache.Set("key1", "value1")) // Test deleting existing key err := cache.Delete("key1") require.NoError(t, err, "Delete returned unexpected error") // Verify the key was deleted cache.dataMu.RLock() _, exists := cache.data["key1"] cache.dataMu.RUnlock() assert.False(t, exists, "Item was not deleted from cache") // Test deleting non-existent key (should not error) err = cache.Delete("nonexistent") assert.NoError(t, err, "Delete on non-existent key returned error") } func TestMemoryCache_Concurrency(t *testing.T) { cache := NewMemoryCache() const goroutines = 10 const operations = 100 wg := sync.WaitGroup{} wg.Add(goroutines * 2) // Writers for i := range goroutines { go func(id int) { for j := range operations { key := "key" + string(rune('A'+id)) + string(rune('0'+j%10)) err := cache.Set(key, j) assert.NoError(t, err) } wg.Done() }(i) } // Readers for i := range goroutines { go func(id int) { for j := range operations { key := "key" + string(rune('A'+id)) + string(rune('0'+j%10)) _, _ = cache.Get(key) // We ignore errors here as keys might not exist yet } wg.Done() }(i) } // Wait for all goroutines to complete wg.Wait() // If we reached here without deadlock or panic, the test passes } // Helper function to create duration pointers func ptrDuration(d time.Duration) *time.Duration { return &d }