package cache import ( "context" "testing" "github.com/go-redis/redis/v8" "trip-planner/internal/metrics" ) // TestCacheGetSet tests basic Get and Set operations. func TestCacheGetSet(t *testing.T) { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", }) client := NewRedisClient(rdb, metrics.New()) defer rdb.Close() // Test Set key := &CacheKey{Kind: "city", Code: "c146"} err := client.Set(ctx, key, []byte(`{"code":"c146"}`), CityTTL) if err != nil { t.Fatalf("expected no error from Set, got: %v", err) } // Test Get (cache hit) data, err := client.Get(ctx, key) if err != nil { t.Fatalf("expected no error from Get, got: %v", err) } if string(data) != `{"code":"c146"}` { t.Errorf("expected %s, got %s", `{"code":"c146"}`, string(data)) } // Test Exists exists, err := client.Exists(ctx, key) if err != nil { t.Fatalf("expected no error from Exists, got: %v", err) } if !exists { t.Error("expected key to exist") } // Test Delete err = client.Delete(ctx, key) if err != nil { t.Fatalf("expected no error from Delete, got: %v", err) } // Test Get after Delete (cache miss) _, err = client.Get(ctx, key) if err != nil { t.Fatalf("expected no error from Get after Delete, got: %v", err) } } // TestCacheKeyString tests key string conversion. func TestCacheKeyString(t *testing.T) { // City key cityKey := &CacheKey{Kind: "city", Code: "c146"} expectedCityKey := "cities:c146" if keyString(cityKey) != expectedCityKey { t.Errorf("expected %s, got %s", expectedCityKey, keyString(cityKey)) } // Station key stationKey := &CacheKey{Kind: "station", Code: "s9600213"} expectedStationKey := "stations:s9600213" if keyString(stationKey) != expectedStationKey { t.Errorf("expected %s, got %s", expectedStationKey, keyString(stationKey)) } // Search key searchKey := &CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-08-15"} expectedSearchKey := "search:c146:c213:2026-08-15:near" if keyString(searchKey) != expectedSearchKey { t.Errorf("expected %s, got %s", expectedSearchKey, keyString(searchKey)) } // Search key with far-term flag searchKeyFarTerm := &CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-08-30", Request: "far"} expectedSearchKeyFarTerm := "search:c146:c213:2026-08-30:far" if keyString(searchKeyFarTerm) != expectedSearchKeyFarTerm { t.Errorf("expected %s, got %s", expectedSearchKeyFarTerm, keyString(searchKeyFarTerm)) } } // TestCacheAsideGetOrSet tests the cache-aside GetOrSetFuncPattern. func TestCacheAsideGetOrSet(t *testing.T) { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", }) client := NewRedisClient(rdb, metrics.New()) defer rdb.Close() fetchCallCount := 0 fetchFunc := func() ([]byte, error) { fetchCallCount++ return []byte(`{"found":true}`), nil } // First call: cache miss, should fetch from backend key := &CacheKey{Kind: "station", Code: "s9600213"} data, err := NewCacheAside(client, metrics.New()).GetOrSetFuncPattern(ctx, key, fetchFunc, CityTTL) if err != nil { t.Fatalf("expected no error on cache miss, got: %v", err) } if string(data) != `{"found":true}` { t.Errorf("expected %s, got %s", `{"found":true}`, string(data)) } if fetchCallCount != 1 { t.Errorf("expected 1 fetch call, got %d", fetchCallCount) } // Second call: cache hit, should not fetch from backend fetchCallCount = 0 data, err = NewCacheAside(client, metrics.New()).GetOrSetFuncPattern(ctx, key, fetchFunc, CityTTL) if err != nil { t.Fatalf("expected no error on cache hit, got: %v", err) } if string(data) != `{"found":true}` { t.Errorf("expected %s, got %s", `{"found":true}`, string(data)) } if fetchCallCount != 0 { t.Errorf("expected 0 fetch calls on cache hit, got %d", fetchCallCount) } } // TestCacheAsideGetCity tests GetCity with cache. func TestCacheAsideGetCity(t *testing.T) { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", }) client := NewRedisClient(rdb, metrics.New()) defer rdb.Close() fetchCallCount := 0 fetchFunc := func() ([]byte, error) { fetchCallCount++ return []byte(`{"code":"c146","title":"Simferopol"}`), nil } key := &CacheKey{Kind: "city", Code: "c146"} data, err := NewCacheAside(client, metrics.New()).GetCity(ctx, key, fetchFunc) if err != nil { t.Fatalf("expected no error on cache miss for city, got: %v", err) } if string(data) != `{"code":"c146","title":"Simferopol"}` { t.Errorf("expected %s, got %s", `{"code":"c146","title":"Simferopol"}`, string(data)) } if fetchCallCount != 1 { t.Errorf("expected 1 fetch call, got %d", fetchCallCount) } // Second call: cache hit fetchCallCount = 0 data, err = NewCacheAside(client, metrics.New()).GetCity(ctx, key, fetchFunc) if err != nil { t.Fatalf("expected no error on cache hit for city, got: %v", err) } if string(data) != `{"code":"c146","title":"Simferopol"}` { t.Errorf("expected %s, got %s", `{"code":"c146","title":"Simferopol"}`, string(data)) } if fetchCallCount != 0 { t.Errorf("expected 0 fetch calls on cache hit, got %d", fetchCallCount) } } // TestCacheAsideGetSearch tests GetSearch with near-term and far-term TTL. func TestCacheAsideGetSearch(t *testing.T) { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", }) client := NewRedisClient(rdb, metrics.New()) defer rdb.Close() fetchNearTerm := func() ([]byte, error) { return []byte(`{"near_term":true}`), nil } fetchFarTerm := func() ([]byte, error) { return []byte(`{"far_term":true}`), nil } // Near-term search key nearKey := &CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-08-15"} // Far-term search key farKey := &CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-09-15"} // Near-term: should use SearchNearTermTTL (3 hours) data, err := NewCacheAside(client, metrics.New()).GetSearch(ctx, nearKey, fetchNearTerm, false) if err != nil { t.Fatalf("expected no error on near-term search cache miss, got: %v", err) } if string(data) != `{"near_term":true}` { t.Errorf("expected %s, got %s", `{"near_term":true}`, string(data)) } // Far-term: should use SearchFarTermTTL (7 days) data, err = NewCacheAside(client, metrics.New()).GetSearch(ctx, farKey, fetchFarTerm, true) if err != nil { t.Fatalf("expected no error on far-term search cache miss, got: %v", err) } if string(data) != `{"far_term":true}` { t.Errorf("expected %s, got %s", `{"far_term":true}`, string(data)) } } // TestCacheInvalidate tests invalidation operations. func TestCacheInvalidate(t *testing.T) { ctx := context.Background() rdb := redis.NewClient(&redis.Options{ Addr: "127.0.0.1:6379", }) client := NewRedisClient(rdb, metrics.New()) defer rdb.Close() // Set up some keys cityKey := &CacheKey{Kind: "city", Code: "c146"} stationKey := &CacheKey{Kind: "station", Code: "s9600213"} searchKey := &CacheKey{Kind: "search", From: "c146", To: "c213", Date: "2026-08-15"} // Set values first client.Set(ctx, cityKey, []byte(`{"code":"c146"}`), CityTTL) client.Set(ctx, stationKey, []byte(`{"id":"s9600213"}`), CityTTL) client.Set(ctx, searchKey, []byte(`{"search":true}`), SearchNearTermTTL) // Invalidate city err := NewCacheAside(client, metrics.New()).InvalidateCity(ctx, cityKey) if err != nil { t.Fatalf("expected no error invalidating city, got: %v", err) } // Invalidate station err = NewCacheAside(client, metrics.New()).InvalidateStation(ctx, stationKey) if err != nil { t.Fatalf("expected no error invalidating station, got: %v", err) } // Invalidate search err = NewCacheAside(client, metrics.New()).InvalidateSearch(ctx, searchKey) if err != nil { t.Fatalf("expected no error invalidating search, got: %v", err) } }