Implemented tests for AddCommand, ExecuteCommand and RemoveCommand methods

This commit is contained in:
Kelvin Clement Mwinuka
2024-04-28 10:43:46 +08:00
parent c241cc07b1
commit 1d56e9839b
10 changed files with 373 additions and 52 deletions

View File

@@ -225,18 +225,18 @@ func (server *EchoVault) RPop(key string) (string, error) {
//
// `values` - ...string - the list of elements to add to push to the beginning of the list.
//
// Returns: "OK" when the list has been successfully modified or created.
// Returns: An integer with the length of the new list.
//
// Errors:
//
// "LPush command on non-list item" - when the provided key is not a list.
func (server *EchoVault) LPush(key string, values ...string) (string, error) {
func (server *EchoVault) LPush(key string, values ...string) (int, error) {
cmd := append([]string{"LPUSH", key}, values...)
b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
if err != nil {
return "", err
return 0, err
}
return internal.ParseStringResponse(b)
return internal.ParseIntegerResponse(b)
}
// LPushX pushed 1 or more values to the beginning of an existing list. The command only succeeds on a pre-existing list.
@@ -247,18 +247,18 @@ func (server *EchoVault) LPush(key string, values ...string) (string, error) {
//
// `values` - ...string - the list of elements to add to push to the beginning of the list.
//
// Returns: "OK" when the list has been successfully modified.
// Returns: An integer with the length of the new list.
//
// Errors:
//
// "LPushX command on non-list item" - when the provided key is not a list or doesn't exist.
func (server *EchoVault) LPushX(key string, values ...string) (string, error) {
func (server *EchoVault) LPushX(key string, values ...string) (int, error) {
cmd := append([]string{"LPUSHX", key}, values...)
b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
if err != nil {
return "", err
return 0, err
}
return internal.ParseStringResponse(b)
return internal.ParseIntegerResponse(b)
}
// RPush pushed 1 or more values to the end of a list. If the list does not exist, a new list is created
@@ -270,18 +270,18 @@ func (server *EchoVault) LPushX(key string, values ...string) (string, error) {
//
// `values` - ...string - the list of elements to add to push to the end of the list.
//
// Returns: "OK" when the list has been successfully modified or created.
// Returns: An integer with the length of the new list.
//
// Errors:
//
// "RPush command on non-list item" - when the provided key is not a list.
func (server *EchoVault) RPush(key string, values ...string) (string, error) {
func (server *EchoVault) RPush(key string, values ...string) (int, error) {
cmd := append([]string{"RPUSH", key}, values...)
b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
if err != nil {
return "", err
return 0, err
}
return internal.ParseStringResponse(b)
return internal.ParseIntegerResponse(b)
}
// RPushX pushed 1 or more values to the end of an existing list. The command only succeeds on a pre-existing list.
@@ -292,16 +292,16 @@ func (server *EchoVault) RPush(key string, values ...string) (string, error) {
//
// `values` - ...string - the list of elements to add to push to the end of the list.
//
// Returns: "OK" when the list has been successfully modified.
// Returns: An integer with the length of the new list.
//
// Errors:
//
// "RPushX command on non-list item" - when the provided key is not a list or doesn't exist.
func (server *EchoVault) RPushX(key string, values ...string) (string, error) {
func (server *EchoVault) RPushX(key string, values ...string) (int, error) {
cmd := append([]string{"RPUSHX", key}, values...)
b, err := server.handleCommand(server.context, internal.EncodeCommand(cmd), nil, false, true)
if err != nil {
return "", err
return 0, err
}
return internal.ParseStringResponse(b)
return internal.ParseIntegerResponse(b)
}

11
echovault/config.go Normal file
View File

@@ -0,0 +1,11 @@
package echovault
import (
"github.com/echovault/echovault/internal/config"
)
// DefaultConfig returns the default configuration.
// This should be used when using EchoVault as an embedded library.
func DefaultConfig() config.Config {
return config.DefaultConfig()
}

View File

@@ -73,7 +73,11 @@ func (server *EchoVault) handleCommand(ctx context.Context, message []byte, conn
synchronize := command.Sync
handler := command.HandlerFunc
subCommand, ok := internal.GetSubCommand(command, cmd).(internal.SubCommand)
sc, err := internal.GetSubCommand(command, cmd)
if err != nil {
return nil, err
}
subCommand, ok := sc.(internal.SubCommand)
if ok {
synchronize = subCommand.Sync
handler = subCommand.HandlerFunc

View File

@@ -193,7 +193,7 @@ func (acl *ACL) DeleteUser(_ context.Context, usernames []string) error {
}
}
// Skip if the current username was not found in the ACL
if username != user.Username {
if user == nil {
continue
}
// Terminate every connection attached to this user

View File

@@ -421,7 +421,7 @@ func handleLPush(params internal.HandlerFuncParams) ([]byte, error) {
if err = params.SetValue(params.Context, key, append(newElems, l...)); err != nil {
return nil, err
}
return []byte(constants.OkResponse), nil
return []byte(fmt.Sprintf(":%d\r\n", len(l)+len(newElems))), nil
}
func handleRPush(params internal.HandlerFuncParams) ([]byte, error) {
@@ -469,7 +469,7 @@ func handleRPush(params internal.HandlerFuncParams) ([]byte, error) {
if err = params.SetValue(params.Context, key, append(l, newElems...)); err != nil {
return nil, err
}
return []byte(constants.OkResponse), nil
return []byte(fmt.Sprintf(":%d\r\n", len(l)+len(newElems))), nil
}
func handlePop(params internal.HandlerFuncParams) ([]byte, error) {

View File

@@ -102,7 +102,14 @@ func (fsm *FSM) Apply(log *raft.Log) interface{} {
handler := command.HandlerFunc
subCommand, ok := internal.GetSubCommand(command, request.CMD).(internal.SubCommand)
sc, err := internal.GetSubCommand(command, request.CMD)
if err != nil {
return internal.ApplyResponse{
Error: err,
Response: nil,
}
}
subCommand, ok := sc.(internal.SubCommand)
if ok {
handler = subCommand.HandlerFunc
}

View File

@@ -128,16 +128,21 @@ func GetIPAddress() (string, error) {
return localAddr, nil
}
func GetSubCommand(command Command, cmd []string) interface{} {
if len(command.SubCommands) == 0 || len(cmd) < 2 {
return nil
func GetSubCommand(command Command, cmd []string) (interface{}, error) {
if command.SubCommands == nil || len(command.SubCommands) == 0 {
// If the command has no sub-commands, return nil
return nil, nil
}
if len(cmd) < 2 {
// If the cmd provided by the user has less than 2 tokens, there's no need to search for a subcommand
return nil, nil
}
for _, subCommand := range command.SubCommands {
if strings.EqualFold(subCommand.Command, cmd[1]) {
return subCommand
return subCommand, nil
}
}
return nil
return nil, fmt.Errorf("command %s %s not supported", cmd[0], cmd[1])
}
func IsWriteCommand(command Command, subCommand SubCommand) bool {

View File

@@ -13,3 +13,297 @@
// limitations under the License.
package admin
import (
"bytes"
"errors"
"fmt"
"github.com/echovault/echovault/constants"
"github.com/echovault/echovault/echovault"
"github.com/echovault/echovault/internal/config"
"github.com/echovault/echovault/types"
"github.com/tidwall/resp"
"strconv"
"testing"
)
func createEchoVault() *echovault.EchoVault {
ev, _ := echovault.NewEchoVault(
echovault.WithConfig(config.Config{
DataDir: "",
}),
)
return ev
}
func TestEchoVault_AddCommand(t *testing.T) {
type args struct {
command echovault.CommandOptions
}
type scenarios struct {
name string
command []string
wantRes int
wantErr error
}
tests := []struct {
name string
args args
scenarios []scenarios
wantErr bool
}{
{
name: "1 Add command without subcommands",
wantErr: false,
args: args{
command: echovault.CommandOptions{
Command: "CommandOne",
Module: "test-module",
Description: `(CommandOne write-key read-key <value>)
Test command to handle successful addition of a single command without subcommands.
The value passed must be an integer.`,
Categories: []string{},
Sync: false,
KeyExtractionFunc: func(cmd []string) (types.PluginKeyExtractionFuncResult, error) {
if len(cmd) != 4 {
return types.PluginKeyExtractionFuncResult{}, errors.New(constants.WrongArgsResponse)
}
return types.PluginKeyExtractionFuncResult{
WriteKeys: cmd[1:2],
ReadKeys: cmd[2:3],
}, nil
},
HandlerFunc: func(params types.PluginHandlerFuncParams) ([]byte, error) {
if len(params.Command) != 4 {
return nil, errors.New(constants.WrongArgsResponse)
}
value := params.Command[3]
i, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return nil, errors.New("value must be an integer")
}
return []byte(fmt.Sprintf(":%d\r\n", i)), nil
},
},
},
scenarios: []scenarios{
{
name: "1 Successfully execute the command and return the expected integer.",
command: []string{"CommandOne", "write-key1", "read-key1", "1111"},
wantRes: 1111,
wantErr: nil,
},
{
name: "2 Get error due to command being too long",
command: []string{"CommandOne", "write-key1", "read-key1", "1111", "2222"},
wantRes: 0,
wantErr: errors.New(constants.WrongArgsResponse),
},
{
name: "3 Get error due to command being too short",
command: []string{"CommandOne", "write-key1", "read-key1"},
wantRes: 0,
wantErr: errors.New(constants.WrongArgsResponse),
},
{
name: "4 Get error due to value not being an integer",
command: []string{"CommandOne", "write-key1", "read-key1", "string"},
wantRes: 0,
wantErr: errors.New("value must be an integer"),
},
},
},
{
name: "2 Add command with subcommands",
wantErr: false,
args: args{
command: echovault.CommandOptions{
Command: "CommandTwo",
SubCommand: []echovault.SubCommandOptions{
{
Command: "SubCommandOne",
Module: "test-module",
Description: `(CommandTwo SubCommandOne write-key read-key <value>)
Test command to handle successful addition of a single command with subcommands.
The value passed must be an integer.`,
Categories: []string{},
Sync: false,
KeyExtractionFunc: func(cmd []string) (types.PluginKeyExtractionFuncResult, error) {
if len(cmd) != 5 {
return types.PluginKeyExtractionFuncResult{}, errors.New(constants.WrongArgsResponse)
}
return types.PluginKeyExtractionFuncResult{
WriteKeys: cmd[2:3],
ReadKeys: cmd[3:4],
}, nil
},
HandlerFunc: func(params types.PluginHandlerFuncParams) ([]byte, error) {
if len(params.Command) != 5 {
return nil, errors.New(constants.WrongArgsResponse)
}
value := params.Command[4]
i, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return nil, errors.New("value must be an integer")
}
return []byte(fmt.Sprintf(":%d\r\n", i)), nil
},
},
},
},
},
scenarios: []scenarios{
{
name: "1 Successfully execute the command and return the expected integer.",
command: []string{"CommandTwo", "SubCommandOne", "write-key1", "read-key1", "1111"},
wantRes: 1111,
wantErr: nil,
},
{
name: "2 Get error due to command being too long",
command: []string{"CommandTwo", "SubCommandOne", "write-key1", "read-key1", "1111", "2222"},
wantRes: 0,
wantErr: errors.New(constants.WrongArgsResponse),
},
{
name: "3 Get error due to command being too short",
command: []string{"CommandTwo", "SubCommandOne", "write-key1", "read-key1"},
wantRes: 0,
wantErr: errors.New(constants.WrongArgsResponse),
},
{
name: "4 Get error due to value not being an integer",
command: []string{"CommandTwo", "SubCommandOne", "write-key1", "read-key1", "string"},
wantRes: 0,
wantErr: errors.New("value must be an integer"),
},
},
},
}
for _, tt := range tests {
server := createEchoVault()
t.Run(tt.name, func(t *testing.T) {
if err := server.AddCommand(tt.args.command); (err != nil) != tt.wantErr {
t.Errorf("AddCommand() error = %v, wantErr %v", err, tt.wantErr)
}
for _, scenario := range tt.scenarios {
b, err := server.ExecuteCommand(scenario.command)
if scenario.wantErr != nil {
if scenario.wantErr.Error() != err.Error() {
t.Errorf("AddCommand() error = %v, wantErr %v", err, scenario.wantErr)
}
continue
}
r := resp.NewReader(bytes.NewReader(b))
v, _, _ := r.ReadValue()
if v.Integer() != scenario.wantRes {
t.Errorf("AddCommand() res = %v, wantRes %v", resp.BytesValue(b).Integer(), scenario.wantRes)
}
}
})
}
}
func TestEchoVault_ExecuteCommand(t *testing.T) {
type args struct {
key string
presetValue []string
command []string
}
tests := []struct {
name string
args args
wantRes int
wantErr error
}{
{
name: "1 Execute LPUSH command and get expected result",
args: args{
key: "key1",
presetValue: []string{"1", "2", "3"},
command: []string{"LPUSH", "key1", "4", "5", "6", "7", "8", "9", "10"},
},
wantRes: 10,
wantErr: nil,
},
{
name: "2 Expect error when trying to execute non-existent command",
args: args{
key: "key2",
presetValue: nil,
command: []string{"NON-EXISTENT", "key1", "key2"},
},
wantRes: 0,
wantErr: errors.New("command NON-EXISTENT not supported"),
},
}
for _, tt := range tests {
server := createEchoVault()
t.Run(tt.name, func(t *testing.T) {
if tt.args.presetValue != nil {
_, _ = server.LPush(tt.args.key, tt.args.presetValue...)
}
b, err := server.ExecuteCommand(tt.args.command)
if tt.wantErr != nil {
if err.Error() != tt.wantErr.Error() {
t.Errorf("ExecuteCommand() error = %v, wantErr %v", err, tt.wantErr)
}
}
r := resp.NewReader(bytes.NewReader(b))
v, _, _ := r.ReadValue()
if v.Integer() != tt.wantRes {
fmt.Println("RES: ", string(b))
t.Errorf("ExecuteCommand() response = %d, wantRes %d", v.Integer(), tt.wantRes)
}
})
}
}
func TestEchoVault_RemoveCommand(t *testing.T) {
type args struct {
removeCommand []string
executeCommand []string
}
tests := []struct {
name string
args args
wantErr error
}{
{
name: "1 Remove command and expect error when the command is called",
args: args{
removeCommand: []string{"LPUSH"},
executeCommand: []string{"LPUSH", "key", "item"},
},
wantErr: errors.New("command LPUSH not supported"),
},
{
name: "2 Remove sub-command and expect error when the subcommand is called",
args: args{
removeCommand: []string{"ACL", "CAT"},
executeCommand: []string{"ACL", "CAT"},
},
wantErr: errors.New("command ACL CAT not supported"),
},
{
name: "3 Remove sub-command and expect successful response from calling another subcommand",
args: args{
removeCommand: []string{"ACL", "WHOAMI"},
executeCommand: []string{"ACL", "DELUSER", "user-one"},
},
wantErr: nil,
},
}
for _, tt := range tests {
server := createEchoVault()
t.Run(tt.name, func(t *testing.T) {
server.RemoveCommand(tt.args.removeCommand...)
_, err := server.ExecuteCommand(tt.args.executeCommand)
if tt.wantErr != nil {
if err.Error() != tt.wantErr.Error() {
t.Errorf("RemoveCommand() error = %v, wantErr %v", err, tt.wantErr)
}
}
})
}
}

View File

@@ -436,8 +436,8 @@ func TestEchoVault_LPUSH(t *testing.T) {
key string
values []string
presetValue interface{}
lpushFunc func(key string, values ...string) (string, error)
want string
lpushFunc func(key string, values ...string) (int, error)
want int
wantErr bool
}{
{
@@ -447,7 +447,7 @@ func TestEchoVault_LPUSH(t *testing.T) {
key: "key1",
values: []string{"value1", "value2"},
lpushFunc: server.LPushX,
want: "OK",
want: 6,
wantErr: false,
},
{
@@ -457,7 +457,7 @@ func TestEchoVault_LPUSH(t *testing.T) {
key: "key2",
values: []string{"value1", "value2"},
lpushFunc: server.LPush,
want: "OK",
want: 6,
wantErr: false,
},
{
@@ -467,7 +467,7 @@ func TestEchoVault_LPUSH(t *testing.T) {
key: "key3",
values: []string{"value1", "value2"},
lpushFunc: server.LPush,
want: "OK",
want: 2,
wantErr: false,
},
{
@@ -477,7 +477,7 @@ func TestEchoVault_LPUSH(t *testing.T) {
key: "key4",
values: []string{"value1", "value2"},
lpushFunc: server.LPushX,
want: "",
want: 0,
wantErr: true,
},
}
@@ -511,8 +511,8 @@ func TestEchoVault_RPUSH(t *testing.T) {
key string
values []string
presetValue interface{}
rpushFunc func(key string, values ...string) (string, error)
want string
rpushFunc func(key string, values ...string) (int, error)
want int
wantErr bool
}{
{
@@ -522,7 +522,7 @@ func TestEchoVault_RPUSH(t *testing.T) {
key: "key1",
values: []string{"value1", "value2"},
rpushFunc: server.RPush,
want: "OK",
want: 2,
wantErr: false,
},
{
@@ -532,7 +532,7 @@ func TestEchoVault_RPUSH(t *testing.T) {
key: "key2",
values: []string{"value1", "value2"},
rpushFunc: server.RPushX,
want: "",
want: 0,
wantErr: true,
},
}

View File

@@ -1241,7 +1241,7 @@ func Test_HandleLPUSH(t *testing.T) {
key string
presetValue interface{}
command []string
expectedResponse interface{}
expectedResponse int
expectedValue []interface{}
expectedError error
}{
@@ -1251,7 +1251,7 @@ func Test_HandleLPUSH(t *testing.T) {
key: "LpushKey1",
presetValue: []interface{}{"1", "2", "4", "5"},
command: []string{"LPUSHX", "LpushKey1", "value1", "value2"},
expectedResponse: "OK",
expectedResponse: 6,
expectedValue: []interface{}{"value1", "value2", "1", "2", "4", "5"},
expectedError: nil,
},
@@ -1261,7 +1261,7 @@ func Test_HandleLPUSH(t *testing.T) {
key: "LpushKey2",
presetValue: []interface{}{"1", "2", "4", "5"},
command: []string{"LPUSH", "LpushKey2", "value1", "value2"},
expectedResponse: "OK",
expectedResponse: 6,
expectedValue: []interface{}{"value1", "value2", "1", "2", "4", "5"},
expectedError: nil,
},
@@ -1271,7 +1271,7 @@ func Test_HandleLPUSH(t *testing.T) {
key: "LpushKey3",
presetValue: nil,
command: []string{"LPUSH", "LpushKey3", "value1", "value2"},
expectedResponse: "OK",
expectedResponse: 2,
expectedValue: []interface{}{"value1", "value2"},
expectedError: nil,
},
@@ -1281,7 +1281,7 @@ func Test_HandleLPUSH(t *testing.T) {
key: "LpushKey5",
presetValue: nil,
command: []string{"LPUSH", "LpushKey5"},
expectedResponse: nil,
expectedResponse: 0,
expectedValue: nil,
expectedError: errors.New(constants.WrongArgsResponse),
},
@@ -1291,7 +1291,7 @@ func Test_HandleLPUSH(t *testing.T) {
key: "LpushKey6",
presetValue: nil,
command: []string{"LPUSHX", "LpushKey7", "count", "value1"},
expectedResponse: nil,
expectedResponse: 0,
expectedValue: nil,
expectedError: errors.New("LPUSHX command on non-list item"),
},
@@ -1329,8 +1329,8 @@ func Test_HandleLPUSH(t *testing.T) {
if err != nil {
t.Error(err)
}
if rv.String() != test.expectedResponse {
t.Errorf("expected \"%s\" response, got \"%s\"", test.expectedResponse, rv.String())
if rv.Integer() != test.expectedResponse {
t.Errorf("expected \"%d\" response, got \"%s\"", test.expectedResponse, rv.String())
}
if _, err = mockServer.KeyRLock(ctx, test.key); err != nil {
t.Error(err)
@@ -1359,7 +1359,7 @@ func Test_HandleRPUSH(t *testing.T) {
key string
presetValue interface{}
command []string
expectedResponse interface{}
expectedResponse int
expectedValue []interface{}
expectedError error
}{
@@ -1369,7 +1369,7 @@ func Test_HandleRPUSH(t *testing.T) {
key: "RpushKey1",
presetValue: []interface{}{"1", "2", "4", "5"},
command: []string{"RPUSHX", "RpushKey1", "value1", "value2"},
expectedResponse: "OK",
expectedResponse: 6,
expectedValue: []interface{}{"1", "2", "4", "5", "value1", "value2"},
expectedError: nil,
},
@@ -1379,7 +1379,7 @@ func Test_HandleRPUSH(t *testing.T) {
key: "RpushKey2",
presetValue: []interface{}{"1", "2", "4", "5"},
command: []string{"RPUSH", "RpushKey2", "value1", "value2"},
expectedResponse: "OK",
expectedResponse: 6,
expectedValue: []interface{}{"1", "2", "4", "5", "value1", "value2"},
expectedError: nil,
},
@@ -1389,7 +1389,7 @@ func Test_HandleRPUSH(t *testing.T) {
key: "RpushKey3",
presetValue: nil,
command: []string{"RPUSH", "RpushKey3", "value1", "value2"},
expectedResponse: "OK",
expectedResponse: 2,
expectedValue: []interface{}{"value1", "value2"},
expectedError: nil,
},
@@ -1399,7 +1399,7 @@ func Test_HandleRPUSH(t *testing.T) {
key: "RpushKey5",
presetValue: nil,
command: []string{"RPUSH", "RpushKey5"},
expectedResponse: nil,
expectedResponse: 0,
expectedValue: nil,
expectedError: errors.New(constants.WrongArgsResponse),
},
@@ -1409,7 +1409,7 @@ func Test_HandleRPUSH(t *testing.T) {
key: "RpushKey6",
presetValue: nil,
command: []string{"RPUSHX", "RpushKey7", "count", "value1"},
expectedResponse: nil,
expectedResponse: 0,
expectedValue: nil,
expectedError: errors.New("RPUSHX command on non-list item"),
},
@@ -1447,8 +1447,8 @@ func Test_HandleRPUSH(t *testing.T) {
if err != nil {
t.Error(err)
}
if rv.String() != test.expectedResponse {
t.Errorf("expected \"%s\" response, got \"%s\"", test.expectedResponse, rv.String())
if rv.Integer() != test.expectedResponse {
t.Errorf("expected \"%d\" response, got \"%s\"", test.expectedResponse, rv.String())
}
if _, err = mockServer.KeyRLock(ctx, test.key); err != nil {
t.Error(err)