diff --git a/integration_tests/commands/async/hyperloglog_test.go b/integration_tests/commands/async/hyperloglog_test.go index 450fa70ae..321077d36 100644 --- a/integration_tests/commands/async/hyperloglog_test.go +++ b/integration_tests/commands/async/hyperloglog_test.go @@ -51,12 +51,6 @@ func TestHyperLogLogCommands(t *testing.T) { "PFMERGE NON_EXISTING_SRC_KEY", "PFCOUNT NON_EXISTING_SRC_KEY"}, expected: []interface{}{"OK", int64(0)}, }, - { - name: "PFMERGE with srcKey non-existing", - commands: []string{ - "PFMERGE NON_EXISTING_SRC_KEY", "PFCOUNT NON_EXISTING_SRC_KEY"}, - expected: []interface{}{"OK", int64(0)}, - }, { name: "PFMERGE with destKey non-existing", commands: []string{ diff --git a/integration_tests/commands/http/hsetnx_test.go b/integration_tests/commands/http/hsetnx_test.go new file mode 100644 index 000000000..f4925f26e --- /dev/null +++ b/integration_tests/commands/http/hsetnx_test.go @@ -0,0 +1,73 @@ +package http + +import ( + "testing" + "time" + + "gotest.tools/v3/assert" +) + +func TestHSetNX(t *testing.T) { + exec := NewHTTPCommandExecutor() + + testCases := []struct { + name string + commands []HTTPCommand + expected []interface{} + delays []time.Duration + }{ + { + name: "HSetNX returns 0 when field is already set", + commands: []HTTPCommand{ + {Command: "HSETNX", Body: map[string]interface{}{"key": "key_nx_t1", "field": "field", "value": "value"}}, + {Command: "HSETNX", Body: map[string]interface{}{"key": "key_nx_t1", "field": "field", "value": "value_new"}}, + }, + expected: []interface{}{float64(1), float64(0)}, + delays: []time.Duration{0, 0}, + }, + { + name: "HSetNX with new field", + commands: []HTTPCommand{ + {Command: "HSETNX", Body: map[string]interface{}{"key": "key_nx_t2", "field": "field", "value": "value"}}, + }, + expected: []interface{}{float64(1)}, + delays: []time.Duration{0}, + }, + { + name: "HSetNX with wrong number of arguments", + commands: []HTTPCommand{ + {Command: "HSETNX", Body: map[string]interface{}{"key": "key_nx_t3", "field": "field", "value": "value"}}, + {Command: "HSETNX", Body: map[string]interface{}{"key": "key_nx_t3", "field": "field", "value": "value_new"}}, + {Command: "HSETNX", Body: map[string]interface{}{"key": "key_nx_t3"}}, + }, + expected: []interface{}{float64(1), float64(0), "ERR wrong number of arguments for 'hsetnx' command"}, + delays: []time.Duration{0, 0, 0}, + }, + { + name: "HSetNX with wrong type", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "key_nx_t4", "value": "v"}}, + {Command: "HSETNX", Body: map[string]interface{}{"key": "key_nx_t4", "field": "f", "value": "v_new"}}, + }, + expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"}, + delays: []time.Duration{0, 0}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.commands { + if tc.delays[i] > 0 { + time.Sleep(tc.delays[i]) + } + result, err := exec.FireCommand(cmd) + if err != nil { + // Check if the error message matches the expected result + assert.Equal(t, tc.expected[i], err.Error(), "Error message mismatch for cmd %s", cmd) + } else { + assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s, expected %v, got %v", cmd, tc.expected[i], result) + } + } + }) + } +} diff --git a/integration_tests/commands/http/hstrlen_test.go b/integration_tests/commands/http/hstrlen_test.go new file mode 100644 index 000000000..f91d53b0b --- /dev/null +++ b/integration_tests/commands/http/hstrlen_test.go @@ -0,0 +1,88 @@ +package http + +import ( + "log" + "testing" + "time" + + "gotest.tools/v3/assert" +) + +func TestHStrLen(t *testing.T) { + exec := NewHTTPCommandExecutor() + + testCases := []struct { + name string + commands []HTTPCommand + expected []interface{} + delays []time.Duration + }{ + { + name: "HSTRLEN with wrong number of arguments", + commands: []HTTPCommand{ + {Command: "HSTRLEN", Body: map[string]interface{}{"key": "KEY"}}, + {Command: "HSTRLEN", Body: map[string]interface{}{"key": "KEY", "field": "field", "another_field": "another_field"}}, + }, + expected: []interface{}{ + "ERR wrong number of arguments for 'hstrlen' command", + "ERR wrong number of arguments for 'hstrlen' command"}, + delays: []time.Duration{0, 0}, + }, + { + name: "HSTRLEN with wrong key", + commands: []HTTPCommand{ + {Command: "HSET", Body: map[string]interface{}{"key": "key_hStrLen1", "field": "field", "value": "value"}}, + {Command: "HSTRLEN", Body: map[string]interface{}{"key": "wrong_key_hStrLen", "field": "field"}}, + }, + expected: []interface{}{float64(1), float64(0)}, + delays: []time.Duration{0, 0}, + }, + { + name: "HSTRLEN with wrong field", + commands: []HTTPCommand{ + {Command: "HSET", Body: map[string]interface{}{"key": "key_hStrLen2", "field": "field", "value": "value"}}, + {Command: "HSTRLEN", Body: map[string]interface{}{"key": "key_hStrLen2", "field": "wrong_field"}}, + }, + expected: []interface{}{float64(1), float64(0)}, + delays: []time.Duration{0, 0}, + }, + { + name: "HSTRLEN", + commands: []HTTPCommand{ + {Command: "HSET", Body: map[string]interface{}{"key": "key_hStrLen3", "field": "field", "value": "HelloWorld"}}, + {Command: "HSTRLEN", Body: map[string]interface{}{"key": "key_hStrLen3", "field": "field"}}, + }, + expected: []interface{}{float64(1), float64(10)}, + delays: []time.Duration{0, 0}, + }, + { + name: "HSTRLEN with wrong type", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "key", "value": "value"}}, + {Command: "HSTRLEN", Body: map[string]interface{}{"key": "key", "field": "field"}}, + }, + expected: []interface{}{"OK", "WRONGTYPE Operation against a key holding the wrong kind of value"}, + delays: []time.Duration{0, 0}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": [...]string{"KEY", "key"}}}) + + for i, cmd := range tc.commands { + if tc.delays[i] > 0 { + time.Sleep(tc.delays[i]) + } + result, err := exec.FireCommand(cmd) + if err != nil { + // Check if the error message matches the expected result + log.Println(tc.expected[i]) + assert.Equal(t, tc.expected[i], err.Error(), "Error message mismatch for cmd %s", cmd) + } else { + assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s, expected %v, got %v", cmd, tc.expected[i], result) + } + } + }) + } +} diff --git a/integration_tests/commands/http/hyperloglog_test.go b/integration_tests/commands/http/hyperloglog_test.go new file mode 100644 index 000000000..d29ec7ce8 --- /dev/null +++ b/integration_tests/commands/http/hyperloglog_test.go @@ -0,0 +1,150 @@ +package http + +import ( + "testing" + "time" + + "gotest.tools/v3/assert" +) + +func TestHyperLogLogCommands(t *testing.T) { + exec := NewHTTPCommandExecutor() + + testCases := []struct { + name string + commands []HTTPCommand + expected []interface{} + delays []time.Duration + }{ + { + name: "PFADD with one key-value pair", + commands: []HTTPCommand{ + {Command: "PFADD", Body: map[string]interface{}{"key": "hll0", "value": "v1"}}, + {Command: "PFCOUNT", Body: map[string]interface{}{"key": "hll0"}}, + }, + expected: []interface{}{float64(1), float64(1)}, + delays: []time.Duration{0, 0}, + }, + { + name: "PFADD with multiple key-value pair", + commands: []HTTPCommand{ + {Command: "PFADD", Body: map[string]interface{}{"key": "hll", "values": [...]string{"a", "b", "c", "d", "e", "f"}}}, + {Command: "PFCOUNT", Body: map[string]interface{}{"key": "hll"}}, + }, + expected: []interface{}{float64(1), float64(6)}, + delays: []time.Duration{0, 0}, + }, + { + name: "PFADD with duplicate key-value pairs", + commands: []HTTPCommand{ + {Command: "PFADD", Body: map[string]interface{}{"key": "hll1", "values": [...]string{"foo", "bar", "zap"}}}, + {Command: "PFADD", Body: map[string]interface{}{"key": "hll1", "values": [...]string{"zap", "zap", "zap"}}}, + {Command: "PFADD", Body: map[string]interface{}{"key": "hll1", "values": [...]string{"foo", "bar"}}}, + {Command: "PFCOUNT", Body: map[string]interface{}{"key": "hll1"}}, + }, + expected: []interface{}{float64(1), float64(0), float64(0), float64(3)}, + delays: []time.Duration{0, 0, 0, 0}, + }, + { + name: "PFADD with multiple keys", + commands: []HTTPCommand{ + {Command: "PFADD", Body: map[string]interface{}{"key": "hll2", "values": [...]string{"foo", "bar", "zap"}}}, + {Command: "PFADD", Body: map[string]interface{}{"key": "hll2", "values": [...]string{"zap", "zap", "zap"}}}, + {Command: "PFCOUNT", Body: map[string]interface{}{"key": "hll2"}}, + {Command: "PFADD", Body: map[string]interface{}{"key": "some-other-hll", "values": [...]string{"1", "2", "3"}}}, + {Command: "PFCOUNT", Body: map[string]interface{}{"keys": [...]string{"hll2", "some-other-hll"}}}, + }, + expected: []interface{}{float64(1), float64(0), float64(3), float64(1), float64(6)}, + delays: []time.Duration{0, 0, 0, 0, 0}, + }, + { + name: "PFADD with non-existing key", + commands: []HTTPCommand{ + {Command: "PFADD", Body: map[string]interface{}{"key": "hll3", "values": [...]string{"foo", "bar", "zap"}}}, + {Command: "PFADD", Body: map[string]interface{}{"key": "hll3", "values": [...]string{"zap", "zap", "zap"}}}, + {Command: "PFCOUNT", Body: map[string]interface{}{"key": "hll3"}}, + {Command: "PFCOUNT", Body: map[string]interface{}{"keys": [...]string{"hll3", "non-exist-hll"}}}, + {Command: "PFADD", Body: map[string]interface{}{"key": "some-new-hll", "value": "abc"}}, + {Command: "PFCOUNT", Body: map[string]interface{}{"keys": [...]string{"hll3", "non-exist-hll", "some-new-hll"}}}, + }, + expected: []interface{}{float64(1), float64(0), float64(3), float64(3), float64(1), float64(4)}, + delays: []time.Duration{0, 0, 0, 0, 0, 0}, + }, + { + name: "PFMERGE with srcKey non-existing", + commands: []HTTPCommand{ + {Command: "PFMERGE", Body: map[string]interface{}{"key": "NON_EXISTING_SRC_KEY"}}, + {Command: "PFCOUNT", Body: map[string]interface{}{"key": "NON_EXISTING_SRC_KEY"}}, + }, + expected: []interface{}{"OK", float64(0)}, + delays: []time.Duration{0, 0}, + }, + { + name: "PFMERGE with destKey non-existing", + commands: []HTTPCommand{ + {Command: "PFMERGE", Body: map[string]interface{}{"keys": []string{"EXISTING_SRC_KEY", "NON_EXISTING_SRC_KEY"}}}, + {Command: "PFCOUNT", Body: map[string]interface{}{"key": "EXISTING_SRC_KEY"}}, + }, + expected: []interface{}{"OK", float64(0)}, + delays: []time.Duration{0, 0}, + }, + { + name: "PFMERGE with destKey existing", + commands: []HTTPCommand{ + {Command: "PFADD", Body: map[string]interface{}{"key": "DEST_KEY_1", "values": [...]string{"foo", "bar", "zap", "a"}}}, + {Command: "PFADD", Body: map[string]interface{}{"key": "DEST_KEY_2", "values": [...]string{"a", "b", "c", "foo"}}}, + {Command: "PFMERGE", Body: map[string]interface{}{"keys": [...]string{"SRC_KEY_1", "DEST_KEY_1", "DEST_KEY_2"}}}, + {Command: "PFCOUNT", Body: map[string]interface{}{"key": "SRC_KEY_1"}}, + }, + expected: []interface{}{float64(1), float64(1), "OK", float64(6)}, + delays: []time.Duration{0, 0, 0, 0}, + }, + { + name: "PFMERGE with only one destKey existing", + commands: []HTTPCommand{ + {Command: "PFADD", Body: map[string]interface{}{"key": "DEST_KEY_3", "values": [...]string{"foo", "bar", "zap", "a"}}}, + {Command: "PFMERGE", Body: map[string]interface{}{"keys": [...]string{"SRC_KEY_2", "DEST_KEY_3", "NON_EXISTING_DEST_KEY"}}}, + {Command: "PFCOUNT", Body: map[string]interface{}{"key": "SRC_KEY_2"}}, + }, + expected: []interface{}{float64(1), "OK", float64(4)}, + delays: []time.Duration{0, 0, 0}, + }, + { + name: "PFMERGE with invalid object", + commands: []HTTPCommand{ + {Command: "PFADD", Body: map[string]interface{}{"key": "INVALID_HLL", "values": [...]string{"a", "b", "c"}}}, + {Command: "SET", Body: map[string]interface{}{"key": "INVALID_HLL", "value": "1"}}, + {Command: "PFMERGE", Body: map[string]interface{}{"key": "INVALID_HLL"}}, + }, + expected: []interface{}{float64(1), "OK", "WRONGTYPE Key is not a valid HyperLogLog string value."}, + delays: []time.Duration{0, 0, 0}, + }, + { + name: "PFMERGE with invalid src object", + commands: []HTTPCommand{ + {Command: "PFADD", Body: map[string]interface{}{"key": "INVALID_SRC_HLL", "values": [...]string{"a", "b", "c"}}}, + {Command: "SET", Body: map[string]interface{}{"key": "INVALID_SRC_HLL", "value": "1"}}, + {Command: "PFMERGE", Body: map[string]interface{}{"keys": [...]string{"HLL", "INVALID_SRC_HLL"}}}, + }, + expected: []interface{}{float64(1), "OK", "WRONGTYPE Key is not a valid HyperLogLog string value."}, + delays: []time.Duration{0, 0, 0}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + for i, cmd := range tc.commands { + if tc.delays[i] > 0 { + time.Sleep(tc.delays[i]) + } + result, err := exec.FireCommand(cmd) + if err != nil { + // Check if the error message matches the expected result + assert.Equal(t, tc.expected[i], err.Error(), "Error message mismatch for cmd %s", cmd) + } else { + assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s, expected %v, got %v", cmd, tc.expected[i], result) + } + } + }) + } +} diff --git a/integration_tests/commands/http/incr_by_float_test.go b/integration_tests/commands/http/incr_by_float_test.go new file mode 100644 index 000000000..d218d8619 --- /dev/null +++ b/integration_tests/commands/http/incr_by_float_test.go @@ -0,0 +1,132 @@ +package http + +import ( + "testing" + "time" + + "gotest.tools/v3/assert" +) + +func TestINCRBYFLOAT(t *testing.T) { + exec := NewHTTPCommandExecutor() + + invalidArgMessage := "ERR wrong number of arguments for 'incrbyfloat' command" + invalidValueTypeMessage := "WRONGTYPE Operation against a key holding the wrong kind of value" + invalidIncrTypeMessage := "ERR value is not an integer or a float" + valueOutOfRangeMessage := "ERR value is out of range" + + testCases := []struct { + name string + commands []HTTPCommand + expected []interface{} + delays []time.Duration + }{ + { + name: "Invalid number of arguments", + commands: []HTTPCommand{ + {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": nil}}, + {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": "foo"}}, + }, + expected: []interface{}{invalidArgMessage, invalidArgMessage}, + delays: []time.Duration{0, 0}, + }, + { + name: "Increment a non existing key", + commands: []HTTPCommand{ + {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": "foo", "value": 0.1}}, + {Command: "GET", Body: map[string]interface{}{"key": "foo"}}, + }, + expected: []interface{}{"0.1", "0.1"}, + delays: []time.Duration{0, 0}, + }, + { + name: "Increment a key with an integer value", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "1"}}, + {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": "foo", "value": 0.1}}, + {Command: "GET", Body: map[string]interface{}{"key": "foo"}}, + }, + expected: []interface{}{"OK", "1.1", "1.1"}, + delays: []time.Duration{0, 0, 0}, + }, + { + name: "Increment and then decrement a key with the same value", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "1"}}, + {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": "foo", "value": 0.1}}, + {Command: "GET", Body: map[string]interface{}{"key": "foo"}}, + {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": "foo", "value": -0.1}}, + {Command: "GET", Body: map[string]interface{}{"key": "foo"}}, + }, + expected: []interface{}{"OK", "1.1", "1.1", "1", "1"}, + delays: []time.Duration{0, 0, 0, 0, 0}, + }, + { + name: "Increment a non numeric value", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "bar"}}, + {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": "foo", "value": 0.1}}, + }, + expected: []interface{}{"OK", invalidValueTypeMessage}, + delays: []time.Duration{0, 0}, + }, + { + name: "Increment by a non numeric value", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "1"}}, + {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": "foo", "value": "bar"}}, + }, + expected: []interface{}{"OK", invalidIncrTypeMessage}, + delays: []time.Duration{0, 0}, + }, + { + name: "Increment by both integer and float", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "1"}}, + {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": "foo", "value": 1}}, + {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": "foo", "value": 0.1}}, + }, + expected: []interface{}{"OK", "2", "2.1"}, + delays: []time.Duration{0, 0, 0}, + }, + { + name: "Increment that would make the value Inf", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "1e308"}}, + {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": "foo", "value": 1e308}}, + {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": "foo", "value": -1e308}}, + }, + expected: []interface{}{"OK", valueOutOfRangeMessage, "0"}, + delays: []time.Duration{0, 0, 0}, + }, + { + name: "Increment that would make the value -Inf", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "foo", "value": "-1e308"}}, + {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": "foo", "value": -1e308}}, + {Command: "INCRBYFLOAT", Body: map[string]interface{}{"key": "foo", "value": 1e308}}, + }, + expected: []interface{}{"OK", valueOutOfRangeMessage, "0"}, + delays: []time.Duration{0, 0, 0}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"key": "foo"}}) + + for i, cmd := range tc.commands { + if tc.delays[i] > 0 { + time.Sleep(tc.delays[i]) + } + result, err := exec.FireCommand(cmd) + if err != nil { + // Check if the error message matches the expected result + assert.Equal(t, tc.expected[i], err.Error(), "Error message mismatch for cmd %s", cmd) + } else { + assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s, expected %v, got %v", cmd, tc.expected[i], result) + } + } + }) + } +} diff --git a/integration_tests/commands/http/incr_test.go b/integration_tests/commands/http/incr_test.go new file mode 100644 index 000000000..6a6ca3704 --- /dev/null +++ b/integration_tests/commands/http/incr_test.go @@ -0,0 +1,222 @@ +package http + +import ( + "math" + "strconv" + "testing" + "time" + + "gotest.tools/v3/assert" +) + +func TestINCR(t *testing.T) { + exec := NewHTTPCommandExecutor() + + exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": [...]string{"key1", "key2"}}}) + + testCases := []struct { + name string + commands []HTTPCommand + expected []interface{} + delays []time.Duration + }{ + { + name: "Increment multiple keys", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "key1", "value": 0}}, + {Command: "INCR", Body: map[string]interface{}{"key": "key1"}}, + {Command: "INCR", Body: map[string]interface{}{"key": "key1"}}, + {Command: "INCR", Body: map[string]interface{}{"key": "key2"}}, + {Command: "GET", Body: map[string]interface{}{"key": "key1"}}, + {Command: "GET", Body: map[string]interface{}{"key": "key2"}}, + }, + expected: []interface{}{"OK", float64(1), float64(2), float64(1), float64(2), float64(1)}, + delays: []time.Duration{0, 0, 0, 0, 0, 0}, + }, + { + name: "Increment to and from max int64", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "max_int", "value": strconv.Itoa(math.MaxInt64 - 1)}}, + {Command: "INCR", Body: map[string]interface{}{"key": "max_int"}}, + {Command: "INCR", Body: map[string]interface{}{"key": "max_int"}}, + {Command: "SET", Body: map[string]interface{}{"key": "max_int", "value": strconv.Itoa(math.MaxInt64)}}, + {Command: "INCR", Body: map[string]interface{}{"key": "max_int"}}, + }, + expected: []interface{}{"OK", float64(math.MaxInt64), "ERR increment or decrement would overflow", "OK", "ERR increment or decrement would overflow"}, + delays: []time.Duration{0, 0, 0, 0, 0}, + }, + { + name: "Increment from min int64", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "min_int", "value": strconv.Itoa(math.MinInt64)}}, + {Command: "INCR", Body: map[string]interface{}{"key": "min_int"}}, + {Command: "INCR", Body: map[string]interface{}{"key": "min_int"}}, + }, + expected: []interface{}{"OK", float64(math.MinInt64 + 1), float64(math.MinInt64 + 2)}, + delays: []time.Duration{0, 0, 0}, + }, + { + name: "Increment non-integer values", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "float_key", "value": "3.14"}}, + {Command: "INCR", Body: map[string]interface{}{"key": "float_key"}}, + {Command: "SET", Body: map[string]interface{}{"key": "string_key", "value": "hello"}}, + {Command: "INCR", Body: map[string]interface{}{"key": "string_key"}}, + {Command: "SET", Body: map[string]interface{}{"key": "bool_key", "value": "true"}}, + {Command: "INCR", Body: map[string]interface{}{"key": "bool_key"}}, + }, + expected: []interface{}{"OK", "ERR value is not an integer or out of range", "OK", "ERR value is not an integer or out of range", "OK", "ERR value is not an integer or out of range"}, + delays: []time.Duration{0, 0, 0, 0, 0, 0}, + }, + { + name: "Increment non-existent key", + commands: []HTTPCommand{ + {Command: "INCR", Body: map[string]interface{}{"key": "non_existent"}}, + {Command: "GET", Body: map[string]interface{}{"key": "non_existent"}}, + {Command: "INCR", Body: map[string]interface{}{"key": "non_existent"}}, + }, + expected: []interface{}{float64(1), float64(1), float64(2)}, + delays: []time.Duration{0, 0, 0}, + }, + { + name: "Increment string representing integers", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "str_int1", "value": "42"}}, + {Command: "INCR", Body: map[string]interface{}{"key": "str_int1"}}, + {Command: "SET", Body: map[string]interface{}{"key": "str_int2", "value": "-10"}}, + {Command: "INCR", Body: map[string]interface{}{"key": "str_int2"}}, + {Command: "SET", Body: map[string]interface{}{"key": "str_int3", "value": "0"}}, + {Command: "INCR", Body: map[string]interface{}{"key": "str_int3"}}, + }, + expected: []interface{}{"OK", float64(43), "OK", float64(-9), "OK", float64(1)}, + delays: []time.Duration{0, 0, 0, 0, 0, 0}, + }, + { + name: "Increment with expiry", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "expiry_key", "value": 0, "ex": 1}}, + {Command: "INCR", Body: map[string]interface{}{"key": "expiry_key"}}, + {Command: "INCR", Body: map[string]interface{}{"key": "expiry_key"}}, + {Command: "INCR", Body: map[string]interface{}{"key": "expiry_key"}}, + }, + expected: []interface{}{"OK", float64(1), float64(2), float64(1)}, + delays: []time.Duration{0, 0, 0, 1 * time.Second}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": [...]string{"key1", "key2", "expiry_key", "max_int", "min_int", "float_key", "string_key", "bool_key"}}}) + + for i, cmd := range tc.commands { + if tc.delays[i] > 0 { + time.Sleep(tc.delays[i]) + } + result, err := exec.FireCommand(cmd) + if err != nil { + // Check if the error message matches the expected result + assert.Equal(t, tc.expected[i], err.Error(), "Error message mismatch for cmd %s", cmd) + } else { + assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s, expected %v, got %v", cmd, tc.expected[i], result) + } + } + }) + } +} + +func TestINCRBY(t *testing.T) { + exec := NewHTTPCommandExecutor() + + testCases := []struct { + name string + commands []HTTPCommand + expected []interface{} + delays []time.Duration + }{ + { + name: "INCRBY with postive increment", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "key", "value": 3}}, + {Command: "INCRBY", Body: map[string]interface{}{"key": "key", "value": 2}}, + {Command: "INCRBY", Body: map[string]interface{}{"key": "key", "value": 1}}, + {Command: "GET", Body: map[string]interface{}{"key": "key"}}, + }, + expected: []interface{}{"OK", float64(5), float64(6), float64(6)}, + delays: []time.Duration{0, 0, 0, 0}, + }, + { + name: "INCRBY with negative increment", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "key", "value": 100}}, + {Command: "INCRBY", Body: map[string]interface{}{"key": "key", "value": -2}}, + {Command: "INCRBY", Body: map[string]interface{}{"key": "key", "value": -10}}, + {Command: "INCRBY", Body: map[string]interface{}{"key": "key", "value": -88}}, + {Command: "INCRBY", Body: map[string]interface{}{"key": "key", "value": -100}}, + {Command: "GET", Body: map[string]interface{}{"key": "key"}}, + }, + expected: []interface{}{"OK", float64(98), float64(88), float64(0), float64(-100), float64(-100)}, + delays: []time.Duration{0, 0, 0, 0, 0, 0}, + }, + { + name: "INCRBY with unset key", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "key", "value": 3}}, + {Command: "INCRBY", Body: map[string]interface{}{"key": "unsetKey", "value": 2}}, + {Command: "GET", Body: map[string]interface{}{"key": "key"}}, + {Command: "GET", Body: map[string]interface{}{"key": "unsetKey"}}, + }, + expected: []interface{}{"OK", float64(2), float64(3), float64(2)}, + delays: []time.Duration{0, 0, 0, 0}, + }, + { + name: "edge case with maximum int value", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "key", "value": strconv.Itoa(math.MaxInt64 - 1)}}, + {Command: "INCRBY", Body: map[string]interface{}{"key": "key", "value": 1}}, + {Command: "INCRBY", Body: map[string]interface{}{"key": "key", "value": 1}}, + {Command: "GET", Body: map[string]interface{}{"key": "key"}}, + }, + expected: []interface{}{"OK", float64(math.MaxInt64), "ERR increment or decrement would overflow", float64(math.MaxInt64)}, + delays: []time.Duration{0, 0, 0, 0}, + }, + { + name: "edge case with minimum int value", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "key", "value": strconv.Itoa(math.MinInt64 + 1)}}, + {Command: "INCRBY", Body: map[string]interface{}{"key": "key", "value": -1}}, + {Command: "INCRBY", Body: map[string]interface{}{"key": "key", "value": -1}}, + {Command: "GET", Body: map[string]interface{}{"key": "key"}}, + }, + expected: []interface{}{"OK", float64(math.MinInt64), "ERR increment or decrement would overflow", float64(math.MinInt64)}, + delays: []time.Duration{0, 0, 0, 0}, + }, + { + name: "edge case with string values", + commands: []HTTPCommand{ + {Command: "SET", Body: map[string]interface{}{"key": "key", "value": 1}}, + {Command: "INCRBY", Body: map[string]interface{}{"key": "stringKey", "value": "abc"}}, + }, + expected: []interface{}{"OK", "ERR value is not an integer or out of range"}, + delays: []time.Duration{0, 0}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer exec.FireCommand(HTTPCommand{Command: "DEL", Body: map[string]interface{}{"keys": [...]string{"key", "unsetKey", "stringkey"}}}) + + for i, cmd := range tc.commands { + if tc.delays[i] > 0 { + time.Sleep(tc.delays[i]) + } + result, err := exec.FireCommand(cmd) + if err != nil { + // Check if the error message matches the expected result + assert.Equal(t, tc.expected[i], err.Error(), "Error message mismatch for cmd %s", cmd) + } else { + assert.Equal(t, tc.expected[i], result, "Value mismatch for cmd %s, expected %v, got %v", cmd, tc.expected[i], result) + } + } + }) + } +}