blob: 928866087d167939e3f2829736022e72f2a3153b [file] [log] [blame]
package service
import (
"context"
"testing"
"time"
"github.com/korap/korap-mcp/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDefaultCacheConfig(t *testing.T) {
cacheConfig := config.DefaultCacheConfig()
assert.True(t, cacheConfig.Enabled)
assert.Equal(t, "5m", cacheConfig.DefaultTTL)
assert.Equal(t, "2m", cacheConfig.SearchTTL)
assert.Equal(t, "15m", cacheConfig.MetadataTTL)
assert.Equal(t, 1000, cacheConfig.MaxSize)
}
func TestNewCache(t *testing.T) {
tests := []struct {
name string
config *config.CacheConfig
expectError bool
expectNilCache bool
}{
{
name: "enabled cache",
config: config.DefaultCacheConfig(),
expectError: false,
expectNilCache: false,
},
{
name: "disabled cache",
config: &config.CacheConfig{
Enabled: false,
},
expectError: false,
expectNilCache: true,
},
{
name: "custom configuration",
config: &config.CacheConfig{
Enabled: true,
DefaultTTL: "1m",
SearchTTL: "30s",
MetadataTTL: "5m",
MaxSize: 500,
},
expectError: false,
expectNilCache: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cache, err := NewCache(tt.config)
if tt.expectError {
assert.Error(t, err)
return
}
require.NoError(t, err)
assert.NotNil(t, cache)
if tt.expectNilCache {
assert.Nil(t, cache.cache)
} else {
assert.NotNil(t, cache.cache)
}
assert.Equal(t, tt.config, cache.config)
})
}
}
func TestCacheGetSet(t *testing.T) {
cache, err := NewCache(config.DefaultCacheConfig())
require.NoError(t, err)
ctx := context.Background()
key := "test-key"
data := []byte("test data")
ttl := 1 * time.Minute
// Test cache miss
result, found := cache.Get(ctx, key)
assert.False(t, found)
assert.Nil(t, result)
// Test cache set and hit
cache.Set(ctx, key, data, ttl)
result, found = cache.Get(ctx, key)
assert.True(t, found)
assert.Equal(t, data, result)
}
func TestCacheExpiry(t *testing.T) {
cache, err := NewCache(config.DefaultCacheConfig())
require.NoError(t, err)
ctx := context.Background()
key := "test-key"
data := []byte("test data")
ttl := 50 * time.Millisecond
// Set cache entry with short TTL
cache.Set(ctx, key, data, ttl)
// Should hit immediately
result, found := cache.Get(ctx, key)
assert.True(t, found)
assert.Equal(t, data, result)
// Wait for expiry
time.Sleep(100 * time.Millisecond)
// Should miss after expiry
result, found = cache.Get(ctx, key)
assert.False(t, found)
assert.Nil(t, result)
}
func TestCacheDelete(t *testing.T) {
cache, err := NewCache(config.DefaultCacheConfig())
require.NoError(t, err)
ctx := context.Background()
key := "test-key"
data := []byte("test data")
ttl := 1 * time.Minute
// Set and verify
cache.Set(ctx, key, data, ttl)
result, found := cache.Get(ctx, key)
assert.True(t, found)
assert.Equal(t, data, result)
// Delete and verify
cache.Delete(ctx, key)
result, found = cache.Get(ctx, key)
assert.False(t, found)
assert.Nil(t, result)
}
func TestCacheClear(t *testing.T) {
cache, err := NewCache(config.DefaultCacheConfig())
require.NoError(t, err)
ctx := context.Background()
ttl := 1 * time.Minute
// Set multiple entries
cache.Set(ctx, "key1", []byte("data1"), ttl)
cache.Set(ctx, "key2", []byte("data2"), ttl)
cache.Set(ctx, "key3", []byte("data3"), ttl)
// Verify all entries exist
_, found1 := cache.Get(ctx, "key1")
_, found2 := cache.Get(ctx, "key2")
_, found3 := cache.Get(ctx, "key3")
assert.True(t, found1)
assert.True(t, found2)
assert.True(t, found3)
// Clear cache
cache.Clear()
// Verify all entries are gone
_, found1 = cache.Get(ctx, "key1")
_, found2 = cache.Get(ctx, "key2")
_, found3 = cache.Get(ctx, "key3")
assert.False(t, found1)
assert.False(t, found2)
assert.False(t, found3)
}
func TestCacheStats(t *testing.T) {
cache, err := NewCache(config.DefaultCacheConfig())
require.NoError(t, err)
ctx := context.Background()
// Test stats with empty cache
stats := cache.Stats()
assert.True(t, stats["enabled"].(bool))
assert.Equal(t, 0, stats["size"].(int))
// Add some data and check stats
cache.Set(ctx, "key1", []byte("data1"), 1*time.Minute)
cache.Set(ctx, "key2", []byte("data2"), 1*time.Minute)
stats = cache.Stats()
assert.True(t, stats["enabled"].(bool))
assert.Equal(t, 2, stats["size"].(int))
assert.Equal(t, 1000, stats["max_size"].(int))
assert.Contains(t, stats, "hits")
assert.Contains(t, stats, "misses")
assert.Contains(t, stats, "hit_ratio")
}
func TestCacheStatsDisabled(t *testing.T) {
cacheConfig := &config.CacheConfig{Enabled: false}
cache, err := NewCache(cacheConfig)
require.NoError(t, err)
stats := cache.Stats()
assert.False(t, stats["enabled"].(bool))
assert.Len(t, stats, 1) // Only "enabled" key should be present
}
func TestCacheDisabled(t *testing.T) {
cacheConfig := &config.CacheConfig{Enabled: false}
cache, err := NewCache(cacheConfig)
require.NoError(t, err)
ctx := context.Background()
key := "test-key"
data := []byte("test data")
ttl := 1 * time.Minute
// All operations should be no-ops
cache.Set(ctx, key, data, ttl)
result, found := cache.Get(ctx, key)
assert.False(t, found)
assert.Nil(t, result)
cache.Delete(ctx, key)
cache.Clear()
// Should not panic
assert.NotPanics(t, func() {
cache.Close()
})
}
func TestGenerateCacheKey(t *testing.T) {
cache, err := NewCache(config.DefaultCacheConfig())
require.NoError(t, err)
tests := []struct {
name string
method string
endpoint string
params map[string]any
wantSame bool
}{
{
name: "same parameters",
method: "GET",
endpoint: "/search",
params: map[string]any{"q": "test", "count": 10},
wantSame: true,
},
{
name: "different method",
method: "POST",
endpoint: "/search",
params: map[string]any{"q": "test", "count": 10},
wantSame: false,
},
{
name: "different endpoint",
method: "GET",
endpoint: "/corpus",
params: map[string]any{"q": "test", "count": 10},
wantSame: false,
},
{
name: "different parameters",
method: "GET",
endpoint: "/search",
params: map[string]any{"q": "different", "count": 10},
wantSame: false,
},
{
name: "nil parameters",
method: "GET",
endpoint: "/search",
params: nil,
wantSame: false,
},
}
baseKey := cache.generateCacheKey("GET", "/search", map[string]any{"q": "test", "count": 10})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key := cache.generateCacheKey(tt.method, tt.endpoint, tt.params)
assert.NotEmpty(t, key)
assert.Len(t, key, 32) // MD5 hash length
if tt.wantSame {
assert.Equal(t, baseKey, key)
} else {
assert.NotEqual(t, baseKey, key)
}
})
}
}
func TestCacheKeyDeterministic(t *testing.T) {
cache, err := NewCache(config.DefaultCacheConfig())
require.NoError(t, err)
// Test that map parameter order doesn't affect cache key generation
// This verifies that json.Marshal provides deterministic ordering
params1 := map[string]any{
"query": "test search",
"count": 50,
"offset": 0,
"corpus": "news-corpus",
"language": "poliqarp",
}
params2 := map[string]any{
"language": "poliqarp",
"corpus": "news-corpus",
"offset": 0,
"count": 50,
"query": "test search",
}
// Same parameters in different map creation order should produce same cache key
key1 := cache.generateCacheKey("GET", "/search", params1)
key2 := cache.generateCacheKey("GET", "/search", params2)
assert.Equal(t, key1, key2, "Cache keys should be identical regardless of map parameter order")
assert.Len(t, key1, 32, "Cache key should be MD5 hash length")
assert.Len(t, key2, 32, "Cache key should be MD5 hash length")
// Generate keys multiple times to ensure consistency
for i := 0; i < 10; i++ {
keyN := cache.generateCacheKey("GET", "/search", params1)
assert.Equal(t, key1, keyN, "Cache key should be consistent across multiple generations")
}
}
func TestGetTTLForEndpoint(t *testing.T) {
cacheConfig := config.DefaultCacheConfig()
cache, err := NewCache(cacheConfig)
require.NoError(t, err)
tests := []struct {
name string
endpoint string
expected time.Duration
}{
{
name: "search endpoint",
endpoint: "/api/v1/search",
expected: cacheConfig.GetSearchTTL(),
},
{
name: "query endpoint",
endpoint: "/query",
expected: cacheConfig.GetSearchTTL(),
},
{
name: "corpus endpoint",
endpoint: "/corpus",
expected: cacheConfig.GetMetadataTTL(),
},
{
name: "metadata endpoint",
endpoint: "/metadata",
expected: cacheConfig.GetMetadataTTL(),
},
{
name: "statistics endpoint",
endpoint: "/statistics",
expected: cacheConfig.GetMetadataTTL(),
},
{
name: "info endpoint",
endpoint: "/info",
expected: cacheConfig.GetMetadataTTL(),
},
{
name: "other endpoint",
endpoint: "/other",
expected: cacheConfig.GetDefaultTTL(),
},
{
name: "case insensitive",
endpoint: "/API/V1/SEARCH",
expected: cacheConfig.GetSearchTTL(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ttl := cache.GetTTLForEndpoint(tt.endpoint)
assert.Equal(t, tt.expected, ttl)
})
}
}
func TestCacheEntry(t *testing.T) {
t.Run("not expired", func(t *testing.T) {
entry := &CacheEntry{
Data: []byte("test"),
Timestamp: time.Now(),
TTL: 1 * time.Minute,
}
assert.False(t, entry.IsExpired())
})
t.Run("expired", func(t *testing.T) {
entry := &CacheEntry{
Data: []byte("test"),
Timestamp: time.Now().Add(-2 * time.Minute),
TTL: 1 * time.Minute,
}
assert.True(t, entry.IsExpired())
})
}
func TestCacheClose(t *testing.T) {
cache, err := NewCache(config.DefaultCacheConfig())
require.NoError(t, err)
// Add some data
ctx := context.Background()
cache.Set(ctx, "key1", []byte("data1"), 1*time.Minute)
// Close should not error
err = cache.Close()
assert.NoError(t, err)
// Disabled cache close should also not error
disabledCache, err := NewCache(&config.CacheConfig{Enabled: false})
require.NoError(t, err)
err = disabledCache.Close()
assert.NoError(t, err)
}