package cache import ( "os" "path/filepath" "testing" "time" "git.nakama.town/fmartingr/gotoolkit/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewFileCache(t *testing.T) { // Create a test folder name testFolderName := "test-cache" // Test initialization with the temp directory cache, err := NewFileCache(testFolderName) require.NoError(t, err, "NewFileCache should not return an error") require.NotNil(t, cache, "NewFileCache should return a non-nil cache") // Verify the directory was created (not checking the exact path as it depends on the os.UserCacheDir output) dirInfo, err := os.Stat(cache.path) require.NoError(t, err, "Cache directory should exist") assert.True(t, dirInfo.IsDir(), "Cache path should be a directory") } func TestFileCache_Set(t *testing.T) { // Create a temporary directory for testing tempDir := t.TempDir() cache := setupTestFileCache(t, tempDir) // Test simple set operation testKey := "test-key" testValue := []byte("test-value") err := cache.Set(testKey, testValue) require.NoError(t, err, "Set should not return an error") // Verify the file exists filePath := filepath.Join(cache.path, testKey) assert.FileExists(t, filePath, "Cache file should exist") assert.FileExists(t, filePath+".metadata", "Cache metadata file should exist") // Read the file contents content, err := os.ReadFile(filePath) require.NoError(t, err, "Should be able to read cache file") assert.Equal(t, testValue, content, "File content should match set value") // Test set with TTL ttlKey := "ttl-key" ttlValue := []byte("ttl-value") ttlDuration := 1 * time.Hour withTTL := func(item *model.CacheItem) { expiry := time.Now().Add(ttlDuration) item.TTL = &expiry } err = cache.Set(ttlKey, ttlValue, withTTL) require.NoError(t, err, "Set with TTL should not return an error") // Verify file exists ttlFilePath := filepath.Join(cache.path, ttlKey) assert.FileExists(t, ttlFilePath, "Cache file with TTL should exist") assert.FileExists(t, ttlFilePath+".metadata", "Cache metadata file with TTL should exist") // Verify the metadata has TTL set metadata, err := cache.getMetadata(ttlKey) require.NoError(t, err, "Should be able to get metadata") assert.NotNil(t, metadata.TTL, "TTL should be set in metadata") } func TestFileCache_SetTableDriven(t *testing.T) { tempDir := t.TempDir() tests := []struct { name string key string value []byte withTTL bool ttlDelta time.Duration }{ { name: "basic set without TTL", key: "key1", value: []byte("value1"), }, { name: "set with future TTL", key: "key2", value: []byte("value2"), withTTL: true, ttlDelta: 1 * time.Hour, // Future time }, { name: "set with past TTL", key: "key3", value: []byte("value3"), withTTL: true, ttlDelta: -1 * time.Hour, // Past time }, { name: "set with empty value", key: "key4", value: []byte{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cache := setupTestFileCache(t, tempDir) var opts []model.CacheOption if tt.withTTL { opts = append(opts, func(item *model.CacheItem) { expiry := time.Now().Add(tt.ttlDelta) item.TTL = &expiry }) } err := cache.Set(tt.key, tt.value, opts...) require.NoError(t, err, "Set should not return an error") // Verify file and metadata exist filePath := filepath.Join(cache.path, tt.key) assert.FileExists(t, filePath, "Cache file should exist") assert.FileExists(t, filePath+".metadata", "Cache metadata file should exist") // Verify file content content, err := os.ReadFile(filePath) require.NoError(t, err, "Should be able to read cache file") assert.Equal(t, tt.value, content, "File content should match set value") // Verify metadata metadata, err := cache.getMetadata(tt.key) require.NoError(t, err, "Should be able to get metadata") assert.Equal(t, tt.key, metadata.Key, "Metadata key should match") if tt.withTTL { assert.NotNil(t, metadata.TTL, "TTL should be set in metadata") expectedTime := time.Now().Add(tt.ttlDelta) assert.WithinDuration(t, expectedTime, *metadata.TTL, 2*time.Second, "TTL should be close to expected value") } else { assert.Nil(t, metadata.TTL, "TTL should not be set for items without TTL") } }) } } func TestFileCache_Get(t *testing.T) { // Create a temporary directory for testing tempDir := t.TempDir() cache := setupTestFileCache(t, tempDir) // Set up test data validKey := "valid-key" validValue := []byte("valid-value") err := cache.Set(validKey, validValue) require.NoError(t, err, "Set should not return an error for setup") // Test getting valid key retrievedValue, err := cache.Get(validKey) require.NoError(t, err, "Get should not return an error for valid key") assert.Equal(t, validValue, retrievedValue, "Retrieved value should match original") // Test getting non-existent key _, err = cache.Get("nonexistent-key") assert.ErrorIs(t, err, model.ErrCacheKeyDontExist, "Get should return ErrCacheKeyDontExist for non-existent key") // Test getting expired key expiredKey := "expired-key" expiredValue := []byte("expired-value") expiredTTL := func(item *model.CacheItem) { expiry := time.Now().Add(-1 * time.Minute) // Past time item.TTL = &expiry } err = cache.Set(expiredKey, expiredValue, expiredTTL) require.NoError(t, err, "Set should not return an error for expired key setup") _, err = cache.Get(expiredKey) assert.ErrorIs(t, err, model.ErrCacheKeyDontExist, "Get should return ErrCacheKeyDontExist for expired key") // Verify the expired files are deleted expiredFilePath := filepath.Join(cache.path, expiredKey) expiredMetadataPath := expiredFilePath + ".metadata" assert.NoFileExists(t, expiredFilePath, "Expired cache file should be deleted") assert.NoFileExists(t, expiredMetadataPath, "Expired cache metadata file should be deleted") } func TestFileCache_GetTableDriven(t *testing.T) { tempDir := t.TempDir() tests := []struct { name string setupKeys map[string][]byte setupTTLs map[string]time.Duration getKey string expectValue []byte expectError error checkFileDeleted bool }{ { name: "get existing key", setupKeys: map[string][]byte{ "test-key": []byte("test-value"), }, getKey: "test-key", expectValue: []byte("test-value"), expectError: nil, }, { name: "get non-existent key", setupKeys: map[string][]byte{ "test-key": []byte("test-value"), }, getKey: "nonexistent-key", expectValue: nil, expectError: model.ErrCacheKeyDontExist, }, { name: "get expired key", setupKeys: map[string][]byte{ "expired-key": []byte("expired-value"), }, setupTTLs: map[string]time.Duration{ "expired-key": -1 * time.Minute, // Past time }, getKey: "expired-key", expectValue: nil, expectError: model.ErrCacheKeyDontExist, checkFileDeleted: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cache := setupTestFileCache(t, tempDir) // Setup test data for key, value := range tt.setupKeys { var opts []model.CacheOption if ttl, ok := tt.setupTTLs[key]; ok { opts = append(opts, func(item *model.CacheItem) { expiry := time.Now().Add(ttl) item.TTL = &expiry }) } err := cache.Set(key, value, opts...) require.NoError(t, err, "Setup: Set should not return an error") } // Test get value, err := cache.Get(tt.getKey) if tt.expectError != nil { assert.ErrorIs(t, err, tt.expectError) assert.Nil(t, value) } else { assert.NoError(t, err) assert.Equal(t, tt.expectValue, value) } // Check if expired files are deleted if tt.checkFileDeleted { filePath := filepath.Join(cache.path, tt.getKey) metadataPath := filePath + ".metadata" assert.NoFileExists(t, filePath, "Expired cache file should be deleted") assert.NoFileExists(t, metadataPath, "Expired cache metadata file should be deleted") } }) } } func TestFileCache_Delete(t *testing.T) { // Create a temporary directory for testing tempDir := t.TempDir() cache := setupTestFileCache(t, tempDir) // Set up test data testKey := "delete-key" testValue := []byte("delete-value") err := cache.Set(testKey, testValue) require.NoError(t, err, "Set should not return an error for setup") // Verify files exist before delete filePath := filepath.Join(cache.path, testKey) metadataPath := filePath + ".metadata" assert.FileExists(t, filePath, "Cache file should exist before delete") assert.FileExists(t, metadataPath, "Cache metadata file should exist before delete") // Test delete err = cache.Delete(testKey) require.NoError(t, err, "Delete should not return an error") // Verify files are deleted assert.NoFileExists(t, filePath, "Cache file should be deleted") assert.NoFileExists(t, metadataPath, "Cache metadata file should be deleted") // Test deleting non-existent key err = cache.Delete("nonexistent-key") assert.Error(t, err, "Delete should return an error for non-existent key") } func TestFileCache_DeleteTableDriven(t *testing.T) { tempDir := t.TempDir() tests := []struct { name string setupKey string setupValue []byte deleteKey string expectError bool checkNotExist bool }{ { name: "delete existing key", setupKey: "existing-key", setupValue: []byte("existing-value"), deleteKey: "existing-key", expectError: false, checkNotExist: true, }, { name: "delete non-existent key", deleteKey: "nonexistent-key", expectError: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cache := setupTestFileCache(t, tempDir) // Setup test data if needed if tt.setupKey != "" { err := cache.Set(tt.setupKey, tt.setupValue) require.NoError(t, err, "Setup: Set should not return an error") // Verify files exist before delete filePath := filepath.Join(cache.path, tt.setupKey) metadataPath := filePath + ".metadata" assert.FileExists(t, filePath, "Cache file should exist before delete") assert.FileExists(t, metadataPath, "Cache metadata file should exist before delete") } // Test delete err := cache.Delete(tt.deleteKey) if tt.expectError { assert.Error(t, err, "Delete should return an error") } else { assert.NoError(t, err, "Delete should not return an error") } // Check files are deleted if needed if tt.checkNotExist { filePath := filepath.Join(cache.path, tt.deleteKey) metadataPath := filePath + ".metadata" assert.NoFileExists(t, filePath, "Cache file should be deleted") assert.NoFileExists(t, metadataPath, "Cache metadata file should be deleted") } }) } } func TestFileCache_GetPathForItem(t *testing.T) { tempDir := t.TempDir() cache := setupTestFileCache(t, tempDir) key := "test-key" expectedPath := filepath.Join(cache.path, key) actualPath := cache.getPathForItem(key) assert.Equal(t, expectedPath, actualPath, "Path for item should match expected format") } func TestFileCache_GetSetMetadata(t *testing.T) { tempDir := t.TempDir() cache := setupTestFileCache(t, tempDir) // Create test metadata key := "metadata-key" now := time.Now() metadata := model.CacheItem{ Key: key, Value: nil, TTL: &now, } // Test setMetadata err := cache.setMetadata(key, metadata) require.NoError(t, err, "setMetadata should not return an error") // Verify metadata file exists metadataPath := filepath.Join(cache.path, key) + ".metadata" assert.FileExists(t, metadataPath, "Metadata file should exist") // Test getMetadata retrievedMetadata, err := cache.getMetadata(key) require.NoError(t, err, "getMetadata should not return an error") assert.Equal(t, key, retrievedMetadata.Key, "Retrieved metadata key should match original") assert.WithinDuration(t, now, *retrievedMetadata.TTL, time.Second, "Retrieved metadata TTL should be close to original") } // Helper function to create a FileCache for testing func setupTestFileCache(t *testing.T, tempDir string) *FileCache { return &FileCache{ path: tempDir, } }