package cache_test import ( "context" "encoding/json" "errors" "fmt" "testing" "time" "tercul/internal/platform/cache" "tercul/internal/platform/config" "github.com/go-redis/redismock/v9" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewRedisCache(t *testing.T) { client, _ := redismock.NewClientMock() keyGen := cache.NewDefaultKeyGenerator("test:") defaultExpiry := 2 * time.Hour c := cache.NewRedisCache(client, keyGen, defaultExpiry) require.NotNil(t, c) } func TestNewDefaultRedisCache(t *testing.T) { t.Run("success", func(t *testing.T) { // This test requires a running Redis instance or a mock for the Ping command. // For now, we'll just test that the function returns a non-nil cache // when the config is valid. A more comprehensive test would involve // mocking the redis.NewClient and its Ping method. cfg := &config.Config{ RedisAddr: "localhost:6379", } // Since this function actually tries to connect, we can't fully test it in a unit test // without a live redis. We will skip a full test here and focus on the other methods. // In a real-world scenario, we might use a test container with Redis. // For now, we'll just ensure it doesn't panic with a basic config. // A proper integration test would be better suited for this. _ = cfg // _, err := cache.NewDefaultRedisCache(cfg) // assert.NoError(t, err) }) t.Run("connection error", func(t *testing.T) { cfg := &config.Config{ RedisAddr: "localhost:9999", // Invalid address } _, err := cache.NewDefaultRedisCache(cfg) assert.Error(t, err) }) } func TestRedisCache_Get(t *testing.T) { client, mock := redismock.NewClientMock() c := cache.NewRedisCache(client, nil, time.Hour) ctx := context.Background() key := "test-key" expectedValue := map[string]string{"foo": "bar"} expectedBytes, _ := json.Marshal(expectedValue) t.Run("success", func(t *testing.T) { mock.ExpectGet(key).SetVal(string(expectedBytes)) var result map[string]string err := c.Get(ctx, key, &result) assert.NoError(t, err) assert.Equal(t, expectedValue, result) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("cache miss", func(t *testing.T) { mock.ExpectGet(key).RedisNil() var result map[string]string err := c.Get(ctx, key, &result) assert.Error(t, err) assert.Equal(t, "cache miss", err.Error()) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("redis error", func(t *testing.T) { mock.ExpectGet(key).SetErr(errors.New("redis error")) var result map[string]string err := c.Get(ctx, key, &result) assert.Error(t, err) assert.Equal(t, "redis error", err.Error()) assert.NoError(t, mock.ExpectationsWereMet()) }) } func TestRedisCache_InvalidateEntityType(t *testing.T) { ctx := context.Background() entityType := "user" pattern := "tercul:user:*" t.Run("success with multiple batches", func(t *testing.T) { client, mock := redismock.NewClientMock() keyGen := cache.NewDefaultKeyGenerator("tercul:") c := cache.NewRedisCache(client, keyGen, time.Hour) keysBatch1 := make([]string, 100) for i := 0; i < 100; i++ { keysBatch1[i] = fmt.Sprintf("tercul:user:key%d", i) } keysBatch2 := []string{"tercul:user:key101", "tercul:user:key102"} // Mocking the SCAN and DEL calls in the correct order mock.ExpectScan(0, pattern, 100).SetVal(keysBatch1, 1) mock.ExpectDel(keysBatch1...).SetVal(100) mock.ExpectScan(1, pattern, 100).SetVal(keysBatch2, 0) mock.ExpectDel(keysBatch2...).SetVal(2) err := c.InvalidateEntityType(ctx, entityType) assert.NoError(t, err) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("success with single batch", func(t *testing.T) { client, mock := redismock.NewClientMock() keyGen := cache.NewDefaultKeyGenerator("tercul:") c := cache.NewRedisCache(client, keyGen, time.Hour) keys := []string{"tercul:user:key1", "tercul:user:key2"} mock.ExpectScan(0, pattern, 100).SetVal(keys, 0) mock.ExpectDel(keys...).SetVal(2) err := c.InvalidateEntityType(ctx, entityType) assert.NoError(t, err) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("scan error", func(t *testing.T) { client, mock := redismock.NewClientMock() keyGen := cache.NewDefaultKeyGenerator("tercul:") c := cache.NewRedisCache(client, keyGen, time.Hour) mock.ExpectScan(0, pattern, 100).SetErr(errors.New("scan error")) err := c.InvalidateEntityType(ctx, entityType) assert.Error(t, err) assert.Equal(t, "scan error", err.Error()) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("del error", func(t *testing.T) { client, mock := redismock.NewClientMock() keyGen := cache.NewDefaultKeyGenerator("tercul:") c := cache.NewRedisCache(client, keyGen, time.Hour) keys := []string{"tercul:user:key1"} mock.ExpectScan(0, pattern, 100).SetVal(keys, 0) mock.ExpectDel(keys...).SetErr(errors.New("del error")) err := c.InvalidateEntityType(ctx, entityType) assert.Error(t, err) assert.Equal(t, "del error", err.Error()) assert.NoError(t, mock.ExpectationsWereMet()) }) } func TestRedisCache_GetMulti(t *testing.T) { client, mock := redismock.NewClientMock() c := cache.NewRedisCache(client, nil, time.Hour) ctx := context.Background() keys := []string{"key1", "key2", "key3"} expectedValues := []interface{}{"value1", nil, "value3"} expectedResult := map[string][]byte{ "key1": []byte("value1"), "key3": []byte("value3"), } t.Run("success", func(t *testing.T) { mock.ExpectMGet(keys...).SetVal(expectedValues) result, err := c.GetMulti(ctx, keys) assert.NoError(t, err) assert.Equal(t, expectedResult, result) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("no keys", func(t *testing.T) { result, err := c.GetMulti(ctx, []string{}) assert.NoError(t, err) assert.Empty(t, result) }) t.Run("redis error", func(t *testing.T) { mock.ExpectMGet(keys...).SetErr(errors.New("redis error")) _, err := c.GetMulti(ctx, keys) assert.Error(t, err) assert.Equal(t, "redis error", err.Error()) assert.NoError(t, mock.ExpectationsWereMet()) }) } func TestRedisCache_SetMulti(t *testing.T) { ctx := context.Background() items := map[string]interface{}{ "key1": "value1", "key2": 123, } t.Run("success", func(t *testing.T) { client, mock := redismock.NewClientMock() c := cache.NewRedisCache(client, nil, time.Hour) mock.MatchExpectationsInOrder(false) // Expect each Set command within the pipeline for key, value := range items { data, _ := json.Marshal(value) mock.ExpectSet(key, data, time.Hour).SetVal("OK") } // The SetMulti function will call Exec(), which triggers the mock's pipeline hook err := c.SetMulti(ctx, items, 0) assert.NoError(t, err) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("no items", func(t *testing.T) { client, _ := redismock.NewClientMock() c := cache.NewRedisCache(client, nil, time.Hour) err := c.SetMulti(ctx, map[string]interface{}{}, 0) assert.NoError(t, err) }) t.Run("redis error", func(t *testing.T) { client, mock := redismock.NewClientMock() c := cache.NewRedisCache(client, nil, time.Hour) mock.MatchExpectationsInOrder(false) // To simulate a pipeline execution error, we make one of the inner commands fail. // The hook will stop processing on the first error and return it. data1, _ := json.Marshal("value1") mock.ExpectSet("key1", data1, time.Hour).SetErr(errors.New("redis error")) data2, _ := json.Marshal(123) mock.ExpectSet("key2", data2, time.Hour).SetVal("OK") err := c.SetMulti(ctx, items, 0) assert.Error(t, err) assert.Contains(t, err.Error(), "redis error") }) } func TestRedisCache_EntityOperations(t *testing.T) { client, mock := redismock.NewClientMock() keyGen := cache.NewDefaultKeyGenerator("tercul:") c := cache.NewRedisCache(client, keyGen, time.Hour) ctx := context.Background() entityType := "user" id := uint(123) entityKey := "tercul:user:id:123" value := map[string]string{"name": "test"} valueBytes, _ := json.Marshal(value) page := 1 pageSize := 10 listKey := "tercul:user:list:1:10" listValue := []string{"user1", "user2"} listValueBytes, _ := json.Marshal(listValue) t.Run("GetEntity", func(t *testing.T) { mock.ExpectGet(entityKey).SetVal(string(valueBytes)) var result map[string]string err := c.GetEntity(ctx, entityType, id, &result) assert.NoError(t, err) assert.Equal(t, value, result) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("SetEntity", func(t *testing.T) { mock.ExpectSet(entityKey, valueBytes, time.Hour).SetVal("OK") err := c.SetEntity(ctx, entityType, id, value, 0) assert.NoError(t, err) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("DeleteEntity", func(t *testing.T) { mock.ExpectDel(entityKey).SetVal(1) err := c.DeleteEntity(ctx, entityType, id) assert.NoError(t, err) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("GetList", func(t *testing.T) { mock.ExpectGet(listKey).SetVal(string(listValueBytes)) var result []string err := c.GetList(ctx, entityType, page, pageSize, &result) assert.NoError(t, err) assert.Equal(t, listValue, result) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("SetList", func(t *testing.T) { mock.ExpectSet(listKey, listValueBytes, time.Hour).SetVal("OK") err := c.SetList(ctx, entityType, page, pageSize, listValue, 0) assert.NoError(t, err) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("DeleteList", func(t *testing.T) { mock.ExpectDel(listKey).SetVal(1) err := c.DeleteList(ctx, entityType, page, pageSize) assert.NoError(t, err) assert.NoError(t, mock.ExpectationsWereMet()) }) } func TestRedisCache_Set(t *testing.T) { client, mock := redismock.NewClientMock() c := cache.NewRedisCache(client, nil, time.Hour) ctx := context.Background() key := "test-key" value := map[string]string{"foo": "bar"} expectedBytes, _ := json.Marshal(value) t.Run("success with default expiration", func(t *testing.T) { mock.ExpectSet(key, expectedBytes, time.Hour).SetVal("OK") err := c.Set(ctx, key, value, 0) assert.NoError(t, err) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("success with custom expiration", func(t *testing.T) { expiration := 5 * time.Minute mock.ExpectSet(key, expectedBytes, expiration).SetVal("OK") err := c.Set(ctx, key, value, expiration) assert.NoError(t, err) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("redis error", func(t *testing.T) { mock.ExpectSet(key, expectedBytes, time.Hour).SetErr(errors.New("redis error")) err := c.Set(ctx, key, value, 0) assert.Error(t, err) assert.Equal(t, "redis error", err.Error()) assert.NoError(t, mock.ExpectationsWereMet()) }) } func TestRedisCache_Delete(t *testing.T) { client, mock := redismock.NewClientMock() c := cache.NewRedisCache(client, nil, time.Hour) ctx := context.Background() key := "test-key" t.Run("success", func(t *testing.T) { mock.ExpectDel(key).SetVal(1) err := c.Delete(ctx, key) assert.NoError(t, err) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("redis error", func(t *testing.T) { mock.ExpectDel(key).SetErr(errors.New("redis error")) err := c.Delete(ctx, key) assert.Error(t, err) assert.Equal(t, "redis error", err.Error()) assert.NoError(t, mock.ExpectationsWereMet()) }) } func TestRedisCache_Clear(t *testing.T) { client, mock := redismock.NewClientMock() c := cache.NewRedisCache(client, nil, time.Hour) ctx := context.Background() t.Run("success", func(t *testing.T) { mock.ExpectFlushAll().SetVal("OK") err := c.Clear(ctx) assert.NoError(t, err) assert.NoError(t, mock.ExpectationsWereMet()) }) t.Run("redis error", func(t *testing.T) { mock.ExpectFlushAll().SetErr(errors.New("redis error")) err := c.Clear(ctx) assert.Error(t, err) assert.Equal(t, "redis error", err.Error()) assert.NoError(t, mock.ExpectationsWereMet()) }) }