package redis_test import ( "bytes" "errors" "strconv" "time" "github.com/redis/go-redis/v9" . "github.com/bsm/ginkgo/v2" . "github.com/bsm/gomega" ) // parseRESPType returns the RESP type character and validates basic structure func parseRESPType(data []byte) (byte, bool) { if len(data) < 3 { return 0, false } // Check for CRLF at the end if !bytes.HasSuffix(data, []byte("\r\n")) { return 0, false } return data[0], true } // countRESPElements counts the number of elements in a RESP array/map response func countRESPElements(data []byte) (int, bool) { if len(data) < 4 { return 0, false } // Find first CRLF idx := bytes.Index(data, []byte("\r\n")) if idx < 2 { return 0, false } count, err := strconv.Atoi(string(data[1:idx])) if err != nil { return 0, false } return count, true } var _ = Describe("Cmd", func() { var client *redis.Client BeforeEach(func() { client = redis.NewClient(redisOptions()) Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) }) AfterEach(func() { Expect(client.Close()).NotTo(HaveOccurred()) }) It("implements Stringer", func() { set := client.Set(ctx, "foo", "bar", 0) Expect(set.String()).To(Equal("set foo bar: OK")) get := client.Get(ctx, "foo") Expect(get.String()).To(Equal("get foo: bar")) }) It("has val/err", func() { set := client.Set(ctx, "key", "hello", 0) Expect(set.Err()).NotTo(HaveOccurred()) Expect(set.Val()).To(Equal("OK")) get := client.Get(ctx, "key") Expect(get.Err()).NotTo(HaveOccurred()) Expect(get.Val()).To(Equal("hello")) Expect(set.Err()).NotTo(HaveOccurred()) Expect(set.Val()).To(Equal("OK")) }) It("has helpers", func() { set := client.Set(ctx, "key", "10", 0) Expect(set.Err()).NotTo(HaveOccurred()) n, err := client.Get(ctx, "key").Int64() Expect(err).NotTo(HaveOccurred()) Expect(n).To(Equal(int64(10))) un, err := client.Get(ctx, "key").Uint64() Expect(err).NotTo(HaveOccurred()) Expect(un).To(Equal(uint64(10))) f, err := client.Get(ctx, "key").Float64() Expect(err).NotTo(HaveOccurred()) Expect(f).To(Equal(float64(10))) }) It("supports float32", func() { f := float32(66.97) err := client.Set(ctx, "float_key", f, 0).Err() Expect(err).NotTo(HaveOccurred()) val, err := client.Get(ctx, "float_key").Float32() Expect(err).NotTo(HaveOccurred()) Expect(val).To(Equal(f)) }) It("supports time.Time", func() { tm := time.Date(2019, 1, 1, 9, 45, 10, 222125, time.UTC) err := client.Set(ctx, "time_key", tm, 0).Err() Expect(err).NotTo(HaveOccurred()) s, err := client.Get(ctx, "time_key").Result() Expect(err).NotTo(HaveOccurred()) Expect(s).To(Equal("2019-01-01T09:45:10.000222125Z")) tm2, err := client.Get(ctx, "time_key").Time() Expect(err).NotTo(HaveOccurred()) Expect(tm2).To(BeTemporally("==", tm)) }) It("allows to set custom error", func() { e := errors.New("custom error") cmd := redis.Cmd{} cmd.SetErr(e) _, err := cmd.Result() Expect(err).To(Equal(e)) }) }) var _ = Describe("RawCmd", func() { var client *redis.Client BeforeEach(func() { client = redis.NewClient(redisOptions()) Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) }) AfterEach(func() { Expect(client.Close()).NotTo(HaveOccurred()) }) It("returns raw RESP bytes for simple string (PING)", func() { cmd := client.DoRaw(ctx, "PING") Expect(cmd.Err()).NotTo(HaveOccurred()) rawBytes, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(string(rawBytes)).To(Equal("+PONG\r\n")) }) It("returns raw RESP bytes for bulk string (GET)", func() { err := client.Set(ctx, "rawtest", "hello", 0).Err() Expect(err).NotTo(HaveOccurred()) cmd := client.DoRaw(ctx, "GET", "rawtest") Expect(cmd.Err()).NotTo(HaveOccurred()) rawBytes, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(string(rawBytes)).To(Equal("$5\r\nhello\r\n")) }) It("returns raw RESP bytes for integer (INCR)", func() { err := client.Set(ctx, "counter", "10", 0).Err() Expect(err).NotTo(HaveOccurred()) cmd := client.DoRaw(ctx, "INCR", "counter") Expect(cmd.Err()).NotTo(HaveOccurred()) rawBytes, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(string(rawBytes)).To(Equal(":11\r\n")) }) It("returns raw RESP bytes for array (KEYS) with validation", func() { err := client.Set(ctx, "key1", "val1", 0).Err() Expect(err).NotTo(HaveOccurred()) err = client.Set(ctx, "key2", "val2", 0).Err() Expect(err).NotTo(HaveOccurred()) cmd := client.DoRaw(ctx, "KEYS", "key*") Expect(cmd.Err()).NotTo(HaveOccurred()) rawBytes, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) // Validate RESP structure respType, valid := parseRESPType(rawBytes) Expect(valid).To(BeTrue()) Expect(respType).To(Equal(byte('*')), "Should be array type") // Verify element count count, ok := countRESPElements(rawBytes) Expect(ok).To(BeTrue()) Expect(count).To(Equal(2)) // Verify keys are present in the raw bytes rawStr := string(rawBytes) Expect(rawStr).To(ContainSubstring("key1")) Expect(rawStr).To(ContainSubstring("key2")) }) It("returns raw RESP bytes for HGETALL with complete validation", func() { err := client.HSet(ctx, "myhash", "field1", "value1", "field2", "value2").Err() Expect(err).NotTo(HaveOccurred()) cmd := client.DoRaw(ctx, "HGETALL", "myhash") Expect(cmd.Err()).NotTo(HaveOccurred()) rawBytes, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) // Validate RESP structure respType, valid := parseRESPType(rawBytes) Expect(valid).To(BeTrue(), "Response should be valid RESP") // HGETALL returns array (RESP2) or map (RESP3) Expect(respType).To(BeElementOf(byte('*'), byte('%')), "Should be array or map type") // Count elements: array has 4 elements (2 key-value pairs), map has 2 pairs count, ok := countRESPElements(rawBytes) Expect(ok).To(BeTrue()) if respType == '*' { Expect(count).To(Equal(4), "Array should have 4 elements (2 key-value pairs)") } else { Expect(count).To(Equal(2), "Map should have 2 pairs") } rawStr := string(rawBytes) Expect(rawStr).To(ContainSubstring("field1")) Expect(rawStr).To(ContainSubstring("value1")) Expect(rawStr).To(ContainSubstring("field2")) Expect(rawStr).To(ContainSubstring("value2")) // Verify response ends with CRLF Expect(rawBytes[len(rawBytes)-2:]).To(Equal([]byte("\r\n"))) }) It("returns raw RESP bytes for nil value", func() { cmd := client.DoRaw(ctx, "GET", "nonexistent") Expect(cmd.Err()).NotTo(HaveOccurred()) rawBytes, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) rawStr := string(rawBytes) Expect(rawStr == "$-1\r\n" || rawStr == "_\r\n").To(BeTrue()) }) It("returns raw RESP bytes for empty array", func() { cmd := client.DoRaw(ctx, "KEYS", "nonexistent*") Expect(cmd.Err()).NotTo(HaveOccurred()) rawBytes, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(string(rawBytes)).To(Equal("*0\r\n")) }) It("has Val and Bytes methods", func() { cmd := client.DoRaw(ctx, "PING") Expect(cmd.Err()).NotTo(HaveOccurred()) val := cmd.Val() Expect(string(val)).To(Equal("+PONG\r\n")) bytes, err := cmd.Bytes() Expect(err).NotTo(HaveOccurred()) Expect(string(bytes)).To(Equal("+PONG\r\n")) }) It("implements Stringer", func() { cmd := client.DoRaw(ctx, "PING") Expect(cmd.Err()).NotTo(HaveOccurred()) Expect(cmd.String()).To(ContainSubstring("PING")) }) It("allows to set custom error", func() { e := errors.New("custom error") cmd := redis.RawCmd{} cmd.SetErr(e) _, err := cmd.Result() Expect(err).To(Equal(e)) }) It("can be cloned", func() { cmd := client.DoRaw(ctx, "PING") Expect(cmd.Err()).NotTo(HaveOccurred()) cloned := cmd.Clone().(*redis.RawCmd) Expect(cloned.Val()).To(Equal(cmd.Val())) originalVal := cmd.Val() clonedVal := cloned.Val() Expect(&originalVal[0]).NotTo(BeIdenticalTo(&clonedVal[0])) }) It("returns raw RESP bytes for error response", func() { cmd := client.DoRaw(ctx, "INVALIDCMD") rawBytes, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(string(rawBytes)).To(HavePrefix("-")) Expect(string(rawBytes)).To(ContainSubstring("ERR")) }) It("returns raw RESP bytes for empty hash (nil map)", func() { // HGETALL on non-existent hash returns empty array/map cmd := client.DoRaw(ctx, "HGETALL", "nonexistent_hash") Expect(cmd.Err()).NotTo(HaveOccurred()) rawBytes, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) respType, valid := parseRESPType(rawBytes) Expect(valid).To(BeTrue()) // Should be empty array or empty map Expect(respType).To(BeElementOf(byte('*'), byte('%'))) count, ok := countRESPElements(rawBytes) Expect(ok).To(BeTrue()) Expect(count).To(Equal(0), "Empty hash should return 0 elements") }) It("returns raw RESP bytes for nested array structures (SMEMBERS)", func() { // Create a set with multiple members err := client.SAdd(ctx, "myset", "member1", "member2", "member3").Err() Expect(err).NotTo(HaveOccurred()) cmd := client.DoRaw(ctx, "SMEMBERS", "myset") Expect(cmd.Err()).NotTo(HaveOccurred()) rawBytes, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) // Validate basic RESP structure respType, valid := parseRESPType(rawBytes) Expect(valid).To(BeTrue()) // SMEMBERS returns array (RESP2) or set (RESP3) Expect(respType).To(BeElementOf(byte('*'), byte('~'))) // Verify count count, ok := countRESPElements(rawBytes) Expect(ok).To(BeTrue()) Expect(count).To(Equal(3)) // Verify members are present rawStr := string(rawBytes) Expect(rawStr).To(ContainSubstring("member1")) Expect(rawStr).To(ContainSubstring("member2")) Expect(rawStr).To(ContainSubstring("member3")) }) It("returns raw RESP bytes for LRANGE with exact byte validation", func() { // Create a list with multiple elements err := client.RPush(ctx, "mylist", "elem1", "elem2", "elem3").Err() Expect(err).NotTo(HaveOccurred()) cmd := client.DoRaw(ctx, "LRANGE", "mylist", "0", "-1") Expect(cmd.Err()).NotTo(HaveOccurred()) rawBytes, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) // LRANGE returns elements in order, so we can validate exact bytes expectedResp := "*3\r\n$5\r\nelem1\r\n$5\r\nelem2\r\n$5\r\nelem3\r\n" Expect(string(rawBytes)).To(Equal(expectedResp)) }) It("returns raw RESP bytes for LPUSH with exact integer response", func() { // LPUSH returns the length of the list after push cmd := client.DoRaw(ctx, "LPUSH", "newlist", "item") Expect(cmd.Err()).NotTo(HaveOccurred()) rawBytes, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(string(rawBytes)).To(Equal(":1\r\n")) }) It("returns raw RESP bytes for ECHO with exact bulk string", func() { cmd := client.DoRaw(ctx, "ECHO", "testmsg") Expect(cmd.Err()).NotTo(HaveOccurred()) rawBytes, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(string(rawBytes)).To(Equal("$7\r\ntestmsg\r\n")) }) }) var _ = Describe("RawWriteToCmd", func() { var client *redis.Client BeforeEach(func() { client = redis.NewClient(redisOptions()) Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) }) AfterEach(func() { Expect(client.Close()).NotTo(HaveOccurred()) }) It("streams raw RESP bytes for PING", func() { var buf bytes.Buffer cmd := client.DoRawWriteTo(ctx, &buf, "PING") written, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(written).To(Equal(int64(7))) Expect(buf.String()).To(Equal("+PONG\r\n")) }) It("streams raw RESP bytes for bulk string (GET)", func() { client.Set(ctx, "rawkey", "rawvalue", 0) var buf bytes.Buffer cmd := client.DoRawWriteTo(ctx, &buf, "GET", "rawkey") written, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(written).To(BeNumerically(">", 0)) Expect(buf.String()).To(Equal("$8\r\nrawvalue\r\n")) }) It("streams raw RESP bytes for integer (INCR)", func() { client.Set(ctx, "writeto_counter", "10", 0) var buf bytes.Buffer cmd := client.DoRawWriteTo(ctx, &buf, "INCR", "writeto_counter") written, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(written).To(Equal(int64(5))) Expect(buf.String()).To(Equal(":11\r\n")) }) It("streams raw RESP bytes for nil value (GET non-existent)", func() { var buf bytes.Buffer cmd := client.DoRawWriteTo(ctx, &buf, "GET", "nonexistent_key") written, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(written).To(Equal(int64(buf.Len()))) rawStr := buf.String() // RESP2 returns $-1\r\n, RESP3 returns _\r\n Expect(rawStr == "$-1\r\n" || rawStr == "_\r\n").To(BeTrue()) }) It("streams raw RESP bytes for array (LRANGE) with exact validation", func() { client.RPush(ctx, "writeto_list", "a", "b", "c") var buf bytes.Buffer cmd := client.DoRawWriteTo(ctx, &buf, "LRANGE", "writeto_list", "0", "-1") written, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(written).To(Equal(int64(buf.Len()))) // LRANGE returns elements in order expectedResp := "*3\r\n$1\r\na\r\n$1\r\nb\r\n$1\r\nc\r\n" Expect(buf.String()).To(Equal(expectedResp)) }) It("streams raw RESP bytes for empty array (LRANGE empty list)", func() { var buf bytes.Buffer cmd := client.DoRawWriteTo(ctx, &buf, "LRANGE", "nonexistent_list", "0", "-1") written, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(written).To(Equal(int64(buf.Len()))) Expect(buf.String()).To(Equal("*0\r\n")) }) It("streams raw RESP bytes for HGETALL", func() { client.HSet(ctx, "rawhash", "f1", "v1", "f2", "v2") var buf bytes.Buffer cmd := client.DoRawWriteTo(ctx, &buf, "HGETALL", "rawhash") written, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(written).To(BeNumerically(">", 0)) raw := buf.String() Expect(raw).To(ContainSubstring("f1")) Expect(raw).To(ContainSubstring("v1")) }) It("returns correct byte count", func() { var buf bytes.Buffer cmd := client.DoRawWriteTo(ctx, &buf, "PING") written, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(written).To(Equal(int64(buf.Len()))) }) It("implements Stringer", func() { var buf bytes.Buffer cmd := client.DoRawWriteTo(ctx, &buf, "PING") Expect(cmd.String()).To(ContainSubstring("PING")) }) It("can be cloned", func() { var buf bytes.Buffer cmd := client.DoRawWriteTo(ctx, &buf, "PING") clone := cmd.Clone().(*redis.RawWriteToCmd) Expect(clone.Val()).To(Equal(cmd.Val())) }) It("streams HGETALL with complete validation", func() { client.HSet(ctx, "rawhash2", "key1", "val1", "key2", "val2") var buf bytes.Buffer cmd := client.DoRawWriteTo(ctx, &buf, "HGETALL", "rawhash2") written, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) // Verify written bytes match buffer length Expect(written).To(Equal(int64(buf.Len()))) // Validate RESP structure rawBytes := buf.Bytes() respType, valid := parseRESPType(rawBytes) Expect(valid).To(BeTrue()) Expect(respType).To(BeElementOf(byte('*'), byte('%'))) // Verify all data present rawStr := buf.String() Expect(rawStr).To(ContainSubstring("key1")) Expect(rawStr).To(ContainSubstring("val1")) Expect(rawStr).To(ContainSubstring("key2")) Expect(rawStr).To(ContainSubstring("val2")) }) It("streams empty hash correctly", func() { var buf bytes.Buffer cmd := client.DoRawWriteTo(ctx, &buf, "HGETALL", "nonexistent_hash") written, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) Expect(written).To(Equal(int64(buf.Len()))) rawBytes := buf.Bytes() respType, valid := parseRESPType(rawBytes) Expect(valid).To(BeTrue()) Expect(respType).To(BeElementOf(byte('*'), byte('%'))) count, ok := countRESPElements(rawBytes) Expect(ok).To(BeTrue()) Expect(count).To(Equal(0)) }) It("streams error response correctly", func() { var buf bytes.Buffer cmd := client.DoRawWriteTo(ctx, &buf, "INVALIDCMD") _, err := cmd.Result() Expect(err).NotTo(HaveOccurred()) rawStr := buf.String() Expect(rawStr).To(HavePrefix("-")) Expect(rawStr).To(ContainSubstring("ERR")) }) }) var _ = Describe("NoRetry behavior", func() { It("RawCmd does not have NoRetry set", func() { cmd := redis.NewRawCmd(ctx, "PING") Expect(cmd.NoRetry()).To(BeFalse()) }) It("RawWriteToCmd has NoRetry set to prevent data corruption", func() { var buf bytes.Buffer cmd := redis.NewRawWriteToCmd(ctx, &buf, "PING") Expect(cmd.NoRetry()).To(BeTrue()) }) })