alby-hub/api/api_test.go
Roman D d799bdff76
feat: proposal: custom node commands (#1007)
* feat: custom node command execution models and methods

* feat: implement custom node command handlers for HTTP server and Wails

* chore: expose GetNodeCommands API methods in HTTP and Wails

* chore: add sample custom node command implementation for Cashu restore

* feat: add frontend for custom node commands

* chore: consistent naming of custom node command entities

* chore: consistent naming of custom node command entities

* chore: return interface{} as custom node command execution result

* test: add tests for ParseCommandLine

* chore: add extra ParseCommandLine tests for json

* test: add API tests and mocks

* fix: stabilize order of arguments when invoking custom node commands

* test: add test for unsupported custom node command argument

* chore: add the mockery configuration file and move mocks to tests/mocks

* docs: document mockery usage

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
2025-01-30 18:20:17 +07:00

228 lines
7.2 KiB
Go

package api
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/getAlby/hub/lnclient"
"github.com/getAlby/hub/service"
"github.com/getAlby/hub/tests/mocks"
)
func TestGetCustomNodeCommandDefinitions(t *testing.T) {
lnClient := mocks.NewMockLNClient(t)
svc := mocks.NewMockService(t)
mockLNCommandDefs := []lnclient.CustomNodeCommandDef{
{
Name: "no_args",
Description: "command without args",
Args: nil,
},
{
Name: "with_args",
Description: "command with args",
Args: []lnclient.CustomNodeCommandArgDef{
{Name: "arg1", Description: "first argument"},
{Name: "arg2", Description: "second argument"},
},
},
}
expectedCommands := []CustomNodeCommandDef{
{
Name: "no_args",
Description: "command without args",
Args: []CustomNodeCommandArgDef{},
},
{
Name: "with_args",
Description: "command with args",
Args: []CustomNodeCommandArgDef{
{Name: "arg1", Description: "first argument"},
{Name: "arg2", Description: "second argument"},
},
},
}
lnClient.On("GetCustomNodeCommandDefinitions").Return(mockLNCommandDefs)
svc.On("GetLNClient").Return(lnClient)
theAPI := instantiateAPIWithService(svc)
commands, err := theAPI.GetCustomNodeCommands()
require.NoError(t, err)
require.NotNil(t, commands)
require.ElementsMatch(t, expectedCommands, commands.Commands)
}
func TestExecuteCustomNodeCommand(t *testing.T) {
type testCase struct {
name string
apiCommandLine string
lnSupportedCommands []lnclient.CustomNodeCommandDef
lnExpectedCommandReq *lnclient.CustomNodeCommandRequest
lnResponse *lnclient.CustomNodeCommandResponse
lnError error
apiExpectedResponse interface{}
apiExpectedErr string
}
// Successful execution of a command without args.
testCaseOkNoArgs := testCase{
name: "command without args",
apiCommandLine: "test_command",
lnSupportedCommands: []lnclient.CustomNodeCommandDef{{Name: "test_command"}},
lnExpectedCommandReq: &lnclient.CustomNodeCommandRequest{Name: "test_command", Args: []lnclient.CustomNodeCommandArg{}},
lnResponse: &lnclient.CustomNodeCommandResponse{Response: "ok"},
lnError: nil,
apiExpectedResponse: "ok",
apiExpectedErr: "",
}
// Successful execution of a command with args. The command line contains
// different arg value styles: with '=' and with space.
testCaseOkWithArgs := testCase{
name: "command with args",
apiCommandLine: "test_command --arg1=foo --arg2 bar",
lnSupportedCommands: []lnclient.CustomNodeCommandDef{
{
Name: "test_command",
Args: []lnclient.CustomNodeCommandArgDef{
{Name: "arg1", Description: "argument one"},
{Name: "arg2", Description: "argument two"},
},
},
},
lnExpectedCommandReq: &lnclient.CustomNodeCommandRequest{Name: "test_command", Args: []lnclient.CustomNodeCommandArg{
{Name: "arg1", Value: "foo"},
{Name: "arg2", Value: "bar"},
}},
lnResponse: &lnclient.CustomNodeCommandResponse{Response: "ok"},
lnError: nil,
apiExpectedResponse: "ok",
apiExpectedErr: "",
}
// Successful execution of a command with a possible but unset arg.
testCaseOkWithUnsetArg := testCase{
name: "command with unset arg",
apiCommandLine: "test_command",
lnSupportedCommands: []lnclient.CustomNodeCommandDef{
{Name: "test_command", Args: []lnclient.CustomNodeCommandArgDef{{Name: "arg1", Description: "argument one"}}},
},
lnExpectedCommandReq: &lnclient.CustomNodeCommandRequest{Name: "test_command", Args: []lnclient.CustomNodeCommandArg{}},
lnResponse: &lnclient.CustomNodeCommandResponse{Response: "ok"},
lnError: nil,
apiExpectedResponse: "ok",
apiExpectedErr: "",
}
// Error: command line is empty.
testCaseErrEmptyCommand := testCase{
name: "empty command",
apiCommandLine: "",
lnSupportedCommands: nil,
lnExpectedCommandReq: nil,
lnResponse: nil,
lnError: nil,
apiExpectedResponse: nil,
apiExpectedErr: "no command provided",
}
// Error: command line is malformed, i.e. non-parseable.
testCaseErrMalformedCommand := testCase{
name: "command with unclosed quote",
apiCommandLine: "test_command\"",
lnSupportedCommands: nil,
lnExpectedCommandReq: nil,
lnResponse: nil,
lnError: nil,
apiExpectedResponse: nil,
apiExpectedErr: "failed to parse node command",
}
// Error: node does not support this command.
testCaseErrUnknownCommand := testCase{
name: "unknown command",
apiCommandLine: "test_command_unknown",
lnSupportedCommands: []lnclient.CustomNodeCommandDef{{Name: "test_command"}},
lnExpectedCommandReq: nil,
lnResponse: nil,
lnError: nil,
apiExpectedResponse: nil,
apiExpectedErr: "unknown command",
}
// Error: unsupported command argument.
testCaseErrUnknownArg := testCase{
name: "unknown argument",
apiCommandLine: "test_command --unknown=fail",
lnSupportedCommands: []lnclient.CustomNodeCommandDef{{Name: "test_command"}},
lnExpectedCommandReq: nil,
lnResponse: nil,
lnError: nil,
apiExpectedResponse: nil,
apiExpectedErr: "flag provided but not defined: -unknown",
}
// Error: the command is valid but the node fails to execute it.
testCaseErrNodeFailed := testCase{
name: "node failed to execute command",
apiCommandLine: "test_command",
lnSupportedCommands: []lnclient.CustomNodeCommandDef{{Name: "test_command"}},
lnExpectedCommandReq: &lnclient.CustomNodeCommandRequest{Name: "test_command", Args: []lnclient.CustomNodeCommandArg{}},
lnResponse: nil,
lnError: fmt.Errorf("utter failure"),
apiExpectedResponse: nil,
apiExpectedErr: "utter failure",
}
testCases := []testCase{
testCaseOkNoArgs,
testCaseOkWithArgs,
testCaseOkWithUnsetArg,
testCaseErrEmptyCommand,
testCaseErrMalformedCommand,
testCaseErrUnknownCommand,
testCaseErrUnknownArg,
testCaseErrNodeFailed,
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
lnClient := mocks.NewMockLNClient(t)
svc := mocks.NewMockService(t)
if tc.lnSupportedCommands != nil {
lnClient.On("GetCustomNodeCommandDefinitions").Return(tc.lnSupportedCommands)
}
if tc.lnExpectedCommandReq != nil {
lnClient.On("ExecuteCustomNodeCommand", mock.Anything, tc.lnExpectedCommandReq).Return(tc.lnResponse, tc.lnError)
}
svc.On("GetLNClient").Return(lnClient)
theAPI := instantiateAPIWithService(svc)
response, err := theAPI.ExecuteCustomNodeCommand(context.TODO(), tc.apiCommandLine)
require.Equal(t, tc.apiExpectedResponse, response)
if tc.apiExpectedErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tc.apiExpectedErr)
}
})
}
}
// instantiateAPIWithService is a helper function that returns a partially
// constructed API instance. It is only suitable for the simplest of test cases.
func instantiateAPIWithService(s service.Service) *api {
return &api{svc: s}
}