gotoolkit/cache/file_test.go
2025-03-23 23:37:26 +01:00

412 lines
12 KiB
Go

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,
}
}