blob: 037f0b8fe26e79a395b7223cdd721ff68dda6c8b [file] [log] [blame]
Akron16343052025-06-17 16:16:13 +02001package service
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10)
11
12func TestDefaultCacheConfig(t *testing.T) {
13 config := DefaultCacheConfig()
14
15 assert.True(t, config.Enabled)
16 assert.Equal(t, 5*time.Minute, config.DefaultTTL)
17 assert.Equal(t, 2*time.Minute, config.SearchTTL)
18 assert.Equal(t, 15*time.Minute, config.MetadataTTL)
19 assert.Equal(t, 1000, config.MaxSize)
20}
21
22func TestNewCache(t *testing.T) {
23 tests := []struct {
24 name string
25 config CacheConfig
26 expectError bool
27 expectNilCache bool
28 }{
29 {
30 name: "enabled cache",
31 config: DefaultCacheConfig(),
32 expectError: false,
33 expectNilCache: false,
34 },
35 {
36 name: "disabled cache",
37 config: CacheConfig{
38 Enabled: false,
39 },
40 expectError: false,
41 expectNilCache: true,
42 },
43 {
44 name: "custom configuration",
45 config: CacheConfig{
46 Enabled: true,
47 DefaultTTL: 1 * time.Minute,
48 SearchTTL: 30 * time.Second,
49 MetadataTTL: 5 * time.Minute,
50 MaxSize: 500,
51 },
52 expectError: false,
53 expectNilCache: false,
54 },
55 }
56
57 for _, tt := range tests {
58 t.Run(tt.name, func(t *testing.T) {
59 cache, err := NewCache(tt.config)
60
61 if tt.expectError {
62 assert.Error(t, err)
63 return
64 }
65
66 require.NoError(t, err)
67 assert.NotNil(t, cache)
68
69 if tt.expectNilCache {
70 assert.Nil(t, cache.cache)
71 } else {
72 assert.NotNil(t, cache.cache)
73 }
74
75 assert.Equal(t, tt.config, cache.config)
76 })
77 }
78}
79
80func TestCacheGetSet(t *testing.T) {
81 cache, err := NewCache(DefaultCacheConfig())
82 require.NoError(t, err)
83
84 ctx := context.Background()
85 key := "test-key"
86 data := []byte("test data")
87 ttl := 1 * time.Minute
88
89 // Test cache miss
90 result, found := cache.Get(ctx, key)
91 assert.False(t, found)
92 assert.Nil(t, result)
93
94 // Test cache set and hit
95 cache.Set(ctx, key, data, ttl)
96 result, found = cache.Get(ctx, key)
97 assert.True(t, found)
98 assert.Equal(t, data, result)
99}
100
101func TestCacheExpiry(t *testing.T) {
102 cache, err := NewCache(DefaultCacheConfig())
103 require.NoError(t, err)
104
105 ctx := context.Background()
106 key := "test-key"
107 data := []byte("test data")
108 ttl := 50 * time.Millisecond
109
110 // Set cache entry with short TTL
111 cache.Set(ctx, key, data, ttl)
112
113 // Should hit immediately
114 result, found := cache.Get(ctx, key)
115 assert.True(t, found)
116 assert.Equal(t, data, result)
117
118 // Wait for expiry
119 time.Sleep(100 * time.Millisecond)
120
121 // Should miss after expiry
122 result, found = cache.Get(ctx, key)
123 assert.False(t, found)
124 assert.Nil(t, result)
125}
126
127func TestCacheDelete(t *testing.T) {
128 cache, err := NewCache(DefaultCacheConfig())
129 require.NoError(t, err)
130
131 ctx := context.Background()
132 key := "test-key"
133 data := []byte("test data")
134 ttl := 1 * time.Minute
135
136 // Set and verify
137 cache.Set(ctx, key, data, ttl)
138 result, found := cache.Get(ctx, key)
139 assert.True(t, found)
140 assert.Equal(t, data, result)
141
142 // Delete and verify
143 cache.Delete(ctx, key)
144 result, found = cache.Get(ctx, key)
145 assert.False(t, found)
146 assert.Nil(t, result)
147}
148
149func TestCacheClear(t *testing.T) {
150 cache, err := NewCache(DefaultCacheConfig())
151 require.NoError(t, err)
152
153 ctx := context.Background()
154 ttl := 1 * time.Minute
155
156 // Set multiple entries
157 cache.Set(ctx, "key1", []byte("data1"), ttl)
158 cache.Set(ctx, "key2", []byte("data2"), ttl)
159 cache.Set(ctx, "key3", []byte("data3"), ttl)
160
161 // Verify all entries exist
162 _, found1 := cache.Get(ctx, "key1")
163 _, found2 := cache.Get(ctx, "key2")
164 _, found3 := cache.Get(ctx, "key3")
165 assert.True(t, found1)
166 assert.True(t, found2)
167 assert.True(t, found3)
168
169 // Clear cache
170 cache.Clear()
171
172 // Verify all entries are gone
173 _, found1 = cache.Get(ctx, "key1")
174 _, found2 = cache.Get(ctx, "key2")
175 _, found3 = cache.Get(ctx, "key3")
176 assert.False(t, found1)
177 assert.False(t, found2)
178 assert.False(t, found3)
179}
180
181func TestCacheStats(t *testing.T) {
182 cache, err := NewCache(DefaultCacheConfig())
183 require.NoError(t, err)
184
185 ctx := context.Background()
186
187 // Test stats with empty cache
188 stats := cache.Stats()
189 assert.True(t, stats["enabled"].(bool))
190 assert.Equal(t, 0, stats["size"].(int))
191
192 // Add some data and check stats
193 cache.Set(ctx, "key1", []byte("data1"), 1*time.Minute)
194 cache.Set(ctx, "key2", []byte("data2"), 1*time.Minute)
195
196 stats = cache.Stats()
197 assert.True(t, stats["enabled"].(bool))
198 assert.Equal(t, 2, stats["size"].(int))
199 assert.Equal(t, 1000, stats["max_size"].(int))
200 assert.Contains(t, stats, "hits")
201 assert.Contains(t, stats, "misses")
202 assert.Contains(t, stats, "hit_ratio")
203}
204
205func TestCacheStatsDisabled(t *testing.T) {
206 config := CacheConfig{Enabled: false}
207 cache, err := NewCache(config)
208 require.NoError(t, err)
209
210 stats := cache.Stats()
211 assert.False(t, stats["enabled"].(bool))
212 assert.Len(t, stats, 1) // Only "enabled" key should be present
213}
214
215func TestCacheDisabled(t *testing.T) {
216 config := CacheConfig{Enabled: false}
217 cache, err := NewCache(config)
218 require.NoError(t, err)
219
220 ctx := context.Background()
221 key := "test-key"
222 data := []byte("test data")
223 ttl := 1 * time.Minute
224
225 // All operations should be no-ops
226 cache.Set(ctx, key, data, ttl)
227 result, found := cache.Get(ctx, key)
228 assert.False(t, found)
229 assert.Nil(t, result)
230
231 cache.Delete(ctx, key)
232 cache.Clear()
233
234 // Should not panic
235 assert.NotPanics(t, func() {
236 cache.Close()
237 })
238}
239
240func TestGenerateCacheKey(t *testing.T) {
241 cache, err := NewCache(DefaultCacheConfig())
242 require.NoError(t, err)
243
244 tests := []struct {
245 name string
246 method string
247 endpoint string
248 params map[string]any
249 wantSame bool
250 }{
251 {
252 name: "same parameters",
253 method: "GET",
254 endpoint: "/search",
255 params: map[string]any{"q": "test", "count": 10},
256 wantSame: true,
257 },
258 {
259 name: "different method",
260 method: "POST",
261 endpoint: "/search",
262 params: map[string]any{"q": "test", "count": 10},
263 wantSame: false,
264 },
265 {
266 name: "different endpoint",
267 method: "GET",
268 endpoint: "/corpus",
269 params: map[string]any{"q": "test", "count": 10},
270 wantSame: false,
271 },
272 {
273 name: "different parameters",
274 method: "GET",
275 endpoint: "/search",
276 params: map[string]any{"q": "different", "count": 10},
277 wantSame: false,
278 },
279 {
280 name: "nil parameters",
281 method: "GET",
282 endpoint: "/search",
283 params: nil,
284 wantSame: false,
285 },
286 }
287
288 baseKey := cache.generateCacheKey("GET", "/search", map[string]any{"q": "test", "count": 10})
289
290 for _, tt := range tests {
291 t.Run(tt.name, func(t *testing.T) {
292 key := cache.generateCacheKey(tt.method, tt.endpoint, tt.params)
293
294 assert.NotEmpty(t, key)
295 assert.Len(t, key, 32) // MD5 hash length
296
297 if tt.wantSame {
298 assert.Equal(t, baseKey, key)
299 } else {
300 assert.NotEqual(t, baseKey, key)
301 }
302 })
303 }
304}
305
306func TestCacheKeyDeterministic(t *testing.T) {
307 cache, err := NewCache(DefaultCacheConfig())
308 require.NoError(t, err)
309
310 // Test that map parameter order doesn't affect cache key generation
311 // This verifies that json.Marshal provides deterministic ordering
312 params1 := map[string]any{
313 "query": "test search",
314 "count": 50,
315 "offset": 0,
316 "corpus": "news-corpus",
317 "language": "poliqarp",
318 }
319
320 params2 := map[string]any{
321 "language": "poliqarp",
322 "corpus": "news-corpus",
323 "offset": 0,
324 "count": 50,
325 "query": "test search",
326 }
327
328 // Same parameters in different map creation order should produce same cache key
329 key1 := cache.generateCacheKey("GET", "/search", params1)
330 key2 := cache.generateCacheKey("GET", "/search", params2)
331
332 assert.Equal(t, key1, key2, "Cache keys should be identical regardless of map parameter order")
333 assert.Len(t, key1, 32, "Cache key should be MD5 hash length")
334 assert.Len(t, key2, 32, "Cache key should be MD5 hash length")
335
336 // Generate keys multiple times to ensure consistency
337 for i := 0; i < 10; i++ {
338 keyN := cache.generateCacheKey("GET", "/search", params1)
339 assert.Equal(t, key1, keyN, "Cache key should be consistent across multiple generations")
340 }
341}
342
343func TestGetTTLForEndpoint(t *testing.T) {
344 config := DefaultCacheConfig()
345 cache, err := NewCache(config)
346 require.NoError(t, err)
347
348 tests := []struct {
349 name string
350 endpoint string
351 expected time.Duration
352 }{
353 {
354 name: "search endpoint",
355 endpoint: "/api/v1/search",
356 expected: config.SearchTTL,
357 },
358 {
359 name: "query endpoint",
360 endpoint: "/query",
361 expected: config.SearchTTL,
362 },
363 {
364 name: "corpus endpoint",
365 endpoint: "/corpus",
366 expected: config.MetadataTTL,
367 },
368 {
369 name: "metadata endpoint",
370 endpoint: "/metadata",
371 expected: config.MetadataTTL,
372 },
373 {
374 name: "statistics endpoint",
375 endpoint: "/statistics",
376 expected: config.MetadataTTL,
377 },
378 {
379 name: "info endpoint",
380 endpoint: "/info",
381 expected: config.MetadataTTL,
382 },
383 {
384 name: "other endpoint",
385 endpoint: "/other",
386 expected: config.DefaultTTL,
387 },
388 {
389 name: "case insensitive",
390 endpoint: "/API/V1/SEARCH",
391 expected: config.SearchTTL,
392 },
393 }
394
395 for _, tt := range tests {
396 t.Run(tt.name, func(t *testing.T) {
397 ttl := cache.GetTTLForEndpoint(tt.endpoint)
398 assert.Equal(t, tt.expected, ttl)
399 })
400 }
401}
402
403func TestCacheEntry(t *testing.T) {
404 t.Run("not expired", func(t *testing.T) {
405 entry := &CacheEntry{
406 Data: []byte("test"),
407 Timestamp: time.Now(),
408 TTL: 1 * time.Minute,
409 }
410
411 assert.False(t, entry.IsExpired())
412 })
413
414 t.Run("expired", func(t *testing.T) {
415 entry := &CacheEntry{
416 Data: []byte("test"),
417 Timestamp: time.Now().Add(-2 * time.Minute),
418 TTL: 1 * time.Minute,
419 }
420
421 assert.True(t, entry.IsExpired())
422 })
423}
424
425func TestCacheClose(t *testing.T) {
426 cache, err := NewCache(DefaultCacheConfig())
427 require.NoError(t, err)
428
429 // Add some data
430 ctx := context.Background()
431 cache.Set(ctx, "key1", []byte("data1"), 1*time.Minute)
432
433 // Close should not error
434 err = cache.Close()
435 assert.NoError(t, err)
436
437 // Disabled cache close should also not error
438 disabledCache, err := NewCache(CacheConfig{Enabled: false})
439 require.NoError(t, err)
440
441 err = disabledCache.Close()
442 assert.NoError(t, err)
443}