// Copyright 2024 Kelvin Clement Mwinuka // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package str import ( "bytes" "context" "errors" "fmt" "github.com/echovault/echovault/internal" "github.com/echovault/echovault/internal/config" "github.com/echovault/echovault/pkg/constants" "github.com/echovault/echovault/pkg/echovault" str "github.com/echovault/echovault/pkg/modules/string" "github.com/echovault/echovault/pkg/types" "github.com/tidwall/resp" "net" "strconv" "strings" "testing" ) var mockServer *echovault.EchoVault func init() { mockServer, _ = echovault.NewEchoVault( echovault.WithCommands(str.Commands()), echovault.WithConfig(config.Config{ DataDir: "", EvictionPolicy: constants.NoEviction, }), ) } func getHandler(commands ...string) types.HandlerFunc { if len(commands) == 0 { return nil } for _, c := range mockServer.GetAllCommands() { if strings.EqualFold(commands[0], c.Command) && len(commands) == 1 { // Get command handler return c.HandlerFunc } if strings.EqualFold(commands[0], c.Command) { // Get sub-command handler for _, sc := range c.SubCommands { if strings.EqualFold(commands[1], sc.Command) { return sc.HandlerFunc } } } } return nil } func getHandlerFuncParams(ctx context.Context, cmd []string, conn *net.Conn) types.HandlerFuncParams { return types.HandlerFuncParams{ Context: ctx, Command: cmd, Connection: conn, KeyExists: mockServer.KeyExists, CreateKeyAndLock: mockServer.CreateKeyAndLock, KeyLock: mockServer.KeyLock, KeyRLock: mockServer.KeyRLock, KeyUnlock: mockServer.KeyUnlock, KeyRUnlock: mockServer.KeyRUnlock, GetValue: mockServer.GetValue, SetValue: mockServer.SetValue, } } func Test_HandleSetRange(t *testing.T) { tests := []struct { name string preset bool key string presetValue string command []string expectedValue string expectedResponse int expectedError error }{ { name: "Test that SETRANGE on non-existent string creates new string", preset: false, key: "SetRangeKey1", presetValue: "", command: []string{"SETRANGE", "SetRangeKey1", "10", "New String Value"}, expectedValue: "New String Value", expectedResponse: len("New String Value"), expectedError: nil, }, { name: "Test SETRANGE with an offset that leads to a longer resulting string", preset: true, key: "SetRangeKey2", presetValue: "Original String Value", command: []string{"SETRANGE", "SetRangeKey2", "16", "Portion Replaced With This New String"}, expectedValue: "Original String Portion Replaced With This New String", expectedResponse: len("Original String Portion Replaced With This New String"), expectedError: nil, }, { name: "SETRANGE with negative offset prepends the string", preset: true, key: "SetRangeKey3", presetValue: "This is a preset value", command: []string{"SETRANGE", "SetRangeKey3", "-10", "Prepended "}, expectedValue: "Prepended This is a preset value", expectedResponse: len("Prepended This is a preset value"), expectedError: nil, }, { name: "SETRANGE with offset that embeds new string inside the old string", preset: true, key: "SetRangeKey4", presetValue: "This is a preset value", command: []string{"SETRANGE", "SetRangeKey4", "0", "That"}, expectedValue: "That is a preset value", expectedResponse: len("That is a preset value"), expectedError: nil, }, { name: "SETRANGE with offset longer than original lengths appends the string", preset: true, key: "SetRangeKey5", presetValue: "This is a preset value", command: []string{"SETRANGE", "SetRangeKey5", "100", " Appended"}, expectedValue: "This is a preset value Appended", expectedResponse: len("This is a preset value Appended"), expectedError: nil, }, { name: "SETRANGE with offset on the last character replaces last character with new string", preset: true, key: "SetRangeKey6", presetValue: "This is a preset value", command: []string{"SETRANGE", "SetRangeKey6", strconv.Itoa(len("This is a preset value") - 1), " replaced"}, expectedValue: "This is a preset valu replaced", expectedResponse: len("This is a preset valu replaced"), expectedError: nil, }, { name: " Offset not integer", preset: false, command: []string{"SETRANGE", "key", "offset", "value"}, expectedResponse: 0, expectedError: errors.New("offset must be an integer"), }, { name: "SETRANGE target is not a string", preset: true, key: "test-int", presetValue: "10", command: []string{"SETRANGE", "test-int", "10", "value"}, expectedResponse: 0, expectedError: errors.New("value at key test-int is not a string"), }, { name: "Command too short", preset: false, command: []string{"SETRANGE", "key"}, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "Command too long", preset: false, command: []string{"SETRANGE", "key", "offset", "value", "value1"}, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SETRANGE, %d", i)) // If there's a preset step, carry it out here if test.preset { if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { t.Error(err) } if err := mockServer.SetValue(ctx, test.key, internal.AdaptType(test.presetValue)); err != nil { t.Error(err) } mockServer.KeyUnlock(ctx, test.key) } handler := getHandler(test.command[0]) if handler == nil { t.Errorf("no handler found for command %s", test.command[0]) return } res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { if err.Error() != test.expectedError.Error() { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } if err != nil { t.Error(err) } rd := resp.NewReader(bytes.NewBuffer(res)) rv, _, err := rd.ReadValue() if err != nil { t.Error(err) } if rv.Integer() != test.expectedResponse { t.Errorf("expected response \"%d\", got \"%d\"", test.expectedResponse, rv.Integer()) } // Get the value from the echovault and check against the expected value if _, err = mockServer.KeyRLock(ctx, test.key); err != nil { t.Error(err) } value, ok := mockServer.GetValue(ctx, test.key).(string) if !ok { t.Error("expected string data type, got another type") } if value != test.expectedValue { t.Errorf("expected value \"%s\", got \"%s\"", test.expectedValue, value) } mockServer.KeyRUnlock(ctx, test.key) }) } } func Test_HandleStrLen(t *testing.T) { tests := []struct { name string preset bool key string presetValue string command []string expectedResponse int expectedError error }{ { name: "Return the correct string length for an existing string", preset: true, key: "StrLenKey1", presetValue: "Test String", command: []string{"STRLEN", "StrLenKey1"}, expectedResponse: len("Test String"), expectedError: nil, }, { name: "If the string does not exist, return 0", preset: false, key: "StrLenKey2", presetValue: "", command: []string{"STRLEN", "StrLenKey2"}, expectedResponse: 0, expectedError: nil, }, { name: "Too few args", preset: false, key: "StrLenKey3", presetValue: "", command: []string{"STRLEN"}, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "Too many args", preset: false, key: "StrLenKey4", presetValue: "", command: []string{"STRLEN", "StrLenKey4", "StrLenKey5"}, expectedResponse: 0, expectedError: errors.New(constants.WrongArgsResponse), }, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("STRLEN, %d", i)) if test.preset { _, err := mockServer.CreateKeyAndLock(ctx, test.key) if err != nil { t.Error(err) } if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { t.Error(err) } mockServer.KeyUnlock(ctx, test.key) } handler := getHandler(test.command[0]) if handler == nil { t.Errorf("no handler found for command %s", test.command[0]) return } res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { if err.Error() != test.expectedError.Error() { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } rd := resp.NewReader(bytes.NewBuffer(res)) rv, _, err := rd.ReadValue() if err != nil { t.Error(err) } if rv.Integer() != test.expectedResponse { t.Errorf("expected respons \"%d\", got \"%d\"", test.expectedResponse, rv.Integer()) } }) } } func Test_HandleSubStr(t *testing.T) { tests := []struct { name string preset bool key string presetValue string command []string expectedResponse string expectedError error }{ { name: "Return substring within the range of the string", preset: true, key: "SubStrKey1", presetValue: "Test String One", command: []string{"SUBSTR", "SubStrKey1", "5", "10"}, expectedResponse: "String", expectedError: nil, }, { name: "Return substring at the end of the string with exact end index", preset: true, key: "SubStrKey2", presetValue: "Test String Two", command: []string{"SUBSTR", "SubStrKey2", "12", "14"}, expectedResponse: "Two", expectedError: nil, }, { name: "Return substring at the end of the string with end index greater than length", preset: true, key: "SubStrKey3", presetValue: "Test String Three", command: []string{"SUBSTR", "SubStrKey3", "12", "75"}, expectedResponse: "Three", expectedError: nil, }, { name: "Return the substring at the start of the string with 0 start index", preset: true, key: "SubStrKey4", presetValue: "Test String Four", command: []string{"SUBSTR", "SubStrKey4", "0", "3"}, expectedResponse: "Test", expectedError: nil, }, { // Return the substring with negative start index. // Substring should begin abs(start) from the end of the string when start is negative. name: "Return the substring with negative start index", preset: true, key: "SubStrKey5", presetValue: "Test String Five", command: []string{"SUBSTR", "SubStrKey5", "-11", "10"}, expectedResponse: "String", expectedError: nil, }, { // Return reverse substring with end index smaller than start index. // When end index is smaller than start index, the 2 indices are reversed. name: "Return reverse substring with end index smaller than start index", preset: true, key: "SubStrKey6", presetValue: "Test String Six", command: []string{"SUBSTR", "SubStrKey6", "4", "0"}, expectedResponse: "tseT", expectedError: nil, }, { name: "Command too short", command: []string{"SUBSTR", "key", "10"}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "Command too long", command: []string{"SUBSTR", "key", "10", "15", "20"}, expectedError: errors.New(constants.WrongArgsResponse), }, { name: "Start index is not an integer", command: []string{"SUBSTR", "key", "start", "10"}, expectedError: errors.New("start and end indices must be integers"), }, { name: "End index is not an integer", command: []string{"SUBSTR", "key", "0", "end"}, expectedError: errors.New("start and end indices must be integers"), }, { name: "Non-existent key", command: []string{"SUBSTR", "non-existent-key", "0", "10"}, expectedError: errors.New("key non-existent-key does not exist"), }, } for i, test := range tests { t.Run(test.name, func(t *testing.T) { ctx := context.WithValue(context.Background(), "test_name", fmt.Sprintf("SUBSTR, %d", i)) if test.preset { if _, err := mockServer.CreateKeyAndLock(ctx, test.key); err != nil { t.Error(err) } if err := mockServer.SetValue(ctx, test.key, test.presetValue); err != nil { t.Error(err) } mockServer.KeyUnlock(ctx, test.key) } handler := getHandler(test.command[0]) if handler == nil { t.Errorf("no handler found for command %s", test.command[0]) return } res, err := handler(getHandlerFuncParams(ctx, test.command, nil)) if test.expectedError != nil { if err.Error() != test.expectedError.Error() { t.Errorf("expected error \"%s\", got \"%s\"", test.expectedError.Error(), err.Error()) } return } rd := resp.NewReader(bytes.NewBuffer(res)) rv, _, err := rd.ReadValue() if err != nil { t.Error(err) } if rv.String() != test.expectedResponse { t.Errorf("expected response \"%s\", got \"%s\"", test.expectedResponse, rv.String()) } }) } }