This commit is contained in:
Vendored
-28
@@ -1,28 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCacheOptions(t *testing.T) {
|
||||
t.Run("default options", func(t *testing.T) {
|
||||
o := newOptions()
|
||||
assert.Equal(t, defaultExpiry, o.Expiry)
|
||||
assert.Equal(t, defaultNotFoundExpiry, o.NotFoundExpiry)
|
||||
})
|
||||
|
||||
t.Run("with expiry", func(t *testing.T) {
|
||||
o := newOptions(WithExpiry(time.Second))
|
||||
assert.Equal(t, time.Second, o.Expiry)
|
||||
assert.Equal(t, defaultNotFoundExpiry, o.NotFoundExpiry)
|
||||
})
|
||||
|
||||
t.Run("with not found expiry", func(t *testing.T) {
|
||||
o := newOptions(WithNotFoundExpiry(time.Second))
|
||||
assert.Equal(t, defaultExpiry, o.Expiry)
|
||||
assert.Equal(t, time.Second, o.NotFoundExpiry)
|
||||
})
|
||||
}
|
||||
Vendored
+7
@@ -109,6 +109,13 @@ func (cc CachedConn) QueryCtx(ctx context.Context, v interface{}, key string, qu
|
||||
}
|
||||
return cc.SetCache(key, v)
|
||||
}
|
||||
// Cache data corrupted (e.g. bad JSON), delete key and fall through to DB
|
||||
_ = cc.DelCache(key)
|
||||
err = query(cc.db.WithContext(ctx), v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cc.SetCache(key, v)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Vendored
+168
-51
@@ -2,65 +2,182 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/orm"
|
||||
"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"
|
||||
"gorm.io/plugin/soft_delete"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Id int64 `gorm:"primarykey"`
|
||||
Email string `gorm:"index:idx_email;type:varchar(100);unique;not null;comment:电子邮箱"`
|
||||
Password string `gorm:"type:varchar(100);comment:用户密码;not null"`
|
||||
Avatar string `gorm:"type:varchar(200);default:'';comment:用户头像"`
|
||||
Balance int64 `gorm:"default:0;comment:用户余额"`
|
||||
Telegram int64 `gorm:"default:null;comment:Telegram账号"`
|
||||
ReferCode string `gorm:"type:varchar(20);default:'';comment:推荐码"`
|
||||
RefererId int64 `gorm:"comment:推荐人ID"`
|
||||
Enable bool `gorm:"default:true;not null;comment:账户是否可用"`
|
||||
IsAdmin bool `gorm:"default:false;not null;comment:是否管理员"`
|
||||
ValidEmail bool `gorm:"default:false;not null;comment:是否验证邮箱"`
|
||||
EnableEmailNotify bool `gorm:"default:false;not null;comment:是否启用邮件通知"`
|
||||
EnableTelegramNotify bool `gorm:"default:false;not null;comment:是否启用Telegram通知"`
|
||||
EnableBalanceNotify bool `gorm:"default:false;not null;comment:是否启用余额变动通知"`
|
||||
EnableLoginNotify bool `gorm:"default:false;not null;comment:是否启用登录通知"`
|
||||
EnableSubscribeNotify bool `gorm:"default:false;not null;comment:是否启用订阅通知"`
|
||||
EnableTradeNotify bool `gorm:"default:false;not null;comment:是否启用交易通知"`
|
||||
CreatedAt time.Time `gorm:"<-:create;comment:创建时间"`
|
||||
UpdatedAt time.Time `gorm:"comment:更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"default:null;comment:删除时间"`
|
||||
IsDel soft_delete.DeletedAt `gorm:"softDelete:flag,DeletedAtField:DeletedAt;comment:1:正常 0:删除"` // Use `1` `0` to identify
|
||||
// testUser is a simple struct used across all QueryCtx tests.
|
||||
type testUser struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func TestGormCacheCtx(t *testing.T) {
|
||||
t.Skipf("skip TestGormCacheCtx test")
|
||||
db, err := orm.ConnectMysql(orm.Mysql{
|
||||
Config: orm.Config{
|
||||
Addr: "localhost:3306",
|
||||
Config: "charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai",
|
||||
Dbname: "vpnboard",
|
||||
Username: "root",
|
||||
Password: "mylove520",
|
||||
},
|
||||
// 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(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
rds := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
})
|
||||
conn := NewConn(db, rds)
|
||||
var u User
|
||||
key := "user:id"
|
||||
err = conn.QueryCtx(context.Background(), &u, key, func(conn *gorm.DB, v interface{}) error {
|
||||
return conn.Where("id = ?", 1).First(v).Error
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Logf("get cache success %+v", u)
|
||||
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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user