package cache import ( "context" "encoding/json" "errors" "testing" "time" "github.com/alicebob/miniredis/v2" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" ) // testUser is a simple struct used across all QueryCtx tests. type testUser struct { ID int64 `json:"id"` Name string `json:"name"` } // setupCachedConn creates a CachedConn backed by a real miniredis instance // and a bare *gorm.DB (no real database connection needed because the // QueryCtxFn callback is fully under our control). func setupCachedConn(t *testing.T) (CachedConn, *miniredis.Miniredis) { t.Helper() mr := miniredis.RunT(t) rdb := redis.NewClient(&redis.Options{ Addr: mr.Addr(), }) t.Cleanup(func() { rdb.Close() }) // Use SQLite in-memory to get a properly initialized *gorm.DB. db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) cc := NewConn(db, rdb, WithExpiry(time.Minute)) return cc, mr } func TestQueryCtx_CacheHit(t *testing.T) { cc, mr := setupCachedConn(t) ctx := context.Background() key := "cache:user:1" // Pre-populate the cache with valid JSON. expected := testUser{ID: 1, Name: "Alice"} data, err := json.Marshal(expected) require.NoError(t, err) mr.Set(key, string(data)) // Track whether the DB query function is called. dbCalled := false queryFn := func(conn *gorm.DB, v interface{}) error { dbCalled = true return nil } var result testUser err = cc.QueryCtx(ctx, &result, key, queryFn) assert.NoError(t, err) assert.False(t, dbCalled, "DB query should NOT be called on cache hit") assert.Equal(t, expected.ID, result.ID) assert.Equal(t, expected.Name, result.Name) } func TestQueryCtx_CacheMiss_QueriesDB_SetsCache(t *testing.T) { cc, mr := setupCachedConn(t) ctx := context.Background() key := "cache:user:2" // Do NOT pre-populate the cache -- this is a cache miss scenario. dbCalled := false queryFn := func(conn *gorm.DB, v interface{}) error { dbCalled = true u := v.(*testUser) u.ID = 2 u.Name = "Bob" return nil } var result testUser err := cc.QueryCtx(ctx, &result, key, queryFn) assert.NoError(t, err) assert.True(t, dbCalled, "DB query should be called on cache miss") assert.Equal(t, int64(2), result.ID) assert.Equal(t, "Bob", result.Name) // Verify the value was written back to cache. cached, cacheErr := mr.Get(key) require.NoError(t, cacheErr) var cachedUser testUser require.NoError(t, json.Unmarshal([]byte(cached), &cachedUser)) assert.Equal(t, int64(2), cachedUser.ID) assert.Equal(t, "Bob", cachedUser.Name) } func TestQueryCtx_CorruptedCache_SelfHeals(t *testing.T) { cc, mr := setupCachedConn(t) ctx := context.Background() key := "cache:user:3" // Store invalid JSON in the cache to simulate corruption. mr.Set(key, "THIS IS NOT VALID JSON{{{") dbCalled := false queryFn := func(conn *gorm.DB, v interface{}) error { dbCalled = true u := v.(*testUser) u.ID = 3 u.Name = "Charlie" return nil } var result testUser err := cc.QueryCtx(ctx, &result, key, queryFn) assert.NoError(t, err) assert.True(t, dbCalled, "DB query should be called when cache is corrupted") assert.Equal(t, int64(3), result.ID) assert.Equal(t, "Charlie", result.Name) // Verify the corrupt key was replaced with valid data. cached, cacheErr := mr.Get(key) require.NoError(t, cacheErr) var cachedUser testUser require.NoError(t, json.Unmarshal([]byte(cached), &cachedUser)) assert.Equal(t, int64(3), cachedUser.ID) assert.Equal(t, "Charlie", cachedUser.Name) } func TestQueryCtx_CacheMiss_DBFails_ReturnsError(t *testing.T) { cc, mr := setupCachedConn(t) ctx := context.Background() key := "cache:user:4" // No cache entry -- this is a miss. dbErr := errors.New("connection refused") queryFn := func(conn *gorm.DB, v interface{}) error { return dbErr } var result testUser err := cc.QueryCtx(ctx, &result, key, queryFn) assert.Error(t, err) assert.Equal(t, dbErr, err) // Cache should remain empty -- no value was written. assert.False(t, mr.Exists(key), "cache should NOT be set when DB query fails") } func TestQueryCtx_CorruptedCache_DBFails_ReturnsError(t *testing.T) { cc, mr := setupCachedConn(t) ctx := context.Background() key := "cache:user:5" // Store invalid JSON to trigger the corruption branch. mr.Set(key, "<<>>") dbErr := errors.New("database is down") queryFn := func(conn *gorm.DB, v interface{}) error { return dbErr } var result testUser err := cc.QueryCtx(ctx, &result, key, queryFn) assert.Error(t, err) assert.Equal(t, dbErr, err) // The corrupt key should have been deleted (DelCache was called), // and no new value was set because the DB query failed. assert.False(t, mr.Exists(key), "corrupt key should be deleted even when DB fails") }