240 lines
7.5 KiB
Go
240 lines
7.5 KiB
Go
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()
|
|
client := NewRedisClient(redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6379",
|
|
}), metrics.New())
|
|
|
|
// 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()
|
|
client := NewRedisClient(redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6379",
|
|
}), metrics.New())
|
|
|
|
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()
|
|
client := NewRedisClient(redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6379",
|
|
}), metrics.New())
|
|
|
|
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()
|
|
client := NewRedisClient(redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6379",
|
|
}), metrics.New())
|
|
|
|
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()
|
|
client := NewRedisClient(redis.NewClient(&redis.Options{
|
|
Addr: "localhost:6379",
|
|
}), metrics.New())
|
|
|
|
// 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)
|
|
}
|
|
}
|