diff --git a/.gitignore b/.gitignore index 63b21b0b4..f1883206a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ testdata/* .DS_Store *.tar.gz *.dic +redis8tests.sh diff --git a/acl_commands.go b/acl_commands.go index 06847be2e..9cb800bb3 100644 --- a/acl_commands.go +++ b/acl_commands.go @@ -4,8 +4,20 @@ import "context" type ACLCmdable interface { ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd + ACLLog(ctx context.Context, count int64) *ACLLogCmd ACLLogReset(ctx context.Context) *StatusCmd + + ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd + ACLDelUser(ctx context.Context, username string) *IntCmd + ACLList(ctx context.Context) *StringSliceCmd + + ACLCat(ctx context.Context) *StringSliceCmd + ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd +} + +type ACLCatArgs struct { + Category string } func (c cmdable) ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd { @@ -33,3 +45,45 @@ func (c cmdable) ACLLogReset(ctx context.Context) *StatusCmd { _ = c(ctx, cmd) return cmd } + +func (c cmdable) ACLDelUser(ctx context.Context, username string) *IntCmd { + cmd := NewIntCmd(ctx, "acl", "deluser", username) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ACLSetUser(ctx context.Context, username string, rules ...string) *StatusCmd { + args := make([]interface{}, 3+len(rules)) + args[0] = "acl" + args[1] = "setuser" + args[2] = username + for i, rule := range rules { + args[i+3] = rule + } + cmd := NewStatusCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ACLList(ctx context.Context) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "acl", "list") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ACLCat(ctx context.Context) *StringSliceCmd { + cmd := NewStringSliceCmd(ctx, "acl", "cat") + _ = c(ctx, cmd) + return cmd +} + +func (c cmdable) ACLCatArgs(ctx context.Context, options *ACLCatArgs) *StringSliceCmd { + // if there is a category passed, build new cmd, if there isn't - use the ACLCat method + if options != nil && options.Category != "" { + cmd := NewStringSliceCmd(ctx, "acl", "cat", options.Category) + _ = c(ctx, cmd) + return cmd + } + + return c.ACLCat(ctx) +} diff --git a/acl_commands_test.go b/acl_commands_test.go new file mode 100644 index 000000000..846455831 --- /dev/null +++ b/acl_commands_test.go @@ -0,0 +1,449 @@ +package redis_test + +import ( + "context" + + "github.com/redis/go-redis/v9" + + . "github.com/bsm/ginkgo/v2" + . "github.com/bsm/gomega" +) + +var TestUserName string = "goredis" +var _ = Describe("ACL", func() { + var client *redis.Client + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + opt := redisOptions() + client = redis.NewClient(opt) + }) + + It("should ACL LOG", Label("NonRedisEnterprise"), func() { + Expect(client.ACLLogReset(ctx).Err()).NotTo(HaveOccurred()) + err := client.Do(ctx, "acl", "setuser", "test", ">test", "on", "allkeys", "+get").Err() + Expect(err).NotTo(HaveOccurred()) + + clientAcl := redis.NewClient(redisOptions()) + clientAcl.Options().Username = "test" + clientAcl.Options().Password = "test" + clientAcl.Options().DB = 0 + _ = clientAcl.Set(ctx, "mystring", "foo", 0).Err() + _ = clientAcl.HSet(ctx, "myhash", "foo", "bar").Err() + _ = clientAcl.SAdd(ctx, "myset", "foo", "bar").Err() + + logEntries, err := client.ACLLog(ctx, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(logEntries)).To(Equal(4)) + + for _, entry := range logEntries { + Expect(entry.Reason).To(Equal("command")) + Expect(entry.Context).To(Equal("toplevel")) + Expect(entry.Object).NotTo(BeEmpty()) + Expect(entry.Username).To(Equal("test")) + Expect(entry.AgeSeconds).To(BeNumerically(">=", 0)) + Expect(entry.ClientInfo).NotTo(BeNil()) + Expect(entry.EntryID).To(BeNumerically(">=", 0)) + Expect(entry.TimestampCreated).To(BeNumerically(">=", 0)) + Expect(entry.TimestampLastUpdated).To(BeNumerically(">=", 0)) + } + + limitedLogEntries, err := client.ACLLog(ctx, 2).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(limitedLogEntries)).To(Equal(2)) + + // cleanup after creating the user + err = client.Do(ctx, "acl", "deluser", "test").Err() + Expect(err).NotTo(HaveOccurred()) + }) + + It("should ACL LOG RESET", Label("NonRedisEnterprise"), func() { + // Call ACL LOG RESET + resetCmd := client.ACLLogReset(ctx) + Expect(resetCmd.Err()).NotTo(HaveOccurred()) + Expect(resetCmd.Val()).To(Equal("OK")) + + // Verify that the log is empty after the reset + logEntries, err := client.ACLLog(ctx, 10).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(logEntries)).To(Equal(0)) + }) + +}) +var _ = Describe("ACL user commands", Label("NonRedisEnterprise"), func() { + var client *redis.Client + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + opt := redisOptions() + client = redis.NewClient(opt) + }) + + AfterEach(func() { + _, err := client.ACLDelUser(context.Background(), TestUserName).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(client.Close()).NotTo(HaveOccurred()) + }) + + It("list only default user", func() { + res, err := client.ACLList(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(HaveLen(1)) + Expect(res[0]).To(ContainSubstring("default")) + }) + + It("setuser and deluser", func() { + res, err := client.ACLList(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(HaveLen(1)) + Expect(res[0]).To(ContainSubstring("default")) + + add, err := client.ACLSetUser(ctx, TestUserName, "nopass", "on", "allkeys", "+set", "+get").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + resAfter, err := client.ACLList(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resAfter).To(HaveLen(2)) + Expect(resAfter[1]).To(ContainSubstring(TestUserName)) + + deletedN, err := client.ACLDelUser(ctx, TestUserName).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(deletedN).To(BeNumerically("==", 1)) + + resAfterDeletion, err := client.ACLList(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resAfterDeletion).To(HaveLen(1)) + Expect(resAfterDeletion[0]).To(BeEquivalentTo(res[0])) + }) + + It("should acl dryrun", func() { + dryRun := client.ACLDryRun(ctx, "default", "get", "randomKey") + Expect(dryRun.Err()).NotTo(HaveOccurred()) + Expect(dryRun.Val()).To(Equal("OK")) + }) +}) + +var _ = Describe("ACL permissions", Label("NonRedisEnterprise"), func() { + var client *redis.Client + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + opt := redisOptions() + opt.UnstableResp3 = true + client = redis.NewClient(opt) + }) + + AfterEach(func() { + _, err := client.ACLDelUser(context.Background(), TestUserName).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(client.Close()).NotTo(HaveOccurred()) + }) + + It("reset permissions", func() { + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + connection := client.Conn() + authed, err := connection.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + _, err = connection.Get(ctx, "anykey").Result() + Expect(err).To(HaveOccurred()) + }) + + It("add write permissions", func() { + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+SET", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + connection := client.Conn() + authed, err := connection.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // can write + v, err := connection.Set(ctx, "anykey", "anyvalue", 0).Result() + Expect(err).ToNot(HaveOccurred()) + Expect(v).To(Equal("OK")) + + // but can't read + value, err := connection.Get(ctx, "anykey").Result() + Expect(err).To(HaveOccurred()) + Expect(value).To(BeEmpty()) + }) + + It("add read permissions", func() { + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+GET", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + connection := client.Conn() + authed, err := connection.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // can read + value, err := connection.Get(ctx, "anykey").Result() + Expect(err).ToNot(HaveOccurred()) + Expect(value).To(Equal("anyvalue")) + + // but can't delete + del, err := connection.Del(ctx, "anykey").Result() + Expect(err).To(HaveOccurred()) + Expect(del).ToNot(Equal(1)) + }) + + It("add del permissions", func() { + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+DEL", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + connection := client.Conn() + authed, err := connection.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // can read + del, err := connection.Del(ctx, "anykey").Result() + Expect(err).ToNot(HaveOccurred()) + Expect(del).To(BeEquivalentTo(1)) + }) + + It("set permissions for module commands", func() { + SkipBeforeRedisMajor(8, "permissions for modules are supported for Redis Version >=8") + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txt") + client.HSet(ctx, "doc1", "txt", "foo baz") + client.HSet(ctx, "doc2", "txt", "foo bar") + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+FT.SEARCH", + "-FT.DROPINDEX", + "+json.set", + "+json.get", + "-json.clear", + "+bf.reserve", + "-bf.info", + "+cf.reserve", + "+cms.initbydim", + "+topk.reserve", + "+tdigest.create", + "+ts.create", + "-ts.info", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + c := client.Conn() + authed, err := c.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // has perm for search + Expect(c.FTSearch(ctx, "txt", "foo ~bar").Err()).NotTo(HaveOccurred()) + + // no perm for dropindex + err = c.FTDropIndex(ctx, "txt").Err() + Expect(err).ToNot(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("NOPERM")) + + // json set and get have perm + Expect(c.JSONSet(ctx, "foo", "$", "\"bar\"").Err()).NotTo(HaveOccurred()) + Expect(c.JSONGet(ctx, "foo", "$").Val()).To(BeEquivalentTo("[\"bar\"]")) + + // no perm for json clear + err = c.JSONClear(ctx, "foo", "$").Err() + Expect(err).ToNot(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("NOPERM")) + + // perm for reserve + Expect(c.BFReserve(ctx, "bloom", 0.01, 100).Err()).NotTo(HaveOccurred()) + + // no perm for info + err = c.BFInfo(ctx, "bloom").Err() + Expect(err).ToNot(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("NOPERM")) + + // perm for cf.reserve + Expect(c.CFReserve(ctx, "cfres", 100).Err()).NotTo(HaveOccurred()) + // perm for cms.initbydim + Expect(c.CMSInitByDim(ctx, "cmsdim", 100, 5).Err()).NotTo(HaveOccurred()) + // perm for topk.reserve + Expect(c.TopKReserve(ctx, "topk", 10).Err()).NotTo(HaveOccurred()) + // perm for tdigest.create + Expect(c.TDigestCreate(ctx, "tdc").Err()).NotTo(HaveOccurred()) + // perm for ts.create + Expect(c.TSCreate(ctx, "tsts").Err()).NotTo(HaveOccurred()) + // noperm for ts.info + err = c.TSInfo(ctx, "tsts").Err() + Expect(err).ToNot(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("NOPERM")) + + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) + + It("set permissions for module categories", func() { + SkipBeforeRedisMajor(8, "permissions for modules are supported for Redis Version >=8") + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + val, err := client.FTCreate(ctx, "txt", &redis.FTCreateOptions{}, &redis.FieldSchema{FieldName: "txt", FieldType: redis.SearchFieldTypeText}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + WaitForIndexing(client, "txt") + client.HSet(ctx, "doc1", "txt", "foo baz") + client.HSet(ctx, "doc2", "txt", "foo bar") + add, err := client.ACLSetUser(ctx, + TestUserName, + "reset", + "nopass", + "on", + "~*", + "+@search", + "+@json", + "+@bloom", + "+@cuckoo", + "+@topk", + "+@cms", + "+@timeseries", + "+@tdigest", + ).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(add).To(Equal("OK")) + + c := client.Conn() + authed, err := c.AuthACL(ctx, TestUserName, "").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(authed).To(Equal("OK")) + + // has perm for search + Expect(c.FTSearch(ctx, "txt", "foo ~bar").Err()).NotTo(HaveOccurred()) + // perm for dropindex + Expect(c.FTDropIndex(ctx, "txt").Err()).NotTo(HaveOccurred()) + // json set and get have perm + Expect(c.JSONSet(ctx, "foo", "$", "\"bar\"").Err()).NotTo(HaveOccurred()) + Expect(c.JSONGet(ctx, "foo", "$").Val()).To(BeEquivalentTo("[\"bar\"]")) + // perm for json clear + Expect(c.JSONClear(ctx, "foo", "$").Err()).NotTo(HaveOccurred()) + // perm for reserve + Expect(c.BFReserve(ctx, "bloom", 0.01, 100).Err()).NotTo(HaveOccurred()) + // perm for info + Expect(c.BFInfo(ctx, "bloom").Err()).NotTo(HaveOccurred()) + // perm for cf.reserve + Expect(c.CFReserve(ctx, "cfres", 100).Err()).NotTo(HaveOccurred()) + // perm for cms.initbydim + Expect(c.CMSInitByDim(ctx, "cmsdim", 100, 5).Err()).NotTo(HaveOccurred()) + // perm for topk.reserve + Expect(c.TopKReserve(ctx, "topk", 10).Err()).NotTo(HaveOccurred()) + // perm for tdigest.create + Expect(c.TDigestCreate(ctx, "tdc").Err()).NotTo(HaveOccurred()) + // perm for ts.create + Expect(c.TSCreate(ctx, "tsts").Err()).NotTo(HaveOccurred()) + // perm for ts.info + Expect(c.TSInfo(ctx, "tsts").Err()).NotTo(HaveOccurred()) + + Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) +}) + +var _ = Describe("ACL Categories", func() { + var client *redis.Client + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + opt := redisOptions() + client = redis.NewClient(opt) + }) + + AfterEach(func() { + Expect(client.Close()).NotTo(HaveOccurred()) + }) + + It("lists acl categories and subcategories", func() { + res, err := client.ACLCat(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(res)).To(BeNumerically(">", 20)) + Expect(res).To(ContainElements( + "read", + "write", + "keyspace", + "dangerous", + "slow", + "set", + "sortedset", + "list", + "hash", + )) + + res, err = client.ACLCatArgs(ctx, &redis.ACLCatArgs{Category: "read"}).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(ContainElement("get")) + }) + + It("lists acl categories and subcategories with Modules", func() { + SkipBeforeRedisMajor(8, "modules are included in acl for redis version >= 8") + aclTestCase := map[string]string{ + "search": "FT.CREATE", + "bloom": "bf.add", + "json": "json.get", + "cuckoo": "cf.insert", + "cms": "cms.query", + "topk": "topk.list", + "tdigest": "tdigest.rank", + "timeseries": "ts.range", + } + var cats []interface{} + + for cat, subitem := range aclTestCase { + cats = append(cats, cat) + + res, err := client.ACLCatArgs(ctx, &redis.ACLCatArgs{ + Category: cat, + }).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(ContainElement(subitem)) + } + + res, err := client.ACLCat(ctx).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(ContainElements(cats...)) + }) +}) diff --git a/command.go b/command.go index f5aad9149..2623a2396 100644 --- a/command.go +++ b/command.go @@ -3862,30 +3862,48 @@ func (cmd *MapMapStringInterfaceCmd) Val() map[string]interface{} { return cmd.val } +// readReply will try to parse the reply from the proto.Reader for both resp2 and resp3 func (cmd *MapMapStringInterfaceCmd) readReply(rd *proto.Reader) (err error) { - n, err := rd.ReadArrayLen() + data, err := rd.ReadReply() if err != nil { return err } + resultMap := map[string]interface{}{} - data := make(map[string]interface{}, n/2) - for i := 0; i < n; i += 2 { - _, err := rd.ReadArrayLen() - if err != nil { - cmd.err = err - } - key, err := rd.ReadString() - if err != nil { - cmd.err = err + switch midResponse := data.(type) { + case map[interface{}]interface{}: // resp3 will return map + for k, v := range midResponse { + stringKey, ok := k.(string) + if !ok { + return fmt.Errorf("redis: invalid map key %#v", k) + } + resultMap[stringKey] = v } - value, err := rd.ReadString() - if err != nil { - cmd.err = err + case []interface{}: // resp2 will return array of arrays + n := len(midResponse) + for i := 0; i < n; i++ { + finalArr, ok := midResponse[i].([]interface{}) // final array that we need to transform to map + if !ok { + return fmt.Errorf("redis: unexpected response %#v", data) + } + m := len(finalArr) + if m%2 != 0 { // since this should be map, keys should be even number + return fmt.Errorf("redis: unexpected response %#v", data) + } + + for j := 0; j < m; j += 2 { + stringKey, ok := finalArr[j].(string) // the first one + if !ok { + return fmt.Errorf("redis: invalid map key %#v", finalArr[i]) + } + resultMap[stringKey] = finalArr[j+1] // second one is value + } } - data[key] = value + default: + return fmt.Errorf("redis: unexpected response %#v", data) } - cmd.val = data + cmd.val = resultMap return nil } diff --git a/commands_test.go b/commands_test.go index 901e96e35..404ffd02b 100644 --- a/commands_test.go +++ b/commands_test.go @@ -211,13 +211,13 @@ var _ = Describe("Commands", func() { select { case <-done: Fail("BLPOP is not blocked.") - case <-time.After(2 * time.Second): + case <-time.After(1 * time.Second): // ok } killed := client.ClientKillByFilter(ctx, "MAXAGE", "1") Expect(killed.Err()).NotTo(HaveOccurred()) - Expect(killed.Val()).To(SatisfyAny(Equal(int64(2)), Equal(int64(3)))) + Expect(killed.Val()).To(SatisfyAny(Equal(int64(2)), Equal(int64(3)), Equal(int64(4)))) select { case <-done: @@ -344,6 +344,23 @@ var _ = Describe("Commands", func() { Expect(val).NotTo(BeEmpty()) }) + It("should ConfigGet Modules", func() { + SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + expected := map[string]string{ + "search-*": "search-timeout", + "ts-*": "ts-retention-policy", + "bf-*": "bf-error-rate", + "cf-*": "cf-initial-size", + } + + for prefix, lookup := range expected { + val, err := client.ConfigGet(ctx, prefix).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).NotTo(BeEmpty()) + Expect(val[lookup]).NotTo(BeEmpty()) + } + }) + It("should ConfigResetStat", Label("NonRedisEnterprise"), func() { r := client.ConfigResetStat(ctx) Expect(r.Err()).NotTo(HaveOccurred()) @@ -362,6 +379,127 @@ var _ = Describe("Commands", func() { Expect(configSet.Val()).To(Equal("OK")) }) + It("should ConfigGet with Modules", Label("NonRedisEnterprise"), func() { + SkipBeforeRedisMajor(8, "config get won't return modules configs before redis 8") + configGet := client.ConfigGet(ctx, "*") + Expect(configGet.Err()).NotTo(HaveOccurred()) + Expect(configGet.Val()).To(HaveKey("maxmemory")) + Expect(configGet.Val()).To(HaveKey("search-timeout")) + Expect(configGet.Val()).To(HaveKey("ts-retention-policy")) + Expect(configGet.Val()).To(HaveKey("bf-error-rate")) + Expect(configGet.Val()).To(HaveKey("cf-initial-size")) + }) + + It("should ConfigSet FT DIALECT", func() { + SkipBeforeRedisMajor(8, "config doesn't include modules before Redis 8") + defaultState, err := client.ConfigGet(ctx, "search-default-dialect").Result() + Expect(err).NotTo(HaveOccurred()) + + // set to 3 + res, err := client.ConfigSet(ctx, "search-default-dialect", "3").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err := client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "3"})) + + resGet, err := client.ConfigGet(ctx, "search-default-dialect").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(BeEquivalentTo(map[string]string{"search-default-dialect": "3"})) + + // set to 2 + res, err = client.ConfigSet(ctx, "search-default-dialect", "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "2"})) + + // set to 1 + res, err = client.ConfigSet(ctx, "search-default-dialect", "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "1"})) + + resGet, err = client.ConfigGet(ctx, "search-default-dialect").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(resGet).To(BeEquivalentTo(map[string]string{"search-default-dialect": "1"})) + + // set to default + res, err = client.ConfigSet(ctx, "search-default-dialect", defaultState["search-default-dialect"]).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + }) + + It("should ConfigSet fail for ReadOnly", func() { + SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + _, err := client.ConfigSet(ctx, "search-max-doctablesize", "100000").Result() + Expect(err).To(HaveOccurred()) + }) + + It("should ConfigSet Modules", func() { + SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + defaults := map[string]string{} + expected := map[string]string{ + "search-timeout": "100", + "ts-retention-policy": "2", + "bf-error-rate": "0.13", + "cf-initial-size": "64", + } + + // read the defaults to set them back later + for setting, _ := range expected { + val, err := client.ConfigGet(ctx, setting).Result() + Expect(err).NotTo(HaveOccurred()) + defaults[setting] = val[setting] + } + + // check if new values can be set + for setting, value := range expected { + val, err := client.ConfigSet(ctx, setting, value).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).NotTo(BeEmpty()) + Expect(val).To(Equal("OK")) + } + + for setting, value := range expected { + val, err := client.ConfigGet(ctx, setting).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).NotTo(BeEmpty()) + Expect(val[setting]).To(Equal(value)) + } + + // set back to the defaults + for setting, value := range defaults { + val, err := client.ConfigSet(ctx, setting, value).Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).NotTo(BeEmpty()) + Expect(val).To(Equal("OK")) + } + }) + + It("should Fail ConfigSet Modules", func() { + SkipBeforeRedisMajor(8, "Config doesn't include modules before Redis 8") + expected := map[string]string{ + "search-timeout": "-100", + "ts-retention-policy": "-10", + "bf-error-rate": "1.5", + "cf-initial-size": "-10", + } + + for setting, value := range expected { + val, err := client.ConfigSet(ctx, setting, value).Result() + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring(setting))) + Expect(val).To(BeEmpty()) + } + }) + It("should ConfigRewrite", Label("NonRedisEnterprise"), func() { configRewrite := client.ConfigRewrite(ctx) Expect(configRewrite.Err()).NotTo(HaveOccurred()) @@ -2090,12 +2228,6 @@ var _ = Describe("Commands", func() { Expect(replace.Val()).To(Equal(int64(1))) }) - It("should acl dryrun", func() { - dryRun := client.ACLDryRun(ctx, "default", "get", "randomKey") - Expect(dryRun.Err()).NotTo(HaveOccurred()) - Expect(dryRun.Val()).To(Equal("OK")) - }) - It("should fail module loadex", Label("NonRedisEnterprise"), func() { dryRun := client.ModuleLoadex(ctx, &redis.ModuleLoadexConfig{ Path: "/path/to/non-existent-library.so", @@ -2143,51 +2275,6 @@ var _ = Describe("Commands", func() { Expect(args).To(Equal(expectedArgs)) }) - - It("should ACL LOG", Label("NonRedisEnterprise"), func() { - err := client.Do(ctx, "acl", "setuser", "test", ">test", "on", "allkeys", "+get").Err() - Expect(err).NotTo(HaveOccurred()) - - clientAcl := redis.NewClient(redisOptions()) - clientAcl.Options().Username = "test" - clientAcl.Options().Password = "test" - clientAcl.Options().DB = 0 - _ = clientAcl.Set(ctx, "mystring", "foo", 0).Err() - _ = clientAcl.HSet(ctx, "myhash", "foo", "bar").Err() - _ = clientAcl.SAdd(ctx, "myset", "foo", "bar").Err() - - logEntries, err := client.ACLLog(ctx, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(logEntries)).To(Equal(4)) - - for _, entry := range logEntries { - Expect(entry.Reason).To(Equal("command")) - Expect(entry.Context).To(Equal("toplevel")) - Expect(entry.Object).NotTo(BeEmpty()) - Expect(entry.Username).To(Equal("test")) - Expect(entry.AgeSeconds).To(BeNumerically(">=", 0)) - Expect(entry.ClientInfo).NotTo(BeNil()) - Expect(entry.EntryID).To(BeNumerically(">=", 0)) - Expect(entry.TimestampCreated).To(BeNumerically(">=", 0)) - Expect(entry.TimestampLastUpdated).To(BeNumerically(">=", 0)) - } - - limitedLogEntries, err := client.ACLLog(ctx, 2).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(limitedLogEntries)).To(Equal(2)) - }) - - It("should ACL LOG RESET", Label("NonRedisEnterprise"), func() { - // Call ACL LOG RESET - resetCmd := client.ACLLogReset(ctx) - Expect(resetCmd.Err()).NotTo(HaveOccurred()) - Expect(resetCmd.Val()).To(Equal("OK")) - - // Verify that the log is empty after the reset - logEntries, err := client.ACLLog(ctx, 10).Result() - Expect(err).NotTo(HaveOccurred()) - Expect(len(logEntries)).To(Equal(0)) - }) }) Describe("hashes", func() { diff --git a/doctests/geo_index_test.go b/doctests/geo_index_test.go new file mode 100644 index 000000000..9c38ba9d3 --- /dev/null +++ b/doctests/geo_index_test.go @@ -0,0 +1,206 @@ +// EXAMPLE: geoindex +// HIDE_START +package example_commands_test + +import ( + "context" + "fmt" + + "github.com/redis/go-redis/v9" +) + +// HIDE_END + +func ExampleClient_geoindex() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + Protocol: 2, + }) + // REMOVE_START + rdb.FTDropIndex(ctx, "productidx") + rdb.FTDropIndex(ctx, "geomidx") + rdb.Del(ctx, "product:46885", "product:46886", "shape:1", "shape:2", "shape:3", "shape:4") + // REMOVE_END + + // STEP_START create_geo_idx + geoCreateResult, err := rdb.FTCreate(ctx, + "productidx", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"product:"}, + }, + &redis.FieldSchema{ + FieldName: "$.location", + As: "location", + FieldType: redis.SearchFieldTypeGeo, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geoCreateResult) // >>> OK + // STEP_END + + // STEP_START add_geo_json + prd46885 := map[string]interface{}{ + "description": "Navy Blue Slippers", + "price": 45.99, + "city": "Denver", + "location": "-104.991531, 39.742043", + } + + gjResult1, err := rdb.JSONSet(ctx, "product:46885", "$", prd46885).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gjResult1) // >>> OK + + prd46886 := map[string]interface{}{ + "description": "Bright Green Socks", + "price": 25.50, + "city": "Fort Collins", + "location": "-105.0618814,40.5150098", + } + + gjResult2, err := rdb.JSONSet(ctx, "product:46886", "$", prd46886).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gjResult2) // >>> OK + // STEP_END + + // STEP_START geo_query + geoQueryResult, err := rdb.FTSearch(ctx, "productidx", + "@location:[-104.800644 38.846127 100 mi]", + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geoQueryResult) + // >>> {1 [{product:46885... + // STEP_END + + // STEP_START create_gshape_idx + geomCreateResult, err := rdb.FTCreate(ctx, "geomidx", + &redis.FTCreateOptions{ + OnJSON: true, + Prefix: []interface{}{"shape:"}, + }, + &redis.FieldSchema{ + FieldName: "$.name", + As: "name", + FieldType: redis.SearchFieldTypeText, + }, + &redis.FieldSchema{ + FieldName: "$.geom", + As: "geom", + FieldType: redis.SearchFieldTypeGeoShape, + GeoShapeFieldType: "FLAT", + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geomCreateResult) // >>> OK + // STEP_END + + // STEP_START add_gshape_json + shape1 := map[string]interface{}{ + "name": "Green Square", + "geom": "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))", + } + + gmjResult1, err := rdb.JSONSet(ctx, "shape:1", "$", shape1).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult1) // >>> OK + + shape2 := map[string]interface{}{ + "name": "Red Rectangle", + "geom": "POLYGON ((2 2.5, 2 3.5, 3.5 3.5, 3.5 2.5, 2 2.5))", + } + + gmjResult2, err := rdb.JSONSet(ctx, "shape:2", "$", shape2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult2) // >>> OK + + shape3 := map[string]interface{}{ + "name": "Blue Triangle", + "geom": "POLYGON ((3.5 1, 3.75 2, 4 1, 3.5 1))", + } + + gmjResult3, err := rdb.JSONSet(ctx, "shape:3", "$", shape3).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult3) // >>> OK + + shape4 := map[string]interface{}{ + "name": "Purple Point", + "geom": "POINT (2 2)", + } + + gmjResult4, err := rdb.JSONSet(ctx, "shape:4", "$", shape4).Result() + + if err != nil { + panic(err) + } + + fmt.Println(gmjResult4) // >>> OK + // STEP_END + + // STEP_START gshape_query + geomQueryResult, err := rdb.FTSearchWithArgs(ctx, "geomidx", + "(-@name:(Green Square) @geom:[WITHIN $qshape])", + &redis.FTSearchOptions{ + Params: map[string]interface{}{ + "qshape": "POLYGON ((1 1, 1 3, 3 3, 3 1, 1 1))", + }, + DialectVersion: 4, + Limit: 1, + }, + ).Result() + + if err != nil { + panic(err) + } + + fmt.Println(geomQueryResult) + // >>> {1 [{shape:4... + // STEP_END + + // Output: + // OK + // OK + // OK + // {1 [{product:46885 map[$:{"city":"Denver","description":"Navy Blue Slippers","location":"-104.991531, 39.742043","price":45.99}]}]} + // OK + // OK + // OK + // OK + // OK + // {1 [{shape:4 map[$:[{"geom":"POINT (2 2)","name":"Purple Point"}]]}]} +} diff --git a/doctests/list_tutorial_test.go b/doctests/list_tutorial_test.go index 908469ce0..bec1e1643 100644 --- a/doctests/list_tutorial_test.go +++ b/doctests/list_tutorial_test.go @@ -388,7 +388,7 @@ func ExampleClient_ltrim() { // REMOVE_END // STEP_START ltrim - res27, err := rdb.LPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result() + res27, err := rdb.RPush(ctx, "bikes:repairs", "bike:1", "bike:2", "bike:3", "bike:4", "bike:5").Result() if err != nil { panic(err) @@ -410,13 +410,13 @@ func ExampleClient_ltrim() { panic(err) } - fmt.Println(res29) // >>> [bike:5 bike:4 bike:3] + fmt.Println(res29) // >>> [bike:1 bike:2 bike:3] // STEP_END // Output: // 5 // OK - // [bike:5 bike:4 bike:3] + // [bike:1 bike:2 bike:3] } func ExampleClient_ltrim_end_of_list() { diff --git a/go.mod b/go.mod index c1d9037ac..1492d2709 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/bsm/ginkgo/v2 v2.12.0 github.com/bsm/gomega v1.27.10 - github.com/cespare/xxhash/v2 v2.2.0 + github.com/cespare/xxhash/v2 v2.3.0 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f ) diff --git a/go.sum b/go.sum index 21b4f64ee..4db68f6d4 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,7 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= diff --git a/hash_commands.go b/hash_commands.go index dcffdcdd9..6596c6f5f 100644 --- a/hash_commands.go +++ b/hash_commands.go @@ -225,7 +225,7 @@ func (c cmdable) HExpire(ctx context.Context, key string, expiration time.Durati return cmd } -// HExpire - Sets the expiration time for specified fields in a hash in seconds. +// HExpireWithArgs - Sets the expiration time for specified fields in a hash in seconds. // It requires a key, an expiration duration, a struct with boolean flags for conditional expiration settings (NX, XX, GT, LT), and a list of fields. // The command constructs an argument list starting with "HEXPIRE", followed by the key, duration, any conditional flags, and the specified fields. // For more information - https://redis.io/commands/hexpire/ diff --git a/main_test.go b/main_test.go index 6b3b563a0..a326960a0 100644 --- a/main_test.go +++ b/main_test.go @@ -73,7 +73,19 @@ var RCEDocker = false // Notes the major version of redis we are executing tests. // This can be used before we change the bsm fork of ginkgo for one, // which have support for label sets, so we can filter tests per redis major version. -var REDIS_MAJOR_VERSION = 7 +var RedisMajorVersion = 7 + +func SkipBeforeRedisMajor(version int, msg string) { + if RedisMajorVersion < version { + Skip(fmt.Sprintf("(redis major version < %d) %s", version, msg)) + } +} + +func SkipAfterRedisMajor(version int, msg string) { + if RedisMajorVersion > version { + Skip(fmt.Sprintf("(redis major version > %d) %s", version, msg)) + } +} func registerProcess(port string, p *redisProcess) { if processes == nil { @@ -92,16 +104,20 @@ var _ = BeforeSuite(func() { RECluster, _ = strconv.ParseBool(os.Getenv("RE_CLUSTER")) RCEDocker, _ = strconv.ParseBool(os.Getenv("RCE_DOCKER")) - REDIS_MAJOR_VERSION, _ = strconv.Atoi(os.Getenv("REDIS_MAJOR_VERSION")) - if REDIS_MAJOR_VERSION == 0 { - REDIS_MAJOR_VERSION = 7 + RedisMajorVersion, _ = strconv.Atoi(os.Getenv("REDIS_MAJOR_VERSION")) + + if RedisMajorVersion == 0 { + RedisMajorVersion = 7 } - Expect(REDIS_MAJOR_VERSION).To(BeNumerically(">=", 6)) - Expect(REDIS_MAJOR_VERSION).To(BeNumerically("<=", 8)) fmt.Printf("RECluster: %v\n", RECluster) fmt.Printf("RCEDocker: %v\n", RCEDocker) - fmt.Printf("REDIS_MAJOR_VERSION: %v\n", REDIS_MAJOR_VERSION) + fmt.Printf("REDIS_MAJOR_VERSION: %v\n", RedisMajorVersion) + + if RedisMajorVersion < 6 || RedisMajorVersion > 8 { + panic("incorrect or not supported redis major version") + } + if !RECluster && !RCEDocker { redisMain, err = startRedis(redisPort) diff --git a/options.go b/options.go index 8ba74ccd1..b9701702f 100644 --- a/options.go +++ b/options.go @@ -267,6 +267,7 @@ func NewDialer(opt *Options) func(context.Context, string, string) (net.Conn, er // URL attributes (scheme, host, userinfo, resp.), query parameters using these // names will be treated as unknown parameters // - unknown parameter names will result in an error +// - use "skip_verify=true" to ignore TLS certificate validation // // Examples: // @@ -487,6 +488,9 @@ func setupConnParams(u *url.URL, o *Options) (*Options, error) { if q.err != nil { return nil, q.err } + if o.TLSConfig != nil && q.has("skip_verify") { + o.TLSConfig.InsecureSkipVerify = q.bool("skip_verify") + } // any parameters left? if r := q.remaining(); len(r) > 0 { diff --git a/options_test.go b/options_test.go index 1db36fdb4..d46ecc858 100644 --- a/options_test.go +++ b/options_test.go @@ -30,6 +30,9 @@ func TestParseURL(t *testing.T) { }, { url: "rediss://localhost:123", o: &Options{Addr: "localhost:123", TLSConfig: &tls.Config{ /* no deep comparison */ }}, + }, { + url: "rediss://localhost:123/?skip_verify=true", + o: &Options{Addr: "localhost:123", TLSConfig: &tls.Config{InsecureSkipVerify: true}}, }, { url: "redis://:bar@localhost:123", o: &Options{Addr: "localhost:123", Password: "bar"}, diff --git a/osscluster.go b/osscluster.go index 72e922a80..188f50359 100644 --- a/osscluster.go +++ b/osscluster.go @@ -487,9 +487,11 @@ func (c *clusterNodes) Addrs() ([]string, error) { closed := c.closed //nolint:ifshort if !closed { if len(c.activeAddrs) > 0 { - addrs = c.activeAddrs + addrs = make([]string, len(c.activeAddrs)) + copy(addrs, c.activeAddrs) } else { - addrs = c.addrs + addrs = make([]string, len(c.addrs)) + copy(addrs, c.addrs) } } c.mu.RUnlock() diff --git a/search_commands.go b/search_commands.go index 9e5928017..1312a78f0 100644 --- a/search_commands.go +++ b/search_commands.go @@ -831,20 +831,32 @@ func (c cmdable) FTAlter(ctx context.Context, index string, skipInitialScan bool return cmd } -// FTConfigGet - Retrieves the value of a RediSearch configuration parameter. +// Retrieves the value of a RediSearch configuration parameter. // The 'option' parameter specifies the configuration parameter to retrieve. -// For more information, please refer to the Redis documentation: -// [FT.CONFIG GET]: (https://redis.io/commands/ft.config-get/) +// For more information, please refer to the Redis [FT.CONFIG GET] documentation. +// +// Deprecated: FTConfigGet is deprecated in Redis 8. +// All configuration will be done with the CONFIG GET command. +// For more information check [Client.ConfigGet] and [CONFIG GET Documentation] +// +// [CONFIG GET Documentation]: https://redis.io/commands/config-get/ +// [FT.CONFIG GET]: https://redis.io/commands/ft.config-get/ func (c cmdable) FTConfigGet(ctx context.Context, option string) *MapMapStringInterfaceCmd { cmd := NewMapMapStringInterfaceCmd(ctx, "FT.CONFIG", "GET", option) _ = c(ctx, cmd) return cmd } -// FTConfigSet - Sets the value of a RediSearch configuration parameter. +// Sets the value of a RediSearch configuration parameter. // The 'option' parameter specifies the configuration parameter to set, and the 'value' parameter specifies the new value. -// For more information, please refer to the Redis documentation: -// [FT.CONFIG SET]: (https://redis.io/commands/ft.config-set/) +// For more information, please refer to the Redis [FT.CONFIG SET] documentation. +// +// Deprecated: FTConfigSet is deprecated in Redis 8. +// All configuration will be done with the CONFIG SET command. +// For more information check [Client.ConfigSet] and [CONFIG SET Documentation] +// +// [CONFIG SET Documentation]: https://redis.io/commands/config-set/ +// [FT.CONFIG SET]: https://redis.io/commands/ft.config-set/ func (c cmdable) FTConfigSet(ctx context.Context, option string, value interface{}) *StatusCmd { cmd := NewStatusCmd(ctx, "FT.CONFIG", "SET", option, value) _ = c(ctx, cmd) diff --git a/search_test.go b/search_test.go index a48f45bf0..a409fc78a 100644 --- a/search_test.go +++ b/search_test.go @@ -136,7 +136,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { Expect(err).NotTo(HaveOccurred()) Expect(val).To(BeEquivalentTo("OK")) WaitForIndexing(client, "txt") - client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch impements a search engine on top of redis") + client.HSet(ctx, "doc1", "title", "RediSearch", "body", "Redisearch implements a search engine on top of redis") res1, err := client.FTSearchWithArgs(ctx, "txt", "search engine", &redis.FTSearchOptions{NoContent: true, Verbatim: true, LimitOffset: 0, Limit: 5}).Result() Expect(err).NotTo(HaveOccurred()) Expect(res1.Total).To(BeEquivalentTo(int64(1))) @@ -374,9 +374,8 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { // up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25 // this test expect redis major version >= 8 It("should FTSearch WithScores", Label("search", "ftsearch"), func() { - if REDIS_MAJOR_VERSION < 8 { - Skip("(redis major version < 8) default scorer is not BM25") - } + SkipBeforeRedisMajor(8, "default scorer is not BM25") + text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() Expect(err).NotTo(HaveOccurred()) @@ -418,9 +417,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { // up until redis 8 the default scorer was TFIDF, in redis 8 it is BM25 // this test expect redis major version <=7 It("should FTSearch WithScores", Label("search", "ftsearch"), func() { - if REDIS_MAJOR_VERSION > 7 { - Skip("(redis major version > 7) default scorer is not TFIDF") - } + SkipAfterRedisMajor(7, "default scorer is not TFIDF") text1 := &redis.FieldSchema{FieldName: "description", FieldType: redis.SearchFieldTypeText} val, err := client.FTCreate(ctx, "idx1", &redis.FTCreateOptions{}, text1).Result() Expect(err).NotTo(HaveOccurred()) @@ -485,7 +482,7 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { WaitForIndexing(client, "idx1") client.HSet(ctx, "search", "title", "RediSearch", - "body", "Redisearch impements a search engine on top of redis", + "body", "Redisearch implements a search engine on top of redis", "parent", "redis", "random_num", 10) client.HSet(ctx, "ai", "title", "RedisAI", @@ -1015,6 +1012,24 @@ var _ = Describe("RediSearch commands Resp 2", Label("search"), func() { }) + It("should FTConfigGet return multiple fields", Label("search", "NonRedisEnterprise"), func() { + res, err := client.FTConfigSet(ctx, "DEFAULT_DIALECT", "1").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err := client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "1"})) + + res, err = client.FTConfigSet(ctx, "DEFAULT_DIALECT", "2").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res).To(BeEquivalentTo("OK")) + + defDialect, err = client.FTConfigGet(ctx, "DEFAULT_DIALECT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(defDialect).To(BeEquivalentTo(map[string]interface{}{"DEFAULT_DIALECT": "2"})) + }) + It("should FTConfigSet and FTConfigGet dialect", Label("search", "ftconfigget", "ftconfigset", "NonRedisEnterprise"), func() { res, err := client.FTConfigSet(ctx, "DEFAULT_DIALECT", "1").Result() Expect(err).NotTo(HaveOccurred()) @@ -1471,6 +1486,46 @@ func _assert_geosearch_result(result *redis.FTSearchResult, expectedDocIDs []str // Expect(results0["extra_attributes"].(map[interface{}]interface{})["__v_score"]).To(BeEquivalentTo("0")) // }) +var _ = Describe("RediSearch FT.Config with Resp2 and Resp3", Label("search", "NonRedisEnterprise"), func() { + + var clientResp2 *redis.Client + var clientResp3 *redis.Client + BeforeEach(func() { + clientResp2 = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 2}) + clientResp3 = redis.NewClient(&redis.Options{Addr: ":6379", Protocol: 3, UnstableResp3: true}) + Expect(clientResp3.FlushDB(ctx).Err()).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + Expect(clientResp2.Close()).NotTo(HaveOccurred()) + Expect(clientResp3.Close()).NotTo(HaveOccurred()) + }) + + It("should FTConfigSet and FTConfigGet ", Label("search", "ftconfigget", "ftconfigset", "NonRedisEnterprise"), func() { + val, err := clientResp3.FTConfigSet(ctx, "TIMEOUT", "100").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(BeEquivalentTo("OK")) + + res2, err := clientResp2.FTConfigGet(ctx, "TIMEOUT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res2).To(BeEquivalentTo(map[string]interface{}{"TIMEOUT": "100"})) + + res3, err := clientResp3.FTConfigGet(ctx, "TIMEOUT").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(res3).To(BeEquivalentTo(map[string]interface{}{"TIMEOUT": "100"})) + }) + + It("should FTConfigGet all resp2 and resp3", Label("search", "NonRedisEnterprise"), func() { + res2, err := clientResp2.FTConfigGet(ctx, "*").Result() + Expect(err).NotTo(HaveOccurred()) + + res3, err := clientResp3.FTConfigGet(ctx, "*").Result() + Expect(err).NotTo(HaveOccurred()) + Expect(len(res3)).To(BeEquivalentTo(len(res2))) + Expect(res2["DEFAULT_DIALECT"]).To(BeEquivalentTo(res2["DEFAULT_DIALECT"])) + }) +}) + var _ = Describe("RediSearch commands Resp 3", Label("search"), func() { ctx := context.TODO() var client *redis.Client