All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m26s
184 lines
4.8 KiB
Go
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")
|
|
}
|