mirror of
https://github.com/EchoVault/SugarDB.git
synced 2025-10-06 00:16:53 +08:00
Instead of passing in EchoVault instance to commands handler, we now pass a struct of params containing all the variables and functions used within the handler function. This removes the modules' dependency on the echovault package. Moved string command and api tests to test/modules/string
This commit is contained in:
466
test/modules/string/commands_test.go
Normal file
466
test/modules/string/commands_test.go
Normal file
@@ -0,0 +1,466 @@
|
||||
// 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_test
|
||||
|
||||
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"
|
||||
"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(command string) types.HandlerFunc {
|
||||
for _, c := range mockServer.GetAllCommands() {
|
||||
if strings.EqualFold(command, c.Command) {
|
||||
return c.HandlerFunc
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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(types.HandlerFuncParams{
|
||||
Context: ctx,
|
||||
Command: test.command,
|
||||
Connection: nil,
|
||||
KeyExists: mockServer.KeyExists,
|
||||
CreateKeyAndLock: mockServer.CreateKeyAndLock,
|
||||
KeyLock: mockServer.KeyLock,
|
||||
KeyUnlock: mockServer.KeyUnlock,
|
||||
GetValue: mockServer.GetValue,
|
||||
SetValue: mockServer.SetValue,
|
||||
})
|
||||
|
||||
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(types.HandlerFuncParams{
|
||||
Context: ctx,
|
||||
Command: test.command,
|
||||
Connection: nil,
|
||||
KeyExists: mockServer.KeyExists,
|
||||
KeyRLock: mockServer.KeyRLock,
|
||||
KeyRUnlock: mockServer.KeyRUnlock,
|
||||
GetValue: mockServer.GetValue,
|
||||
SetValue: mockServer.SetValue,
|
||||
})
|
||||
|
||||
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(types.HandlerFuncParams{
|
||||
Context: ctx,
|
||||
Command: test.command,
|
||||
Connection: nil,
|
||||
KeyExists: mockServer.KeyExists,
|
||||
KeyRLock: mockServer.KeyRLock,
|
||||
KeyRUnlock: mockServer.KeyRUnlock,
|
||||
GetValue: mockServer.GetValue,
|
||||
SetValue: mockServer.SetValue,
|
||||
})
|
||||
|
||||
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())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user