package redis import ( "context" "errors" "fmt" "os" "testing" "time" "git.yuntuo.hk/k2safe/go-omni/cache" goredis "github.com/redis/go-redis/v9" ) type fakeClient struct { goredis.Cmdable key string ttl time.Duration failure error } func (f *fakeClient) Get(ctx context.Context, key string) *goredis.StringCmd { f.key = key return goredis.NewStringResult("", f.failure) } func (f *fakeClient) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) *goredis.StatusCmd { f.key, f.ttl = key, ttl return goredis.NewStatusResult("OK", f.failure) } func (f *fakeClient) Del(ctx context.Context, keys ...string) *goredis.IntCmd { f.key = keys[0] return goredis.NewIntResult(1, f.failure) } func TestAdapterPreservesErrorsAndExpiration(t *testing.T) { f := &fakeClient{failure: goredis.Nil} store, _ := New(f, "app:") if _, err := store.Get(context.Background(), "key"); !errors.Is(err, cache.ErrMiss) || f.key != "app:key" { t.Fatal(err, f.key) } f.failure = errors.New("unavailable") if _, err := store.Get(context.Background(), "key"); !errors.Is(err, f.failure) { t.Fatal(err) } f.failure = nil if err := store.Set(context.Background(), "key", []byte("value"), time.Nanosecond); err != nil || f.ttl != time.Millisecond { t.Fatal(err, f.ttl) } if err := store.Set(context.Background(), "key", nil, -time.Second); err == nil { t.Fatal("negative TTL accepted") } if err := store.Delete(context.Background(), "key"); err != nil || f.key != "app:key" { t.Fatal(err) } } func TestRedisIntegration(t *testing.T) { address := os.Getenv("GO_OMNI_REDIS_TEST_ADDR") if address == "" { t.Skip("set GO_OMNI_REDIS_TEST_ADDR to an isolated test Redis") } client := goredis.NewClient(&goredis.Options{Addr: address, PoolSize: 2, MaxRetries: -1, ContextTimeoutEnabled: true, DialTimeout: time.Second, ReadTimeout: time.Second, WriteTimeout: time.Second}) defer client.Close() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := client.Ping(ctx).Err(); err != nil { t.Fatal(err) } prefix := fmt.Sprintf("goOmniTest:%d:", time.Now().UnixNano()) store, _ := New(client, prefix) defer store.Delete(context.Background(), "key", "forever", "list", "canceled") if err := store.Set(ctx, "key", []byte{0, 1, 255}, 80*time.Millisecond); err != nil { t.Fatal(err) } v, err := store.Get(ctx, "key") if err != nil || len(v) != 3 || v[2] != 255 { t.Fatal(v, err) } deadline := time.Now().Add(time.Second) for { _, err := store.Get(ctx, "key") if errors.Is(err, cache.ErrMiss) { break } if err != nil || time.Now().After(deadline) { t.Fatal("TTL failed", err) } time.Sleep(5 * time.Millisecond) } if err := store.Set(ctx, "forever", []byte("ok"), 0); err != nil { t.Fatal(err) } if ttl := client.PTTL(ctx, prefix+"forever").Val(); ttl != -1 { t.Fatalf("unexpected persistent TTL %v", ttl) } if err := store.Delete(ctx, "forever"); err != nil { t.Fatal(err) } if _, err := store.Get(ctx, "forever"); !errors.Is(err, cache.ErrMiss) { t.Fatal(err) } if err := client.RPush(ctx, prefix+"list", "value").Err(); err != nil { t.Fatal(err) } if _, err := store.Get(ctx, "list"); err == nil || errors.Is(err, cache.ErrMiss) { t.Fatal("Redis error treated as miss", err) } canceled, stop := context.WithCancel(ctx) stop() if err := store.Set(canceled, "canceled", []byte("value"), 0); !errors.Is(err, context.Canceled) { t.Fatal(err) } if _, err := store.Get(ctx, "canceled"); !errors.Is(err, cache.ErrMiss) { t.Fatal("canceled write reached Redis", err) } if err := store.Delete(ctx, "list"); err != nil { t.Fatal(err) } if err := client.Close(); err != nil { t.Fatal(err) } if _, err := store.Get(ctx, "key"); err == nil || errors.Is(err, cache.ErrMiss) { t.Fatal("closed client treated as miss", err) } }