From 1303ac97fc3473a5220e8c02ac4305f89b69aaea Mon Sep 17 00:00:00 2001 From: oleiade Date: Tue, 12 Jul 2022 18:24:28 +0200 Subject: [PATCH] Cover the module's API with integration tests Those tests use the previously introduced redis stub server to send request to a fac simile server, and ensure that the exposed JS API behaves as intended. We are less intrested in actual redis behavior correctness than in verifying that the module sends proper commands to redis, and reacts as expected to somewhat expected responses. --- client.go | 178 ++-- client_test.go | 2312 ++++++++++++++++++++++++++++++++++++++++++++++-- go.mod | 3 - go.sum | 7 - 4 files changed, 2337 insertions(+), 163 deletions(-) diff --git a/client.go b/client.go index ac140b7..43b8511 100644 --- a/client.go +++ b/client.go @@ -20,11 +20,13 @@ type Client struct { // Set the given key with the given value. // +// If the provided value is not a supported type, the promise is rejected with an error. +// // The value for `expiration` is interpreted as seconds. func (c *Client) Set(key string, value interface{}, expiration int) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -48,10 +50,14 @@ func (c *Client) Set(key string, value interface{}, expiration int) *goja.Promis } // Get returns the value for the given key. +// +// If the key does not exist, the promise is rejected with an error. +// +// If the key does not exist, the promise is rejected with an error. func (c *Client) Get(key string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -70,10 +76,12 @@ func (c *Client) Get(key string) *goja.Promise { } // GetSet sets the value of key to value and returns the old value stored +// +// If the provided value is not a supported type, the promise is rejected with an error. func (c *Client) GetSet(key string, value interface{}) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -100,7 +108,7 @@ func (c *Client) GetSet(key string, value interface{}) *goja.Promise { func (c *Client) Del(keys ...string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -119,10 +127,12 @@ func (c *Client) Del(keys ...string) *goja.Promise { } // GetDel gets the value of key and deletes the key. +// +// If the key does not exist, the promise is rejected with an error. func (c *Client) GetDel(key string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -146,7 +156,7 @@ func (c *Client) GetDel(key string) *goja.Promise { func (c *Client) Exists(keys ...string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -171,7 +181,7 @@ func (c *Client) Exists(keys ...string) *goja.Promise { func (c *Client) Incr(key string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -196,7 +206,7 @@ func (c *Client) Incr(key string) *goja.Promise { func (c *Client) IncrBy(key string, increment int64) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -221,7 +231,7 @@ func (c *Client) IncrBy(key string, increment int64) *goja.Promise { func (c *Client) Decr(key string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -246,7 +256,7 @@ func (c *Client) Decr(key string) *goja.Promise { func (c *Client) DecrBy(key string, decrement int64) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -265,10 +275,12 @@ func (c *Client) DecrBy(key string, decrement int64) *goja.Promise { } // RandomKey returns a random key. +// +// If the database is empty, the promise is rejected with an error. func (c *Client) RandomKey() *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -290,7 +302,7 @@ func (c *Client) RandomKey() *goja.Promise { func (c *Client) Mget(keys ...string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -315,7 +327,7 @@ func (c *Client) Mget(keys ...string) *goja.Promise { func (c *Client) Expire(key string, seconds int) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -334,11 +346,11 @@ func (c *Client) Expire(key string, seconds int) *goja.Promise { } // Ttl returns the remaining time to live of a key that has a timeout. -// nolint:stylecheck,revive +//nolint:revive,stylecheck func (c *Client) Ttl(key string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -360,7 +372,7 @@ func (c *Client) Ttl(key string) *goja.Promise { func (c *Client) Persist(key string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -385,7 +397,7 @@ func (c *Client) Persist(key string) *goja.Promise { func (c *Client) Lpush(key string, values ...interface{}) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -414,7 +426,7 @@ func (c *Client) Lpush(key string, values ...interface{}) *goja.Promise { func (c *Client) Rpush(key string, values ...interface{}) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -438,10 +450,13 @@ func (c *Client) Rpush(key string, values ...interface{}) *goja.Promise { } // Lpop removes and returns the first element of the list stored at `key`. +// +// If the list does not exist, this command rejects the promise with an error. func (c *Client) Lpop(key string) *goja.Promise { + // TODO: redis supports indicating the amount of values to pop promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -460,10 +475,13 @@ func (c *Client) Lpop(key string) *goja.Promise { } // Rpop removes and returns the last element of the list stored at `key`. +// +// If the list does not exist, this command rejects the promise with an error. func (c *Client) Rpop(key string) *goja.Promise { + // TODO: redis supports indicating the amount of values to pop promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -488,7 +506,7 @@ func (c *Client) Rpop(key string) *goja.Promise { func (c *Client) Lrange(key string, start, stop int64) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -509,10 +527,12 @@ func (c *Client) Lrange(key string, start, stop int64) *goja.Promise { // Lindex returns the specified element of the list stored at `key`. // The index is zero-based. Negative indices can be used to designate // elements starting at the tail of the list. +// +// If the list does not exist, this command rejects the promise with an error. func (c *Client) Lindex(key string, index int64) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -531,10 +551,12 @@ func (c *Client) Lindex(key string, index int64) *goja.Promise { } // Lset sets the list element at `index` to `element`. +// +// If the list does not exist, this command rejects the promise with an error. func (c *Client) Lset(key string, index int64, element string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -556,10 +578,12 @@ func (c *Client) Lset(key string, index int64, element string) *goja.Promise { // at `key`. If `count` is positive, elements are removed from the beginning of the list. // If `count` is negative, elements are removed from the end of the list. // If `count` is zero, all elements matching `value` are removed. +// +// If the list does not exist, this command rejects the promise with an error. func (c *Client) Lrem(key string, count int64, value string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -579,10 +603,12 @@ func (c *Client) Lrem(key string, count int64, value string) *goja.Promise { // Llen returns the length of the list stored at `key`. If `key` // does not exist, it is interpreted as an empty list and 0 is returned. +// +// If the list does not exist, this command rejects the promise with an error. func (c *Client) Llen(key string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -603,10 +629,12 @@ func (c *Client) Llen(key string) *goja.Promise { // Hset sets the specified field in the hash stored at `key` to `value`. // If the `key` does not exist, a new key holding a hash is created. // If `field` already exists in the hash, it is overwritten. +// +// If the hash does not exist, this command rejects the promise with an error. func (c *Client) Hset(key string, field string, value interface{}) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -636,7 +664,7 @@ func (c *Client) Hset(key string, field string, value interface{}) *goja.Promise func (c *Client) Hsetnx(key, field, value string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -655,10 +683,12 @@ func (c *Client) Hsetnx(key, field, value string) *goja.Promise { } // Hget returns the value associated with `field` in the hash stored at `key`. +// +// If the hash does not exist, this command rejects the promise with an error. func (c *Client) Hget(key, field string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -680,7 +710,7 @@ func (c *Client) Hget(key, field string) *goja.Promise { func (c *Client) Hdel(key string, fields ...string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -699,10 +729,12 @@ func (c *Client) Hdel(key string, fields ...string) *goja.Promise { } // Hgetall returns all fields and values of the hash stored at `key`. +// +// If the hash does not exist, this command rejects the promise with an error. func (c *Client) Hgetall(key string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -721,10 +753,12 @@ func (c *Client) Hgetall(key string) *goja.Promise { } // Hkeys returns all fields of the hash stored at `key`. +// +// If the hash does not exist, this command rejects the promise with an error. func (c *Client) Hkeys(key string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -743,10 +777,12 @@ func (c *Client) Hkeys(key string) *goja.Promise { } // Hvals returns all values of the hash stored at `key`. +// +// If the hash does not exist, this command rejects the promise with an error. func (c *Client) Hvals(key string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -765,10 +801,12 @@ func (c *Client) Hvals(key string) *goja.Promise { } // Hlen returns the number of fields in the hash stored at `key`. +// +// If the hash does not exist, this command rejects the promise with an error. func (c *Client) Hlen(key string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -793,7 +831,7 @@ func (c *Client) Hlen(key string) *goja.Promise { func (c *Client) Hincrby(key, field string, increment int64) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -817,7 +855,7 @@ func (c *Client) Hincrby(key, field string, increment int64) *goja.Promise { func (c *Client) Sadd(key string, members ...interface{}) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -846,7 +884,7 @@ func (c *Client) Sadd(key string, members ...interface{}) *goja.Promise { func (c *Client) Srem(key string, members ...interface{}) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -873,7 +911,7 @@ func (c *Client) Srem(key string, members ...interface{}) *goja.Promise { func (c *Client) Sismember(key string, member interface{}) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -900,7 +938,7 @@ func (c *Client) Sismember(key string, member interface{}) *goja.Promise { func (c *Client) Smembers(key string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -919,10 +957,12 @@ func (c *Client) Smembers(key string) *goja.Promise { } // Srandmember returns a random element from the set value stored at key. +// +// If the set does not exist, the promise is rejected with an error. func (c *Client) Srandmember(key string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -941,10 +981,12 @@ func (c *Client) Srandmember(key string) *goja.Promise { } // Spop removes and returns a random element from the set value stored at key. +// +// If the set does not exist, the promise is rejected with an error. func (c *Client) Spop(key string) *goja.Promise { promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -970,7 +1012,7 @@ func (c *Client) SendCommand(command string, args ...interface{}) *goja.Promise promise, resolve, reject := c.makeHandledPromise() - if err := c.Connect(); err != nil { + if err := c.connect(); err != nil { reject(err) return promise } @@ -1016,9 +1058,9 @@ func (c *Client) makeHandledPromise() (*goja.Promise, func(interface{}), func(in } } -// Connect establishes the client's connection to the target +// connect establishes the client's connection to the target // redis instance(s). -func (c *Client) Connect() error { +func (c *Client) connect() error { // A nil VU state indicates we are in the init context. // As a general convention, k6 should not perform IO in the // init context. Thus, the Connect method will error if @@ -1030,43 +1072,23 @@ func (c *Client) Connect() error { // If the redisClient is already instantiated, it is safe // to assume that the connection is already established. - if c.redisClient == nil { - // If k6 has a TLSConfig set in its state, use - // it has redis' client TLSConfig too. - if vuState.TLSConfig != nil { - c.redisOptions.TLSConfig = vuState.TLSConfig - } - - // use k6's lib.DialerContexter function has redis' - // client Dialer - c.redisOptions.Dialer = vuState.Dialer.DialContext - - c.redisClient = redis.NewUniversalClient(c.redisOptions) + if c.redisClient != nil { + return nil } - return nil -} - -// Close closes the client's connection to the target redis instance(s). -func (c *Client) Close() error { - // A nil VU state indicates we are in the init context. - // As a general convention, k6 should not perform IO in the - // init context. As the Close method is symmetric to the Connect - // method, it will error if called in the init context; even though - // calling doesn't effectively perform any IO. - if c.vu.State() == nil { - return common.NewInitContextError("closing a redis connection in the init context is not supported") + // If k6 has a TLSConfig set in its state, use + // it has redis' client TLSConfig too. + if vuState.TLSConfig != nil { + c.redisOptions.TLSConfig = vuState.TLSConfig } - // The redisClient attribute is set when redis' client dialer, - // TLSConfig and options are set: allowing the redis client to - // communicate with the outside. Setting it to nil will cause - // the Client to not be able to communicate with the outside. - if c.redisClient != nil { - err := c.redisClient.Close() - c.redisClient = nil - return err - } + // use k6's lib.DialerContexter function has redis' + // client Dialer + c.redisOptions.Dialer = vuState.Dialer.DialContext + + // Replace the internal redis client instance with a new + // one using our custom options. + c.redisClient = redis.NewUniversalClient(c.redisOptions) return nil } @@ -1095,7 +1117,9 @@ func (c *Client) isSupportedType(offset int, args ...interface{}) error { case string, int, int64, float64, bool: continue default: - return fmt.Errorf("unsupported type: %T for argument at index: %d", arg, idx+offset) + return fmt.Errorf( + "unsupported type provided for argument at index %d, "+ + "supported types are string, number, and boolean", idx+offset) } } diff --git a/client_test.go b/client_test.go index a4445cc..488972e 100644 --- a/client_test.go +++ b/client_test.go @@ -2,10 +2,11 @@ package redis import ( "context" + "errors" "fmt" + "strconv" "testing" - "github.com/alicebob/miniredis/v2" "github.com/dop251/goja" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -18,140 +19,2292 @@ import ( "gopkg.in/guregu/null.v3" ) -func TestClientConnect(t *testing.T) { +func TestClientSet(t *testing.T) { t.Parallel() - t.Run("connecting in the init context throws", func(t *testing.T) { - t.Parallel() + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("SET", func(c *Connection, args []string) { + if len(args) <= 2 && len(args) > 4 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'GET' command")) + return + } + + switch args[0] { + case "existing_key", "non_existing_key": //nolint:goconst + c.WriteOK() + case "expires": + if len(args) != 4 && args[2] != "EX" && args[3] != "0" { + c.WriteError(errors.New("ERR unexpected number of arguments for 'SET' command")) + } + c.WriteOK() + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.set("existing_key", "new_value") + .then(res => { if (res !== "OK") { throw 'unexpected value for set result: ' + res } }) + .then(() => redis.set("non_existing_key", "some_value")) + .then(res => { if (res !== "OK") { throw 'unexpected value for set result: ' + res } }) + .then(() => redis.set("expires", "expired", 10)) + .then(res => { if (res !== "OK") { throw 'unexpected value for set result: ' + res } }) + .then(() => redis.set("unsupported_type", new Array("unsupported"))) + .then( + res => { throw 'expected to fail setting unsupported type' }, + err => { if (!err.error().startsWith('unsupported type')) { throw 'unexpected error: ' + err } } + ) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 3, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"SET", "existing_key", "new_value"}, + {"SET", "non_existing_key", "some_value"}, + {"SET", "expires", "expired", "ex", "10"}, + }, rs.GotCommands()) +} + +func TestClientGet(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("GET", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'GET' command")) + return + } + + switch args[0] { + case "existing_key": + c.WriteBulkString("old_value") + case "non_existing_key": + c.WriteNull() + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.get("existing_key") + .then(res => { if (res !== "old_value") { throw 'unexpected value for get result: ' + res } }) + .then(() => redis.get("non_existing_key")) + .then( + res => { throw 'expected to fail getting non-existing key from redis' }, + err => { if (err.error() != 'redis: nil') { throw 'unexpected error: ' + err } } + ) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"GET", "existing_key"}, + {"GET", "non_existing_key"}, + }, rs.GotCommands()) +} + +func TestClientGetSet(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("GETSET", func(c *Connection, args []string) { + if len(args) != 2 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'GETSET' command")) + return + } + + switch args[0] { + case "existing_key": + c.WriteBulkString("old_value") + case "non_existing_key": + c.WriteOK() + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.getSet("existing_key", "new_value") + .then(res => { if (res !== "old_value") { throw 'unexpected value for getSet result: ' + res } }) + .then(() => redis.getSet("non_existing_key", "some_value")) + .then(res => { if (res !== "OK") { throw 'unexpected value for getSet result: ' + res } }) + .then(() => redis.getSet("unsupported_type", new Array("unsupported"))) + .then( + res => { throw 'unexpectedly resolve getset unsupported type' }, + err => { if (!err.error().startsWith('unsupported type')) { throw 'unexpected error: ' + err } } + ) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"GETSET", "existing_key", "new_value"}, + {"GETSET", "non_existing_key", "some_value"}, + }, rs.GotCommands()) +} + +func TestClientDel(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("DEL", func(c *Connection, args []string) { + if len(args) != 3 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'DEL' command")) + return + } + + c.WriteInteger(2) + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.del("key1", "key2", "nonexisting_key") + .then(res => { if (res !== 2) { throw 'unexpected value for del result: ' + res } }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 1, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"DEL", "key1", "key2", "nonexisting_key"}, + }, rs.GotCommands()) +} + +func TestClientGetDel(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("GETDEL", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'GETDEL' command")) + return + } + + switch args[0] { + case "existing_key": + c.WriteBulkString("old_value") + case "non_existing_key": + c.WriteNull() + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.getDel("existing_key") + .then(res => { if (res !== "old_value") { throw 'unexpected value for getDel result: ' + res } }) + .then(() => redis.getDel("non_existing_key")) + .then( + res => { if (res !== null) { throw 'unexpected value for getSet result: ' + res } }, + err => { if (err.error() != 'redis: nil') { throw 'unexpected error: ' + err } } + ) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"GETDEL", "existing_key"}, + {"GETDEL", "non_existing_key"}, + }, rs.GotCommands()) +} + +func TestClientExists(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("EXISTS", func(c *Connection, args []string) { + if len(args) == 0 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'EXISTS' command")) + return + } + + c.WriteInteger(1) + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.exists("existing_key", "nonexisting_key") + .then(res => { if (res !== 1) { throw 'unexpected value for exists result: ' + res } }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 1, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"EXISTS", "existing_key", "nonexisting_key"}, + }, rs.GotCommands()) +} + +func TestClientIncr(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("INCR", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'INCR' command")) + return + } + + existingValue := 10 + + switch args[0] { + case "existing_key": + c.WriteInteger(existingValue + 1) + case "non_existing_key": + c.WriteInteger(0 + 1) + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.incr("existing_key") + .then(res => { if (res !== 11) { throw 'unexpected value for existing key incr result: ' + res } }) + .then(() => redis.incr("non_existing_key")) + .then(res => { if (res !== 1) { throw 'unexpected value for non existing key incr result: ' + res } }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"INCR", "existing_key"}, + {"INCR", "non_existing_key"}, + }, rs.GotCommands()) +} + +func TestClientIncrBy(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("INCRBY", func(c *Connection, args []string) { + if len(args) != 2 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'INCRBY' command")) + return + } + + value, err := strconv.Atoi(args[1]) + if err != nil { + c.WriteError(err) + return + } + + existingValue := 10 + + switch args[0] { + case "existing_key": + c.WriteInteger(existingValue + value) + case "non_existing_key": + c.WriteInteger(0 + value) + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.incrBy("existing_key", 10) + .then(res => { if (res !== 20) { throw 'unexpected value for incrBy result: ' + res } }) + .then(() => redis.incrBy("non_existing_key", 10)) + .then(res => { if (res !== 10) { throw 'unexpected value for incrBy result: ' + res } }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"INCRBY", "existing_key", "10"}, + {"INCRBY", "non_existing_key", "10"}, + }, rs.GotCommands()) +} + +func TestClientDecr(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("DECR", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'DECR' command")) + return + } + + existingValue := 10 + + switch args[0] { + case "existing_key": + c.WriteInteger(existingValue - 1) + case "non_existing_key": + c.WriteInteger(0 - 1) + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.decr("existing_key") + .then(res => { if (res !== 9) { throw 'unexpected value for decr result: ' + res } }) + .then(() => redis.decr("non_existing_key")) + .then(res => { if (res !== -1) { throw 'unexpected value for decr result: ' + res } }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"DECR", "existing_key"}, + {"DECR", "non_existing_key"}, + }, rs.GotCommands()) +} + +func TestClientDecrBy(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("DECRBY", func(c *Connection, args []string) { + if len(args) != 2 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'DECRBY' command")) + return + } + + value, err := strconv.Atoi(args[1]) + if err != nil { + c.WriteError(err) + return + } + + existingValue := 10 + + switch args[0] { + case "existing_key": + c.WriteInteger(existingValue - value) + case "non_existing_key": + c.WriteInteger(0 - value) + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.decrBy("existing_key", 2) + .then(res => { if (res !== 8) { throw 'unexpected value for decrBy result: ' + res } }) + .then(() => redis.decrBy("non_existing_key", 2)) + .then(res => { if (res !== -2) { throw 'unexpected value for decrBy result: ' + res } }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"DECRBY", "existing_key", "2"}, + {"DECRBY", "non_existing_key", "2"}, + }, rs.GotCommands()) +} + +func TestClientRandomKey(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + calledN := 0 + rs.RegisterCommandHandler("RANDOMKEY", func(c *Connection, args []string) { + if len(args) != 0 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'RANDOMKEY' command")) + return + } + + if calledN == 0 { + // let's consider the DB empty + calledN++ + c.WriteNull() + return + } + + c.WriteBulkString("random_key") + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.randomKey() + .then( + res => { throw 'unexpectedly resolved promise for randomKey command: ' + res }, + err => { if (err.error() != 'redis: nil') { throw 'unexpected error: ' + err } } + ) + .then(() => redis.randomKey()) + .then(res => { if (res !== "random_key") { throw 'unexpected value for randomKey result: ' + res } }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"RANDOMKEY"}, + {"RANDOMKEY"}, + }, rs.GotCommands()) +} + +func TestClientMget(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("MGET", func(c *Connection, args []string) { + if len(args) < 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'MGET' command")) + return + } + + c.WriteArray("old_value", "") + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.mget("existing_key", "non_existing_key") + .then( + res => { + if (res.length !== 2 || res[0] !== "old_value" || res[1] !== null) { + throw 'unexpected value for mget result: ' + res + } + } + ) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 1, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"MGET", "existing_key", "non_existing_key"}, + }, rs.GotCommands()) +} + +func TestClientExpire(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("EXPIRE", func(c *Connection, args []string) { + if len(args) != 2 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'EXPIRE' command")) + return + } + + switch args[0] { + case "expires_key": + c.WriteInteger(1) + case "non_existing_key": + c.WriteInteger(0) + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.expire("expires_key", 10) + .then(res => { if (res !== true) { throw 'unexpected value for expire result: ' + res } }) + .then(() => redis.expire("non_existing_key", 1)) + .then(res => { if (res !== false) { throw 'unexpected value for expire result: ' + res } }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"EXPIRE", "expires_key", "10"}, + {"EXPIRE", "non_existing_key", "1"}, + }, rs.GotCommands()) +} + +func TestClientTTL(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("TTL", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'EXPIRE' command")) + return + } + + switch args[0] { + case "expires_key": + c.WriteInteger(10) + case "non_existing_key": + c.WriteInteger(0) + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.ttl("expires_key") + .then(res => { if (res !== 10) { throw 'unexpected value for expire result: ' + res } }) + .then(() => redis.ttl("non_existing_key")) + .then(res => { if (res > 0) { throw 'unexpected value for expire result: ' + res } }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"TTL", "expires_key"}, + {"TTL", "non_existing_key"}, + }, rs.GotCommands()) +} + +func TestClientPersist(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("PERSIST", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'PERSIST' command")) + return + } + + switch args[0] { + case "expires_key": + c.WriteInteger(1) + case "non_existing_key": + c.WriteInteger(0) + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.persist("expires_key") + .then(res => { if (res !== true) { throw 'unexpected value for expire result: ' + res } }) + .then(() => redis.persist("non_existing_key")) + .then(res => { if (res !== false) { throw 'unexpected value for expire result: ' + res } }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"PERSIST", "expires_key"}, + {"PERSIST", "non_existing_key"}, + }, rs.GotCommands()) +} + +func TestClientLPush(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("LPUSH", func(c *Connection, args []string) { + if len(args) < 2 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'LPUSH' command")) + return + } + + existingList := []string{"existing_key"} + + switch args[0] { + case "existing_list": //nolint:goconst + existingList = append(args[1:], existingList...) + c.WriteInteger(len(existingList)) + case "new_list": + c.WriteInteger(1) + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.lpush("existing_list", "second", "first") + .then(res => { if (res !== 3) { throw 'unexpected value for lpush result: ' + res } }) + .then(() => redis.lpush("new_list", 1)) + .then(res => { if (res !== 1) { throw 'unexpected value for lpush result: ' + res } }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"LPUSH", "existing_list", "second", "first"}, + {"LPUSH", "new_list", "1"}, + }, rs.GotCommands()) +} + +func TestClientRPush(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("RPUSH", func(c *Connection, args []string) { + if len(args) < 2 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'RPUSH' command")) + return + } + + existingList := []string{"existing_key"} + + switch args[0] { + case "existing_list": + existingList = append(existingList, args[1:]...) + c.WriteInteger(len(existingList)) + case "new_list": + c.WriteInteger(1) + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.rpush("existing_list", "second", "third") + .then(res => { if (res !== 3) { throw 'unexpected value for rpush result: ' + res } }) + .then(() => redis.rpush("new_list", 1)) + .then(res => { if (res !== 1) { throw 'unexpected value for rpush result: ' + res } }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"RPUSH", "existing_list", "second", "third"}, + {"RPUSH", "new_list", "1"}, + }, rs.GotCommands()) +} + +func TestClientLPop(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + listState := []string{"first", "second"} + rs.RegisterCommandHandler("LPOP", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'LPOP' command")) + return + } + + switch args[0] { + case "existing_list": + c.WriteBulkString(listState[0]) + listState = listState[1:] + case "non_existing_list": //nolint:goconst + c.WriteNull() + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.lpop("existing_list") + .then(res => { if (res !== "first") { throw 'unexpected value for lpop first result: ' + res } }) + .then(() => redis.lpop("existing_list")) + .then(res => { if (res !== "second") { throw 'unexpected value for lpop second result: ' + res } }) + .then(() => redis.lpop("non_existing_list")) + .then( + res => { if (res !== null) { throw 'unexpectedly resolved lpop promise: ' + res } }, + + // An error is returned if the list does not exist + err => { if (err.error() != 'redis: nil') { throw 'unexpected error for lpop: ' + err.error() } } + ) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 3, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"LPOP", "existing_list"}, + {"LPOP", "existing_list"}, + {"LPOP", "non_existing_list"}, + }, rs.GotCommands()) +} + +func TestClientRPop(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + listState := []string{"first", "second"} + rs.RegisterCommandHandler("RPOP", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'RPOP' command")) + return + } + + switch args[0] { + case "existing_list": + c.WriteBulkString(listState[len(listState)-1]) + listState = listState[:len(listState)-1] + case "non_existing_list": + c.WriteNull() + } + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.rpop("existing_list") + .then(res => { if (res !== "second") { throw 'unexpected value for rpop result: ' + res }}) + .then(() => redis.rpop("existing_list")) + .then(res => { if (res !== "first") { throw 'unexpected value for rpop result: ' + res }}) + .then(() => redis.rpop("non_existing_list")) + .then( + res => { if (res !== null) { throw 'unexpectedly resolved lpop promise: ' + res } }, + + // An error is returned if the list does not exist + err => { if (err.error() != 'redis: nil') { throw 'unexpected error for rpop: ' + err.error() } } + ) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 3, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"RPOP", "existing_list"}, + {"RPOP", "existing_list"}, + {"RPOP", "non_existing_list"}, + }, rs.GotCommands()) +} + +func TestClientLRange(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + listState := []string{"first", "second", "third"} + rs.RegisterCommandHandler("LRANGE", func(c *Connection, args []string) { + if len(args) != 3 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'LRANGE' command")) + return + } + + start, err := strconv.Atoi(args[1]) + if err != nil { + c.WriteError(err) + return + } + + stop, err := strconv.Atoi(args[2]) + if err != nil { + c.WriteError(err) + return + } + + if start < 0 { + start = len(listState) + start + } + + // This calculation is done in a way that is not 100% correct, but it is + // good enough for the test. + c.WriteArray(listState[start : stop+1]...) + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.lrange("existing_list", 0, 0) + .then(res => { if (res.length !== 1 || res[0] !== "first") { throw 'unexpected value for lrange result: ' + res }}) + .then(() => redis.lrange("existing_list", 0, 1)) + .then(res => { if (res.length !== 2 || res[0] !== "first" || res[1] !== "second") { throw 'unexpected value for lrange result: ' + res } }) + .then(() => redis.lrange("existing_list", -2, 2)) + .then(res => { + if (res.length !== 2 || + res[0] !== "second" || + res[1] !== "third") { + throw 'unexpected value for lrange result: ' + res + } + }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 3, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"LRANGE", "existing_list", "0", "0"}, + {"LRANGE", "existing_list", "0", "1"}, + {"LRANGE", "existing_list", "-2", "2"}, + }, rs.GotCommands()) +} + +func TestClientLIndex(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + listState := []string{"first", "second", "third"} + rs.RegisterCommandHandler("LINDEX", func(c *Connection, args []string) { + if len(args) != 2 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'LINDEX' command")) + return + } + + if args[0] == "non_existing_list" { + c.WriteNull() + return + } + + index, err := strconv.Atoi(args[1]) + if err != nil { + c.WriteError(err) + return + } + + if index > len(listState)-1 { + c.WriteNull() + return + } + + // This calculation is done in a way that is not 100% correct, but it is + // good enough for the test. + c.WriteBulkString(listState[index]) + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.lindex("existing_list", 0) + .then(res => { if (res !== "first") { throw 'unexpected value for lindex result: ' + res } }) + .then(() => redis.lindex("existing_list", 3)) + .then( + res => { throw 'unexpectedly resolved lindex command promise: ' + res }, + err => { if (err.error() != 'redis: nil') { throw 'unexpected error for lindex: ' + err.error() } } + ) + .then(() => redis.lindex("non_existing_list", 0)) + .then( + res => { throw 'unexpectedly resolved lindex command promise: ' + res }, + err => { if (err.error() != 'redis: nil') { throw 'unexpected error for lindex: ' + err.error() } } + ) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 3, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"LINDEX", "existing_list", "0"}, + {"LINDEX", "existing_list", "3"}, + {"LINDEX", "non_existing_list", "0"}, + }, rs.GotCommands()) +} + +func TestClientClientLSet(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + listState := []string{"first"} + rs.RegisterCommandHandler("LSET", func(c *Connection, args []string) { + if len(args) != 3 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'LSET' command")) + return + } + + if args[0] == "non_existing_list" { + c.WriteError(errors.New("ERR no such key")) + return + } + + index, err := strconv.Atoi(args[1]) + if err != nil { + c.WriteError(err) + return + } + + listState[index] = args[2] + c.WriteOK() + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.lset("existing_list", 0, "new_first") + .then(res => { if (res !== "OK") { throw 'unexpected value for lset result: ' + res }}) + .then(() => redis.lset("existing_list", 0, "overridden_value")) + .then(() => redis.lset("non_existing_list", 0, "new_first")) + .then( + res => { if (res !== null) { throw 'unexpectedly resolved promise: ' + res } }, + err => { if (err.error() != 'ERR no such key') { throw 'unexpected error for lset: ' + err.error() } } + ) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 3, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"LSET", "existing_list", "0", "new_first"}, + {"LSET", "existing_list", "0", "overridden_value"}, + {"LSET", "non_existing_list", "0", "new_first"}, + }, rs.GotCommands()) +} + +func TestClientLrem(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("LREM", func(c *Connection, args []string) { + if len(args) != 3 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'LREM' command")) + return + } + + if args[0] == "non_existing_list" { + c.WriteError(errors.New("ERR no such key")) + return + } + + c.WriteInteger(1) + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.lrem("existing_list", 1, "first") + .then(() => redis.lrem("existing_list", 0, "second")) + .then(() => { + redis.lrem("non_existing_list", 2, "third") + .then( + res => { if (res !== null) { throw 'unexpectedly resolved promise: ' + res } }, + err => { if (err.error() != 'ERR no such key') { throw 'unexpected error for lrem: ' + err.error() } }, + ) + }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 3, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"LREM", "existing_list", "1", "first"}, + {"LREM", "existing_list", "0", "second"}, + {"LREM", "non_existing_list", "2", "third"}, + }, rs.GotCommands()) +} + +func TestClientLlen(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("LLEN", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'LREM' command")) + return + } + + if args[0] == "non_existing_list" { + c.WriteError(errors.New("ERR no such key")) + return + } + + c.WriteInteger(3) + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.llen("existing_list") + .then(res => { if (res !== 3) { throw 'unexpected value for llen result: ' + res } }) + .then(() => { + redis.llen("non_existing_list") + .then( + res => { if (res !== null) { throw 'unexpectedly resolved promise: ' + res } }, + err => { if (err.error() != 'ERR no such key') { throw 'unexpected error for llen: ' + err.error() } } + ) + }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"LLEN", "existing_list"}, + {"LLEN", "non_existing_list"}, + }, rs.GotCommands()) +} + +func TestClientHSet(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("HSET", func(c *Connection, args []string) { + if len(args) != 3 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'LREM' command")) + return + } + + if args[0] == "non_existing_hash" { //nolint:goconst + c.WriteError(errors.New("ERR no such key")) + return + } + + c.WriteInteger(1) + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.hset("existing_hash", "key", "value") + .then(res => { if (res !== 1) { throw 'unexpected value for hset result: ' + res } }) + .then(() => redis.hset("existing_hash", "fou", "barre")) + .then(res => { if (res !== 1) { throw 'unexpected value for hset result: ' + res } }) + .then(() => redis.hset("non_existing_hash", "cle", "valeur")) + .then( + res => { if (res !== null) { throw 'unexpectedly resolved promise: ' + res } }, + err => { if (err.error() != 'ERR no such key') { throw 'unexpected error for hset: ' + err.error() } }, + ) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 3, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"HSET", "existing_hash", "key", "value"}, + {"HSET", "existing_hash", "fou", "barre"}, + {"HSET", "non_existing_hash", "cle", "valeur"}, + }, rs.GotCommands()) +} + +func TestClientHsetnx(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("HSETNX", func(c *Connection, args []string) { + if len(args) != 3 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'HSETNX' command")) + return + } + + if args[0] == "non_existing_hash" { + c.WriteInteger(1) // HSET on a non existing hash creates it + return + } + + // key does not exist + if args[1] == "key" { + c.WriteInteger(1) + return + } + + // key already exists + c.WriteInteger(0) + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.hsetnx("existing_hash", "key", "value") + .then(res => { if (res !== true) { throw 'unexpected value for hsetnx result: ' + res } }) + .then(() => redis.hsetnx("existing_hash", "foo", "barre")) + .then(res => { if (res !== false) { throw 'unexpected value for hsetnx result: ' + res } }) + .then(() => redis.hsetnx("non_existing_hash", "key", "value")) + .then(res => { if (res !== true) { throw 'unexpected value for hsetnx result: ' + res } }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 3, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"HSETNX", "existing_hash", "key", "value"}, + {"HSETNX", "existing_hash", "foo", "barre"}, + {"HSETNX", "non_existing_hash", "key", "value"}, + }, rs.GotCommands()) +} + +func TestClientHget(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("HGET", func(c *Connection, args []string) { + if len(args) != 2 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'HGET' command")) + return + } + + if args[0] == "non_existing_hash" { + c.WriteNull() + return + } + + c.WriteBulkString("bar") + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.hget("existing_hash", "foo") + .then(res => { if (res !== "bar") { throw 'unexpected value for hget result: ' + res } }) + .then(() => redis.hget("non_existing_hash", "key")) + .then( + res => { throw 'unexpectedly resolved hget promise : ' + res }, + err => { if (err.error() != 'redis: nil') { throw 'unexpected error for hget: ' + err.error() } }, + ) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"HGET", "existing_hash", "foo"}, + {"HGET", "non_existing_hash", "key"}, + }, rs.GotCommands()) +} + +func TestClientHdel(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("HDEL", func(c *Connection, args []string) { + if len(args) != 2 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'HDEL' command")) + return + } + + if args[0] == "non_existing_hash" || args[1] == "non_existing_key" { + c.WriteInteger(0) + return + } + + c.WriteInteger(1) + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.hdel("existing_hash", "foo") + .then(res => { if (res !== 1) { throw 'unexpected value for hdel result: ' + res } }) + .then(() => redis.hdel("existing_hash", "non_existing_key")) + .then(res => { if (res !== 0) { throw 'unexpected value for hdel result: ' + res } }) + .then(() => redis.hdel("non_existing_hash", "key")) + .then(res => { if (res !== 0) { throw 'unexpected value for hdel result: ' + res } }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 3, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"HDEL", "existing_hash", "foo"}, + {"HDEL", "existing_hash", "non_existing_key"}, + {"HDEL", "non_existing_hash", "key"}, + }, rs.GotCommands()) +} + +func TestClientHgetall(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("HGETALL", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'HGETALL' command")) + return + } + + if args[0] == "non_existing_hash" { + c.WriteArray() + return + } + + c.WriteArray("foo", "bar") + }) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.hgetall("existing_hash") + .then(res => { if (typeof res !== "object" || res['foo'] !== 'bar') { throw 'unexpected value for hgetall result: ' + res } }) + .then(() => redis.hgetall("non_existing_hash")) + .then( + res => { if (Object.keys(res).length !== 0) { throw 'unexpected value for hgetall result: ' + res} }, + err => { if (err.error() != 'redis: nil') { throw 'unexpected error for hgetall: ' + err.error() } }, + ) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"HGETALL", "existing_hash"}, + {"HGETALL", "non_existing_hash"}, + }, rs.GotCommands()) +} + +func TestClientHkeys(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("HKEYS", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'HKEYS' command")) + return + } + + if args[0] == "non_existing_hash" { + c.WriteArray() + return + } - ts := newInitContextTestSetup(t) // setup to execute code in the init context + c.WriteArray("foo") + }) - gotScriptErr := ts.ev.Start(func() error { - _, err := ts.rt.RunString(fmt.Sprintf(` + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` const redis = new Client({ addrs: new Array("%s"), }); - redis.connect(); - `, ts.redis.Addr())) + redis.hkeys("existing_hash") + .then(res => { if (res.length !== 1 || res[0] !== 'foo') { throw 'unexpected value for hkeys result: ' + res } }) + .then(() => redis.hkeys("non_existing_hash")) + .then( + res => { if (res.length !== 0) { throw 'unexpected value for hkeys result: ' + res} }, + err => { if (err.error() != 'redis: nil') { throw 'unexpected error for hkeys: ' + err.error() } }, + ) + `, rs.Addr())) - return err - }) + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"HKEYS", "existing_hash"}, + {"HKEYS", "non_existing_hash"}, + }, rs.GotCommands()) +} + +func TestClientHvals(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("HVALS", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'HVALS' command")) + return + } + + if args[0] == "non_existing_hash" { + c.WriteArray() + return + } - assert.Error(t, gotScriptErr) - assert.Contains(t, gotScriptErr.Error(), "connecting to a redis server in the init context is not supported") + c.WriteArray("bar") }) - t.Run("connecting in the main context works", func(t *testing.T) { - t.Parallel() + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.hvals("existing_hash") + .then(res => { if (res.length !== 1 || res[0] !== 'bar') { throw 'unexpected value for hvals result: ' + res } }) + .then(() => redis.hvals("non_existing_hash")) + .then( + res => { if (res.length !== 0) { throw 'unexpected value for hvals result: ' + res} }, + err => { if (err.error() != 'redis: nil') { throw 'unexpected error for hvals: ' + err.error() } }, + ) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"HVALS", "existing_hash"}, + {"HVALS", "non_existing_hash"}, + }, rs.GotCommands()) +} + +func TestClientHlen(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("HLEN", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'HLEN' command")) + return + } + + if args[0] == "non_existing_hash" { + c.WriteInteger(0) + return + } - ts := newTestSetup(t) // Setup to execute code in the main context + c.WriteInteger(1) + }) - gotScriptErr := ts.ev.Start(func() error { - _, err := ts.rt.RunString(fmt.Sprintf(` + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` const redis = new Client({ addrs: new Array("%s"), }); - redis.connect(); - `, ts.redis.Addr())) + redis.hlen("existing_hash") + .then(res => { if (res !== 1) { throw 'unexpected value for hlen result: ' + res } }) + .then(() => redis.hlen("non_existing_hash")) + .then( + res => { if (res !== 0) { throw 'unexpected value for hlen result: ' + res} }, + err => { if (err.error() != 'redis: nil') { throw 'unexpected error for hlen: ' + err.error() } }, + ) + `, rs.Addr())) - return err - }) + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"HLEN", "existing_hash"}, + {"HLEN", "non_existing_hash"}, + }, rs.GotCommands()) +} + +func TestClientHincrby(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + fooHValue := 1 + rs.RegisterCommandHandler("HINCRBY", func(c *Connection, args []string) { + if len(args) != 3 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'HINCRBY' command")) + return + } + + if args[0] == "non_existing_hash" { + c.WriteInteger(1) + return + } + + value, err := strconv.Atoi(args[2]) + if err != nil { + c.WriteError(err) + return + } + + fooHValue += value + + c.WriteInteger(fooHValue) + }) - assert.NoError(t, gotScriptErr) + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.hincrby("existing_hash", "foo", 1) + .then(res => { if (res !== 2) { throw 'unexpected value for hincrby result: ' + res } }) + .then(() => redis.hincrby("existing_hash", "foo", -1)) + .then(res => { if (res !== 1) { throw 'unexpected value for hincrby result: ' + res } }) + .then(() => redis.hincrby("non_existing_hash", "foo", 1)) + .then(res => { if (res !== 1) { throw 'unexpected value for hincrby result: ' + res } }) + `, rs.Addr())) + + return err }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 3, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"HINCRBY", "existing_hash", "foo", "1"}, + {"HINCRBY", "existing_hash", "foo", "-1"}, + {"HINCRBY", "non_existing_hash", "foo", "1"}, + }, rs.GotCommands()) } -func TestClientClose(t *testing.T) { +func TestClientSadd(t *testing.T) { t.Parallel() - t.Run("closing in the init context throws", func(t *testing.T) { - t.Parallel() + ts := newTestSetup(t) + rs := RunT(t) + barWasSet := false + rs.RegisterCommandHandler("SADD", func(c *Connection, args []string) { + if len(args) != 2 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'SADD' command")) + return + } - ts := newInitContextTestSetup(t) // setup to execute code in the init context + if args[0] == "non_existing_set" { //nolint:goconst + c.WriteInteger(1) + return + } + + if barWasSet == false { + barWasSet = true + c.WriteInteger(1) + return + } + + c.WriteInteger(0) + }) - gotScriptErr := ts.ev.Start(func() error { - _, err := ts.rt.RunString(fmt.Sprintf(` + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` const redis = new Client({ addrs: new Array("%s"), }); - redis.close(); - `, ts.redis.Addr())) + redis.sadd("existing_set", "bar") + .then(res => { if (res !== 1) { throw 'unexpected value for sadd result: ' + res } }) + .then(() => redis.sadd("existing_set", "bar")) + .then(res => { if (res !== 0) { throw 'unexpected value for sadd result: ' + res } }) + .then(() => redis.sadd("non_existing_set", "foo")) + .then(res => { if (res !== 1) { throw 'unexpected value for sadd result: ' + res} }) + `, rs.Addr())) - return err - }) + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 3, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"SADD", "existing_set", "bar"}, + {"SADD", "existing_set", "bar"}, + {"SADD", "non_existing_set", "foo"}, + }, rs.GotCommands()) +} + +func TestClientSrem(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + fooWasRemoved := false + rs.RegisterCommandHandler("SREM", func(c *Connection, args []string) { + if len(args) != 2 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'SREM' command")) + return + } + + if args[0] == "non_existing_set" { + c.WriteInteger(0) + return + } + + if fooWasRemoved == false { + fooWasRemoved = true + c.WriteInteger(1) + return + } - assert.Error(t, gotScriptErr) - assert.Contains(t, gotScriptErr.Error(), "closing a redis connection in the init context is not supported") + c.WriteInteger(0) }) - t.Run("closing a connected client in the main context", func(t *testing.T) { - t.Parallel() + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.srem("existing_set", "foo") + .then(res => { if (res !== 1) { throw 'unexpected value for srem result: ' + res } }) + .then(() => redis.srem("existing_set", "foo")) + .then(res => { if (res !== 0) { throw 'unexpected value for srem result: ' + res } }) + .then(() => redis.srem("existing_set", "doesnotexist")) + .then(res => { if (res !== 0) { throw 'unexpected value for srem result: ' + res } }) + .then(() => redis.srem("non_existing_set", "foo")) + .then(res => { if (res !== 0) { throw 'unexpected value for srem result: ' + res} }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 4, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"SREM", "existing_set", "foo"}, + {"SREM", "existing_set", "foo"}, + {"SREM", "existing_set", "doesnotexist"}, + {"SREM", "non_existing_set", "foo"}, + }, rs.GotCommands()) +} + +func TestClientSismember(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("SISMEMBER", func(c *Connection, args []string) { + if len(args) != 2 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'SISMEMBER' command")) + return + } + + if args[0] == "non_existing_set" { + c.WriteInteger(0) + return + } + + if args[1] == "foo" { + c.WriteInteger(1) + return + } - ts := newTestSetup(t) // Setup to execute code in the main context + c.WriteInteger(0) + }) - gotScriptErr := ts.ev.Start(func() error { - _, err := ts.rt.RunString(fmt.Sprintf(` + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` const redis = new Client({ addrs: new Array("%s"), }); - redis.connect(); - redis.close(); + redis.sismember("existing_set", "foo") + .then(res => { if (res !== true) { throw 'unexpected value for sismember result: ' + res } }) + .then(() => redis.sismember("existing_set", "bar")) + .then(res => { if (res !== false) { throw 'unexpected value for sismember result: ' + res } }) + .then(() => redis.sismember("non_existing_set", "foo")) + .then(res => { if (res !== false) { throw 'unexpected value for sismember result: ' + res} }) + `, rs.Addr())) - if (redis.isConnected() === true) { - throw new Error("redis client is still connected"); - } - `, ts.redis.Addr())) + return err + }) - return err - }) + assert.NoError(t, gotScriptErr) + assert.Equal(t, 3, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"SISMEMBER", "existing_set", "foo"}, + {"SISMEMBER", "existing_set", "bar"}, + {"SISMEMBER", "non_existing_set", "foo"}, + }, rs.GotCommands()) +} + +func TestClientSmembers(t *testing.T) { + t.Parallel() - assert.NoError(t, gotScriptErr) + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("SMEMBERS", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'SMEMBERS' command")) + return + } + + if args[0] == "non_existing_set" { + c.WriteArray() + return + } + + c.WriteArray("foo", "bar") }) - t.Run("closing an already closed client ", func(t *testing.T) { - t.Parallel() + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.smembers("existing_set") + .then(res => { if (res.length !== 2 || 'foo' in res || 'bar' in res) { throw 'unexpected value for smembers result: ' + res } }) + .then(() => redis.smembers("non_existing_set")) + .then(res => { if (res.length !== 0) { throw 'unexpected value for smembers result: ' + res} }) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"SMEMBERS", "existing_set"}, + {"SMEMBERS", "non_existing_set"}, + }, rs.GotCommands()) +} + +func TestClientSrandmember(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("SRANDMEMBER", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'SRANDMEMBER' command")) + return + } + + if args[0] == "non_existing_set" { + c.WriteError(errors.New("ERR no elements in set")) + return + } - ts := newTestSetup(t) // Setup to execute code in the main context + c.WriteBulkString("foo") + }) - gotScriptErr := ts.ev.Start(func() error { - _, err := ts.rt.RunString(fmt.Sprintf(` + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` const redis = new Client({ addrs: new Array("%s"), }); - redis.close(); - `, ts.redis.Addr())) + redis.srandmember("existing_set") + .then(res => { if (res !== 'foo' && res !== 'bar') { throw 'unexpected value for srandmember result: ' + res} }) + .then(() => redis.srandmember("non_existing_set")) + .then( + res => { throw 'unexpectedly resolved promise for srandmember result: ' + res }, + err => { if (err.error() !== 'ERR no elements in set') { throw 'unexpected error for srandmember operation: ' + err.error() } } + ) + `, rs.Addr())) - return err - }) + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"SRANDMEMBER", "existing_set"}, + {"SRANDMEMBER", "non_existing_set"}, + }, rs.GotCommands()) +} + +func TestClientSpop(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + rs.RegisterCommandHandler("SPOP", func(c *Connection, args []string) { + if len(args) != 1 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'SPOP' command")) + return + } + + if args[0] == "non_existing_set" { + c.WriteError(errors.New("ERR no elements in set")) + return + } - assert.NoError(t, gotScriptErr) + c.WriteBulkString("foo") }) - t.Run("double closing a client ", func(t *testing.T) { - t.Parallel() + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("%s"), + }); + + redis.spop("existing_set") + .then(res => { if (res !== 'foo' && res !== 'bar') { throw 'unexpected value for spop result: ' + res} }) + .then(() => redis.spop("non_existing_set")) + .then( + res => { throw 'unexpectedly resolved promise for spop result: ' + res }, + err => { if (err.error() !== 'ERR no elements in set') { throw 'unexpected error for srandmember operation: ' + err.error() } } + ) + `, rs.Addr())) + + return err + }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"SPOP", "existing_set"}, + {"SPOP", "non_existing_set"}, + }, rs.GotCommands()) +} + +func TestClientSendCommand(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + rs := RunT(t) + fooWasSet := false + rs.RegisterCommandHandler("SADD", func(c *Connection, args []string) { + if len(args) != 2 { + c.WriteError(errors.New("ERR unexpected number of arguments for 'SADD' command")) + return + } + + if args[1] == "foo" && !fooWasSet { + fooWasSet = true + c.WriteInteger(1) + return + } - ts := newTestSetup(t) // Setup to execute code in the main context + c.WriteInteger(0) + }) - gotScriptErr := ts.ev.Start(func() error { - _, err := ts.rt.RunString(fmt.Sprintf(` + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` const redis = new Client({ addrs: new Array("%s"), }); - redis.close(); - redis.close(); - `, ts.redis.Addr())) + redis.sendCommand("sadd", "existing_set", "foo") + .then(res => { if (res !== 1) { throw 'unexpected value for sadd result: ' + res } }) + .then(() => redis.sendCommand("sadd", "existing_set", "foo")) + .then(res => { if (res !== 0) { throw 'unexpected value for sadd result: ' + res } }) - return err - }) + `, rs.Addr())) - assert.NoError(t, gotScriptErr) + return err }) + + assert.NoError(t, gotScriptErr) + assert.Equal(t, 2, rs.HandledCommandsCount()) + assert.Equal(t, [][]string{ + {"SADD", "existing_set", "foo"}, + {"SADD", "existing_set", "foo"}, + }, rs.GotCommands()) +} + +func TestClientCommandsInInitContext(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + statement string + }{ + { + name: "set should fail when used in the init context", + statement: "redis.set('should', 'fail')", + }, + { + name: "get should fail when used in the init context", + statement: "redis.get('shouldfail')", + }, + { + name: "getSet should fail when used in the init context", + statement: "redis.getSet('should', 'fail')", + }, + { + name: "del should fail when used in the init context", + statement: "redis.del('should', 'fail')", + }, + { + name: "getDel should fail when used in the init context", + statement: "redis.getDel('shouldfail')", + }, + { + name: "exists should fail when used in the init context", + statement: "redis.exists('should', 'fail')", + }, + { + name: "incr should fail when used in the init context", + statement: "redis.incr('shouldfail')", + }, + { + name: "incrBy should fail when used in the init context", + statement: "redis.incrBy('shouldfail', 10)", + }, + { + name: "decr should fail when used in the init context", + statement: "redis.decr('shouldfail')", + }, + { + name: "decrBy should fail when used in the init context", + statement: "redis.decrBy('shouldfail', 10)", + }, + { + name: "randomKey should fail when used in the init context", + statement: "redis.randomKey()", + }, + { + name: "mget should fail when used in the init context", + statement: "redis.mget('should', 'fail')", + }, + { + name: "expire should fail when used in the init context", + statement: "redis.expire('shouldfail', 10)", + }, + { + name: "ttl should fail when used in the init context", + statement: "redis.ttl('shouldfail')", + }, + { + name: "persist should fail when used in the init context", + statement: "redis.persist('shouldfail')", + }, + { + name: "lpush should fail when used in the init context", + statement: "redis.lpush('should', 'fail', 'indeed')", + }, + { + name: "rpush should fail when used in the init context", + statement: "redis.rpush('should', 'fail', 'indeed')", + }, + { + name: "lpop should fail when used in the init context", + statement: "redis.lpop('shouldfail')", + }, + { + name: "rpop should fail when used in the init context", + statement: "redis.rpop('shouldfail')", + }, + { + name: "lrange should fail when used in the init context", + statement: "redis.lrange('shouldfail', 0, 5)", + }, + { + name: "lindex should fail when used in the init context", + statement: "redis.lindex('shouldfail', 1)", + }, + { + name: "lset should fail when used in the init context", + statement: "redis.lset('shouldfail', 1, 'fail')", + }, + { + name: "lrem should fail when used in the init context", + statement: "redis.lrem('should', 1, 'fail')", + }, + { + name: "llen should fail when used in the init context", + statement: "redis.llen('shouldfail')", + }, + { + name: "hset should fail when used in the init context", + statement: "redis.hset('shouldfail', 'foo', 'bar')", + }, + { + name: "hsetnx should fail when used in the init context", + statement: "redis.hsetnx('shouldfail', 'foo', 'bar')", + }, + { + name: "hget should fail when used in the init context", + statement: "redis.hget('should', 'fail')", + }, + { + name: "hdel should fail when used in the init context", + statement: "redis.hdel('should', 'fail', 'indeed')", + }, + { + name: "hgetall should fail when used in the init context", + statement: "redis.hgetall('shouldfail')", + }, + { + name: "hkeys should fail when used in the init context", + statement: "redis.hkeys('shouldfail')", + }, + { + name: "hvals should fail when used in the init context", + statement: "redis.hvals('shouldfail')", + }, + { + name: "hlen should fail when used in the init context", + statement: "redis.hlen('shouldfail')", + }, + { + name: "hincrby should fail when used in the init context", + statement: "redis.hincrby('should', 'fail', 10)", + }, + { + name: "sadd should fail when used in the init context", + statement: "redis.sadd('should', 'fail', 'indeed')", + }, + { + name: "srem should fail when used in the init context", + statement: "redis.srem('should', 'fail', 'indeed')", + }, + { + name: "sismember should fail when used in the init context", + statement: "redis.sismember('should', 'fail')", + }, + { + name: "smembers should fail when used in the init context", + statement: "redis.smembers('shouldfail')", + }, + { + name: "srandmember should fail when used in the init context", + statement: "redis.srandmember('shouldfail')", + }, + { + name: "persist should fail when used in the init context", + statement: "redis.persist('shouldfail')", + }, + { + name: "spop should fail when used in the init context", + statement: "redis.spop('shouldfail')", + }, + { + name: "sendCommand should fail when used in the init context", + statement: "redis.sendCommand('GET', 'shouldfail')", + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ts := newInitContextTestSetup(t) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("unreachable:42424"), + }); + + %s.then(res => { throw 'expected to fail when called in the init context' }) + `, tc.statement)) + + return err + }) + + assert.Error(t, gotScriptErr) + }) + } +} + +func TestClientCommandsAgainstUnreachableServer(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + statement string + }{ + { + name: "set should fail when server is unreachable", + statement: "redis.set('should', 'fail')", + }, + { + name: "get should fail when server is unreachable", + statement: "redis.get('shouldfail')", + }, + { + name: "getSet should fail when server is unreachable", + statement: "redis.getSet('should', 'fail')", + }, + { + name: "del should fail when server is unreachable", + statement: "redis.del('should', 'fail')", + }, + { + name: "getDel should fail when server is unreachable", + statement: "redis.getDel('shouldfail')", + }, + { + name: "exists should fail when server is unreachable", + statement: "redis.exists('should', 'fail')", + }, + { + name: "incr should fail when server is unreachable", + statement: "redis.incr('shouldfail')", + }, + { + name: "incrBy should fail when server is unreachable", + statement: "redis.incrBy('shouldfail', 10)", + }, + { + name: "decr should fail when server is unreachable", + statement: "redis.decr('shouldfail')", + }, + { + name: "decrBy should fail when server is unreachable", + statement: "redis.decrBy('shouldfail', 10)", + }, + { + name: "randomKey should fail when server is unreachable", + statement: "redis.randomKey()", + }, + { + name: "mget should fail when server is unreachable", + statement: "redis.mget('should', 'fail')", + }, + { + name: "expire should fail when server is unreachable", + statement: "redis.expire('shouldfail', 10)", + }, + { + name: "ttl should fail when server is unreachable", + statement: "redis.ttl('shouldfail')", + }, + { + name: "persist should fail when server is unreachable", + statement: "redis.persist('shouldfail')", + }, + { + name: "lpush should fail when server is unreachable", + statement: "redis.lpush('should', 'fail', 'indeed')", + }, + { + name: "rpush should fail when server is unreachable", + statement: "redis.rpush('should', 'fail', 'indeed')", + }, + { + name: "lpop should fail when server is unreachable", + statement: "redis.lpop('shouldfail')", + }, + { + name: "rpop should fail when server is unreachable", + statement: "redis.rpop('shouldfail')", + }, + { + name: "lrange should fail when server is unreachable", + statement: "redis.lrange('shouldfail', 0, 5)", + }, + { + name: "lindex should fail when server is unreachable", + statement: "redis.lindex('shouldfail', 1)", + }, + { + name: "lset should fail when server is unreachable", + statement: "redis.lset('shouldfail', 1, 'fail')", + }, + { + name: "lrem should fail when server is unreachable", + statement: "redis.lrem('should', 1, 'fail')", + }, + { + name: "llen should fail when server is unreachable", + statement: "redis.llen('shouldfail')", + }, + { + name: "hset should fail when server is unreachable", + statement: "redis.hset('shouldfail', 'foo', 'bar')", + }, + { + name: "hsetnx should fail when server is unreachable", + statement: "redis.hsetnx('shouldfail', 'foo', 'bar')", + }, + { + name: "hget should fail when server is unreachable", + statement: "redis.hget('should', 'fail')", + }, + { + name: "hdel should fail when server is unreachable", + statement: "redis.hdel('should', 'fail', 'indeed')", + }, + { + name: "hgetall should fail when server is unreachable", + statement: "redis.hgetall('shouldfail')", + }, + { + name: "hkeys should fail when server is unreachable", + statement: "redis.hkeys('shouldfail')", + }, + { + name: "hvals should fail when server is unreachable", + statement: "redis.hvals('shouldfail')", + }, + { + name: "hlen should fail when server is unreachable", + statement: "redis.hlen('shouldfail')", + }, + { + name: "hincrby should fail when server is unreachable", + statement: "redis.hincrby('should', 'fail', 10)", + }, + { + name: "sadd should fail when server is unreachable", + statement: "redis.sadd('should', 'fail', 'indeed')", + }, + { + name: "srem should fail when server is unreachable", + statement: "redis.srem('should', 'fail', 'indeed')", + }, + { + name: "sismember should fail when server is unreachable", + statement: "redis.sismember('should', 'fail')", + }, + { + name: "smembers should fail when server is unreachable", + statement: "redis.smembers('shouldfail')", + }, + { + name: "srandmember should fail when server is unreachable", + statement: "redis.srandmember('shouldfail')", + }, + { + name: "persist should fail when server is unreachable", + statement: "redis.persist('shouldfail')", + }, + { + name: "spop should fail when server is unreachable", + statement: "redis.spop('shouldfail')", + }, + { + name: "sendCommand should fail when server is unreachable", + statement: "redis.sendCommand('GET', 'shouldfail')", + }, + } + + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ts := newTestSetup(t) + + gotScriptErr := ts.ev.Start(func() error { + _, err := ts.rt.RunString(fmt.Sprintf(` + const redis = new Client({ + addrs: new Array("unreachable:42424"), + }); + + %s.then(res => { throw 'expected to fail when server is unreachable' }) + `, tc.statement)) + + return err + }) + + assert.Error(t, gotScriptErr) + }) + } } func TestClientIsSupportedType(t *testing.T) { @@ -246,19 +2399,24 @@ func TestClientIsSupportedType(t *testing.T) { gotErr := c.isSupportedType(3, int(123), []string{"1", "2", "3"}) assert.Error(t, gotErr) - assert.Contains(t, gotErr.Error(), "argument at index: 4") + assert.Contains(t, gotErr.Error(), "argument at index 4") }) } +// testSetup is a helper struct holding components +// necessary to test the redis client, in the context +// of the execution of a k6 script. type testSetup struct { - tb *httpmultibin.HTTPMultiBin rt *goja.Runtime state *lib.State samples chan metrics.SampleContainer ev *eventloop.EventLoop - redis *miniredis.Miniredis } +// newTestSetup initializes a new test setup. +// It prepares a test setup with a mocked redis server and a goja runtime, +// and event loop, ready to execute scripts as if being executed in the +// main context of k6. func newTestSetup(t testing.TB) testSetup { tb := httpmultibin.NewHTTPMultiBin(t) @@ -306,17 +2464,20 @@ func newTestSetup(t testing.TB) testSetup { state: state, samples: samples, ev: ev, - redis: miniredis.RunT(t), } } +// newInitContextTestSetup initializes a new test setup. +// It prepares a test setup with a mocked redis server and a goja runtime, +// and event loop, ready to execute scripts as if being executed in the +// main context of k6. func newInitContextTestSetup(t testing.TB) testSetup { rt := goja.New() rt.SetFieldNameMapper(common.FieldNameMapper{}) samples := make(chan metrics.SampleContainer, 1000) - var state *lib.State = nil + var state *lib.State vu := &modulestest.VU{ CtxField: context.Background(), @@ -336,6 +2497,5 @@ func newInitContextTestSetup(t testing.TB) testSetup { state: state, samples: samples, ev: ev, - redis: miniredis.RunT(t), } } diff --git a/go.mod b/go.mod index 24ad7bb..bdd8dc6 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/grafana/xk6-redis go 1.17 require ( - github.com/alicebob/miniredis/v2 v2.22.0 github.com/dop251/goja v0.0.0-20220405120441-9037c2b61cbf github.com/go-redis/redis/v8 v8.11.5 github.com/stretchr/testify v1.7.1 @@ -12,7 +11,6 @@ require ( ) require ( - github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -32,7 +30,6 @@ require ( github.com/serenize/snaker v0.0.0-20201027110005-a7ad2135616e // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/spf13/afero v1.1.2 // indirect - github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd // indirect golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect diff --git a/go.sum b/go.sum index ae9330e..9c7d0c0 100644 --- a/go.sum +++ b/go.sum @@ -5,10 +5,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/DataDog/datadog-go v0.0.0-20180330214955-e67964b4021a/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= github.com/Soontao/goHttpDigestClient v0.0.0-20170320082612-6d28bb1415c5/go.mod h1:5Q4+CyR7+Q3VMG8f78ou+QSX/BNUNUx5W48eFRat8DQ= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= -github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= -github.com/alicebob/miniredis/v2 v2.22.0 h1:lIHHiSkEyS1MkKHCHzN+0mWrA4YdbGdimE5iZ2sHSzo= -github.com/alicebob/miniredis/v2 v2.22.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= @@ -159,8 +155,6 @@ github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vl github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw= -github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= go.k6.io/k6 v0.38.2 h1:v4Dr7KhZVf+s6V6oz/pGtQ9ejTfNgUPcd/D3RH3GVdY= go.k6.io/k6 v0.38.2/go.mod h1:1bTdDsXTT2V3in3ZgdR15MDW6SQQh5nWni59tirqNB8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= @@ -201,7 +195,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=