api_test.go raw

   1  package api
   2  
   3  import (
   4  	"context"
   5  	"fmt"
   6  	"testing"
   7  
   8  	"github.com/stretchr/testify/mock"
   9  	"github.com/stretchr/testify/require"
  10  
  11  	"github.com/getAlby/hub/lnclient"
  12  	"github.com/getAlby/hub/service"
  13  	"github.com/getAlby/hub/tests/mocks"
  14  )
  15  
  16  func TestGetCustomNodeCommandDefinitions(t *testing.T) {
  17  	lnClient := mocks.NewMockLNClient(t)
  18  	svc := mocks.NewMockService(t)
  19  
  20  	mockLNCommandDefs := []lnclient.CustomNodeCommandDef{
  21  		{
  22  			Name:        "no_args",
  23  			Description: "command without args",
  24  			Args:        nil,
  25  		},
  26  		{
  27  			Name:        "with_args",
  28  			Description: "command with args",
  29  			Args: []lnclient.CustomNodeCommandArgDef{
  30  				{Name: "arg1", Description: "first argument"},
  31  				{Name: "arg2", Description: "second argument"},
  32  			},
  33  		},
  34  	}
  35  
  36  	expectedCommands := []CustomNodeCommandDef{
  37  		{
  38  			Name:        "no_args",
  39  			Description: "command without args",
  40  			Args:        []CustomNodeCommandArgDef{},
  41  		},
  42  		{
  43  			Name:        "with_args",
  44  			Description: "command with args",
  45  			Args: []CustomNodeCommandArgDef{
  46  				{Name: "arg1", Description: "first argument"},
  47  				{Name: "arg2", Description: "second argument"},
  48  			},
  49  		},
  50  	}
  51  
  52  	lnClient.On("GetCustomNodeCommandDefinitions").Return(mockLNCommandDefs)
  53  	svc.On("GetLNClient").Return(lnClient)
  54  
  55  	theAPI := instantiateAPIWithService(svc)
  56  
  57  	commands, err := theAPI.GetCustomNodeCommands()
  58  	require.NoError(t, err)
  59  	require.NotNil(t, commands)
  60  	require.ElementsMatch(t, expectedCommands, commands.Commands)
  61  }
  62  
  63  func TestExecuteCustomNodeCommand(t *testing.T) {
  64  	type testCase struct {
  65  		name                 string
  66  		apiCommandLine       string
  67  		lnSupportedCommands  []lnclient.CustomNodeCommandDef
  68  		lnExpectedCommandReq *lnclient.CustomNodeCommandRequest
  69  		lnResponse           *lnclient.CustomNodeCommandResponse
  70  		lnError              error
  71  		apiExpectedResponse  interface{}
  72  		apiExpectedErr       string
  73  	}
  74  
  75  	// Successful execution of a command without args.
  76  	testCaseOkNoArgs := testCase{
  77  		name:                 "command without args",
  78  		apiCommandLine:       "test_command",
  79  		lnSupportedCommands:  []lnclient.CustomNodeCommandDef{{Name: "test_command"}},
  80  		lnExpectedCommandReq: &lnclient.CustomNodeCommandRequest{Name: "test_command", Args: []lnclient.CustomNodeCommandArg{}},
  81  		lnResponse:           &lnclient.CustomNodeCommandResponse{Response: "ok"},
  82  		lnError:              nil,
  83  		apiExpectedResponse:  "ok",
  84  		apiExpectedErr:       "",
  85  	}
  86  
  87  	// Successful execution of a command with args. The command line contains
  88  	// different arg value styles: with '=' and with space.
  89  	testCaseOkWithArgs := testCase{
  90  		name:           "command with args",
  91  		apiCommandLine: "test_command --arg1=foo --arg2 bar",
  92  		lnSupportedCommands: []lnclient.CustomNodeCommandDef{
  93  			{
  94  				Name: "test_command",
  95  				Args: []lnclient.CustomNodeCommandArgDef{
  96  					{Name: "arg1", Description: "argument one"},
  97  					{Name: "arg2", Description: "argument two"},
  98  				},
  99  			},
 100  		},
 101  		lnExpectedCommandReq: &lnclient.CustomNodeCommandRequest{Name: "test_command", Args: []lnclient.CustomNodeCommandArg{
 102  			{Name: "arg1", Value: "foo"},
 103  			{Name: "arg2", Value: "bar"},
 104  		}},
 105  		lnResponse:          &lnclient.CustomNodeCommandResponse{Response: "ok"},
 106  		lnError:             nil,
 107  		apiExpectedResponse: "ok",
 108  		apiExpectedErr:      "",
 109  	}
 110  
 111  	// Successful execution of a command with a possible but unset arg.
 112  	testCaseOkWithUnsetArg := testCase{
 113  		name:           "command with unset arg",
 114  		apiCommandLine: "test_command",
 115  		lnSupportedCommands: []lnclient.CustomNodeCommandDef{
 116  			{Name: "test_command", Args: []lnclient.CustomNodeCommandArgDef{{Name: "arg1", Description: "argument one"}}},
 117  		},
 118  		lnExpectedCommandReq: &lnclient.CustomNodeCommandRequest{Name: "test_command", Args: []lnclient.CustomNodeCommandArg{}},
 119  		lnResponse:           &lnclient.CustomNodeCommandResponse{Response: "ok"},
 120  		lnError:              nil,
 121  		apiExpectedResponse:  "ok",
 122  		apiExpectedErr:       "",
 123  	}
 124  
 125  	// Error: command line is empty.
 126  	testCaseErrEmptyCommand := testCase{
 127  		name:                 "empty command",
 128  		apiCommandLine:       "",
 129  		lnSupportedCommands:  nil,
 130  		lnExpectedCommandReq: nil,
 131  		lnResponse:           nil,
 132  		lnError:              nil,
 133  		apiExpectedResponse:  nil,
 134  		apiExpectedErr:       "no command provided",
 135  	}
 136  
 137  	// Error: command line is malformed, i.e. non-parseable.
 138  	testCaseErrMalformedCommand := testCase{
 139  		name:                 "command with unclosed quote",
 140  		apiCommandLine:       "test_command\"",
 141  		lnSupportedCommands:  nil,
 142  		lnExpectedCommandReq: nil,
 143  		lnResponse:           nil,
 144  		lnError:              nil,
 145  		apiExpectedResponse:  nil,
 146  		apiExpectedErr:       "failed to parse node command",
 147  	}
 148  
 149  	// Error: node does not support this command.
 150  	testCaseErrUnknownCommand := testCase{
 151  		name:                 "unknown command",
 152  		apiCommandLine:       "test_command_unknown",
 153  		lnSupportedCommands:  []lnclient.CustomNodeCommandDef{{Name: "test_command"}},
 154  		lnExpectedCommandReq: nil,
 155  		lnResponse:           nil,
 156  		lnError:              nil,
 157  		apiExpectedResponse:  nil,
 158  		apiExpectedErr:       "unknown command",
 159  	}
 160  
 161  	// Error: unsupported command argument.
 162  	testCaseErrUnknownArg := testCase{
 163  		name:                 "unknown argument",
 164  		apiCommandLine:       "test_command --unknown=fail",
 165  		lnSupportedCommands:  []lnclient.CustomNodeCommandDef{{Name: "test_command"}},
 166  		lnExpectedCommandReq: nil,
 167  		lnResponse:           nil,
 168  		lnError:              nil,
 169  		apiExpectedResponse:  nil,
 170  		apiExpectedErr:       "flag provided but not defined: -unknown",
 171  	}
 172  
 173  	// Error: the command is valid but the node fails to execute it.
 174  	testCaseErrNodeFailed := testCase{
 175  		name:                 "node failed to execute command",
 176  		apiCommandLine:       "test_command",
 177  		lnSupportedCommands:  []lnclient.CustomNodeCommandDef{{Name: "test_command"}},
 178  		lnExpectedCommandReq: &lnclient.CustomNodeCommandRequest{Name: "test_command", Args: []lnclient.CustomNodeCommandArg{}},
 179  		lnResponse:           nil,
 180  		lnError:              fmt.Errorf("utter failure"),
 181  		apiExpectedResponse:  nil,
 182  		apiExpectedErr:       "utter failure",
 183  	}
 184  
 185  	testCases := []testCase{
 186  		testCaseOkNoArgs,
 187  		testCaseOkWithArgs,
 188  		testCaseOkWithUnsetArg,
 189  		testCaseErrEmptyCommand,
 190  		testCaseErrMalformedCommand,
 191  		testCaseErrUnknownCommand,
 192  		testCaseErrUnknownArg,
 193  		testCaseErrNodeFailed,
 194  	}
 195  
 196  	for _, tc := range testCases {
 197  		t.Run(tc.name, func(t *testing.T) {
 198  			lnClient := mocks.NewMockLNClient(t)
 199  			svc := mocks.NewMockService(t)
 200  
 201  			if tc.lnSupportedCommands != nil {
 202  				lnClient.On("GetCustomNodeCommandDefinitions").Return(tc.lnSupportedCommands)
 203  			}
 204  
 205  			if tc.lnExpectedCommandReq != nil {
 206  				lnClient.On("ExecuteCustomNodeCommand", mock.Anything, tc.lnExpectedCommandReq).Return(tc.lnResponse, tc.lnError)
 207  			}
 208  
 209  			svc.On("GetLNClient").Return(lnClient)
 210  
 211  			theAPI := instantiateAPIWithService(svc)
 212  
 213  			response, err := theAPI.ExecuteCustomNodeCommand(context.TODO(), tc.apiCommandLine)
 214  			require.Equal(t, tc.apiExpectedResponse, response)
 215  			if tc.apiExpectedErr == "" {
 216  				require.NoError(t, err)
 217  			} else {
 218  				require.ErrorContains(t, err, tc.apiExpectedErr)
 219  			}
 220  		})
 221  	}
 222  }
 223  
 224  // instantiateAPIWithService is a helper function that returns a partially
 225  // constructed API instance. It is only suitable for the simplest of test cases.
 226  func instantiateAPIWithService(s service.Service) *api {
 227  	return &api{svc: s}
 228  }
 229