fix(cmd): accept nil HGetEXOptions/HSetEXOptions (#4214)

HGetEXWithArgs and HSetEXWithArgs panicked with a nil pointer
dereference when the options were nil. Treat nil like empty options,
as SDiffCard and SUnionCard do, so the commands match HGetEX and
HSetEX.

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>
This commit is contained in:
Yingzi ZhangandNedyalko Dyakov authored and GitHub committed 2026-10-27 15:22:27 +02:00
1 parent 470a926a08
commit 7f1c98cdf7
2 files changed
+97

No files matched your search

+6
View File
@@ -549,6 +549,9 @@ type HGetEXOptions struct {
}
func (c cmdable) HGetEXWithArgs(ctx context.Context, key string, options *HGetEXOptions, fields ...string) *StringSliceCmd {
if options == nil {
options = &HGetEXOptions{}
}
args := []interface{}{"HGETEX", key}
if options.ExpirationType != "" {
args = append(args, string(options.ExpirationType))
@@ -602,6 +605,9 @@ func (c cmdable) HSetEX(ctx context.Context, key string, fieldsAndValues ...stri
}
func (c cmdable) HSetEXWithArgs(ctx context.Context, key string, options *HSetEXOptions, fieldsAndValues ...string) *IntCmd {
if options == nil {
options = &HSetEXOptions{}
}
args := []interface{}{"HSETEX", key}
if options.Condition != "" {
args = append(args, string(options.Condition))
+91
View File
@@ -0,0 +1,91 @@
package redis
import (
"context"
"reflect"
"testing"
)
// TestHGetEXWithArgsArgs checks that HGetEXWithArgs builds HGETEX with and
// without options. Nil options send the same command as HGetEX.
func TestHGetEXWithArgsArgs(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
options *HGetEXOptions
want []interface{}
}{
{
name: "nil_options",
want: []interface{}{"HGETEX", "key", "FIELDS", 2, "f1", "f2"},
},
{
name: "empty_options",
options: &HGetEXOptions{},
want: []interface{}{"HGETEX", "key", "FIELDS", 2, "f1", "f2"},
},
{
name: "ex",
options: &HGetEXOptions{ExpirationType: HGetEXExpirationEX, ExpirationVal: 60},
want: []interface{}{"HGETEX", "key", "EX", int64(60), "FIELDS", 2, "f1", "f2"},
},
{
name: "persist",
options: &HGetEXOptions{ExpirationType: HGetEXExpirationPERSIST},
want: []interface{}{"HGETEX", "key", "PERSIST", "FIELDS", 2, "f1", "f2"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var captured Cmder
cmd := captureCmdable(&captured).HGetEXWithArgs(ctx, "key", tt.options, "f1", "f2")
if !reflect.DeepEqual(cmd.Args(), tt.want) {
t.Errorf("args mismatch\n got: %#v\nwant: %#v", cmd.Args(), tt.want)
}
})
}
}
// TestHSetEXWithArgsArgs checks that HSetEXWithArgs builds HSETEX with and
// without options. Nil options send the same command as HSetEX.
func TestHSetEXWithArgsArgs(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
options *HSetEXOptions
want []interface{}
}{
{
name: "nil_options",
want: []interface{}{"HSETEX", "key", "FIELDS", 1, "f1", "v1"},
},
{
name: "empty_options",
options: &HSetEXOptions{},
want: []interface{}{"HSETEX", "key", "FIELDS", 1, "f1", "v1"},
},
{
name: "fnx_ex",
options: &HSetEXOptions{Condition: HSetEXFNX, ExpirationType: HSetEXExpirationEX, ExpirationVal: 60},
want: []interface{}{"HSETEX", "key", "FNX", "EX", int64(60), "FIELDS", 1, "f1", "v1"},
},
{
name: "keepttl",
options: &HSetEXOptions{ExpirationType: HSetEXExpirationKEEPTTL},
want: []interface{}{"HSETEX", "key", "KEEPTTL", "FIELDS", 1, "f1", "v1"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var captured Cmder
cmd := captureCmdable(&captured).HSetEXWithArgs(ctx, "key", tt.options, "f1", "v1")
if !reflect.DeepEqual(cmd.Args(), tt.want) {
t.Errorf("args mismatch\n got: %#v\nwant: %#v", cmd.Args(), tt.want)
}
})
}
}