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} }