hi-server/pkg/cache/gorm_test.go
shanshanzhong 4d913c1728
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m26s
修复缓存
2026-03-06 21:58:29 -08:00

184 lines
4.8 KiB
Go

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, "<<<CORRUPT>>>")
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")
}