mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
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>
This commit is contained in:
parent
82e4a162fb
commit
d799bdff76
21 changed files with 3195 additions and 7 deletions
16
.mockery.yaml
Normal file
16
.mockery.yaml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
filename: "{{.InterfaceName}}.go"
|
||||||
|
dir: tests/mocks
|
||||||
|
outpkg: mocks
|
||||||
|
|
||||||
|
# Fix deprecation warnings:
|
||||||
|
issue-845-fix: True
|
||||||
|
resolve-type-alias: False
|
||||||
|
|
||||||
|
packages:
|
||||||
|
github.com/getAlby/hub/service:
|
||||||
|
interfaces:
|
||||||
|
Service:
|
||||||
|
|
||||||
|
github.com/getAlby/hub/lnclient:
|
||||||
|
interfaces:
|
||||||
|
LNClient:
|
||||||
|
|
@ -95,6 +95,14 @@ _If you get a blank screen, try running in your normal terminal (outside of vsco
|
||||||
|
|
||||||
$ go test ./... -run TestHandleGetInfoEvent
|
$ go test ./... -run TestHandleGetInfoEvent
|
||||||
|
|
||||||
|
#### Mocking
|
||||||
|
|
||||||
|
We use [testify/mock](https://github.com/stretchr/testify) to facilitate mocking in tests. Instead of writing mocks manually, we generate them using [vektra/mockery](https://github.com/vektra/mockery). To regenerate them, [install mockery](https://vektra.github.io/mockery/latest/installation) and run it in the project's root directory:
|
||||||
|
|
||||||
|
$ mockery
|
||||||
|
|
||||||
|
Mockery loads its configuration from the .mockery.yaml file in the root directory of this project. To add mocks for new interfaces, add them to the configuration file and run mockery.
|
||||||
|
|
||||||
### Profiling
|
### Profiling
|
||||||
|
|
||||||
The application supports both the Go pprof library and the DataDog profiler.
|
The application supports both the Go pprof library and the DataDog profiler.
|
||||||
|
|
|
||||||
88
api/api.go
88
api/api.go
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -1068,6 +1069,93 @@ func (api *api) Health(ctx context.Context) (*HealthResponse, error) {
|
||||||
return &HealthResponse{Alarms: alarms}, nil
|
return &HealthResponse{Alarms: alarms}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *api) GetCustomNodeCommands() (*CustomNodeCommandsResponse, error) {
|
||||||
|
lnClient := api.svc.GetLNClient()
|
||||||
|
if lnClient == nil {
|
||||||
|
return nil, errors.New("LNClient not started")
|
||||||
|
}
|
||||||
|
|
||||||
|
allCommandDefs := lnClient.GetCustomNodeCommandDefinitions()
|
||||||
|
commandDefs := make([]CustomNodeCommandDef, 0, len(allCommandDefs))
|
||||||
|
for _, commandDef := range allCommandDefs {
|
||||||
|
argDefs := make([]CustomNodeCommandArgDef, 0, len(commandDef.Args))
|
||||||
|
for _, argDef := range commandDef.Args {
|
||||||
|
argDefs = append(argDefs, CustomNodeCommandArgDef{
|
||||||
|
Name: argDef.Name,
|
||||||
|
Description: argDef.Description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
commandDefs = append(commandDefs, CustomNodeCommandDef{
|
||||||
|
Name: commandDef.Name,
|
||||||
|
Description: commandDef.Description,
|
||||||
|
Args: argDefs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CustomNodeCommandsResponse{Commands: commandDefs}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *api) ExecuteCustomNodeCommand(ctx context.Context, command string) (interface{}, error) {
|
||||||
|
lnClient := api.svc.GetLNClient()
|
||||||
|
if lnClient == nil {
|
||||||
|
return nil, errors.New("LNClient not started")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split command line into arguments. Command name must be the first argument.
|
||||||
|
parsedArgs, err := utils.ParseCommandLine(command)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse node command: %w", err)
|
||||||
|
} else if len(parsedArgs) == 0 {
|
||||||
|
return nil, errors.New("no command provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the requested command definition.
|
||||||
|
allCommandDefs := lnClient.GetCustomNodeCommandDefinitions()
|
||||||
|
commandDefIdx := slices.IndexFunc(allCommandDefs, func(def lnclient.CustomNodeCommandDef) bool {
|
||||||
|
return def.Name == parsedArgs[0]
|
||||||
|
})
|
||||||
|
if commandDefIdx < 0 {
|
||||||
|
return nil, fmt.Errorf("unknown command: %q", parsedArgs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build flag set.
|
||||||
|
commandDef := allCommandDefs[commandDefIdx]
|
||||||
|
flagSet := flag.NewFlagSet(commandDef.Name, flag.ContinueOnError)
|
||||||
|
for _, argDef := range commandDef.Args {
|
||||||
|
flagSet.String(argDef.Name, "", argDef.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = flagSet.Parse(parsedArgs[1:]); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse command arguments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect flags that have been set.
|
||||||
|
argValues := make(map[string]string)
|
||||||
|
flagSet.Visit(func(f *flag.Flag) {
|
||||||
|
argValues[f.Name] = f.Value.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
reqArgs := make([]lnclient.CustomNodeCommandArg, 0, len(argValues))
|
||||||
|
for _, argDef := range commandDef.Args {
|
||||||
|
if argValue, ok := argValues[argDef.Name]; ok {
|
||||||
|
reqArgs = append(reqArgs, lnclient.CustomNodeCommandArg{
|
||||||
|
Name: argDef.Name,
|
||||||
|
Value: argValue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeResp, err := lnClient.ExecuteCustomNodeCommand(ctx, &lnclient.CustomNodeCommandRequest{
|
||||||
|
Name: commandDef.Name,
|
||||||
|
Args: reqArgs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("node failed to execute custom command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeResp.Response, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (api *api) parseExpiresAt(expiresAtString string) (*time.Time, error) {
|
func (api *api) parseExpiresAt(expiresAtString string) (*time.Time, error) {
|
||||||
var expiresAt *time.Time
|
var expiresAt *time.Time
|
||||||
if expiresAtString != "" {
|
if expiresAtString != "" {
|
||||||
|
|
|
||||||
228
api/api_test.go
Normal file
228
api/api_test.go
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
|
@ -57,6 +57,8 @@ type API interface {
|
||||||
MigrateNodeStorage(ctx context.Context, to string) error
|
MigrateNodeStorage(ctx context.Context, to string) error
|
||||||
GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesResponse, error)
|
GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesResponse, error)
|
||||||
Health(ctx context.Context) (*HealthResponse, error)
|
Health(ctx context.Context) (*HealthResponse, error)
|
||||||
|
GetCustomNodeCommands() (*CustomNodeCommandsResponse, error)
|
||||||
|
ExecuteCustomNodeCommand(ctx context.Context, command string) (interface{}, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
|
|
@ -392,3 +394,22 @@ func NewHealthAlarm(kind HealthAlarmKind, rawDetails any) HealthAlarm {
|
||||||
type HealthResponse struct {
|
type HealthResponse struct {
|
||||||
Alarms []HealthAlarm `json:"alarms,omitempty"`
|
Alarms []HealthAlarm `json:"alarms,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CustomNodeCommandArgDef struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomNodeCommandDef struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Args []CustomNodeCommandArgDef `json:"args"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomNodeCommandsResponse struct {
|
||||||
|
Commands []CustomNodeCommandDef `json:"commands"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecuteCustomNodeCommandRequest struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "src/components/ui/alert-dialog";
|
||||||
|
import { Textarea } from "src/components/ui/textarea";
|
||||||
|
import { useToast } from "src/components/ui/use-toast";
|
||||||
|
import { useInfo } from "src/hooks/useInfo";
|
||||||
|
import { request } from "src/utils/request";
|
||||||
|
|
||||||
|
type ExecuteCustomNodeCommandDialogContentProps = {
|
||||||
|
availableCommands: string;
|
||||||
|
setCommandResponse: (response: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ExecuteCustomNodeCommandDialogContent({
|
||||||
|
setCommandResponse,
|
||||||
|
availableCommands,
|
||||||
|
}: ExecuteCustomNodeCommandDialogContentProps) {
|
||||||
|
const { mutate: reloadInfo } = useInfo();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [command, setCommand] = React.useState<string>();
|
||||||
|
|
||||||
|
let parsedAvailableCommands = availableCommands;
|
||||||
|
try {
|
||||||
|
parsedAvailableCommands = JSON.stringify(
|
||||||
|
JSON.parse(availableCommands).commands,
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// ignore unexpected json
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit(e: React.FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
if (!command) {
|
||||||
|
throw new Error("No command set");
|
||||||
|
}
|
||||||
|
const result = await request("/api/command", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ command }),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await reloadInfo();
|
||||||
|
|
||||||
|
const parsedResponse = JSON.stringify(result);
|
||||||
|
setCommandResponse(parsedResponse);
|
||||||
|
|
||||||
|
toast({ title: "Command executed", description: parsedResponse });
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Something went wrong: " + error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlertDialogContent>
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Execute Custom Node Command</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription className="text-left">
|
||||||
|
<Textarea
|
||||||
|
className="h-36 font-mono"
|
||||||
|
value={command}
|
||||||
|
onChange={(e) => setCommand(e.target.value)}
|
||||||
|
placeholder="commandname --arg1=value1"
|
||||||
|
/>
|
||||||
|
<p className="mt-2">Available commands</p>
|
||||||
|
<Textarea
|
||||||
|
readOnly
|
||||||
|
className="mt-2 font-mono"
|
||||||
|
value={parsedAvailableCommands}
|
||||||
|
rows={10}
|
||||||
|
/>
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter className="mt-4">
|
||||||
|
<AlertDialogCancel onClick={() => setCommand("")}>
|
||||||
|
Cancel
|
||||||
|
</AlertDialogCancel>
|
||||||
|
<AlertDialogAction type="submit">Execute</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</form>
|
||||||
|
</AlertDialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { ExecuteCustomNodeCommandDialogContent } from "src/components/ExecuteCustomNodeCommandDialogContent";
|
||||||
import { ResetRoutingDataDialogContent } from "src/components/ResetRoutingDataDialogContent";
|
import { ResetRoutingDataDialogContent } from "src/components/ResetRoutingDataDialogContent";
|
||||||
import SettingsHeader from "src/components/SettingsHeader";
|
import SettingsHeader from "src/components/SettingsHeader";
|
||||||
import {
|
import {
|
||||||
|
|
@ -221,6 +222,7 @@ export default function DebugTools() {
|
||||||
| "getNodeLogs"
|
| "getNodeLogs"
|
||||||
| "getNetworkGraph"
|
| "getNetworkGraph"
|
||||||
| "resetRoutingData"
|
| "resetRoutingData"
|
||||||
|
| "customNodeCommand"
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const { data: info } = useInfo();
|
const { data: info } = useInfo();
|
||||||
|
|
@ -311,6 +313,23 @@ export default function DebugTools() {
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
)}
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
apiRequest(`/api/commands`, "GET");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Get Node Commands
|
||||||
|
</Button>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
apiRequest(`/api/commands`, "GET");
|
||||||
|
setDialog("customNodeCommand");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Execute Node Command
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
{/* probing functions are not useful */}
|
{/* probing functions are not useful */}
|
||||||
{/*info?.backendType === "LDK" && (
|
{/*info?.backendType === "LDK" && (
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
|
|
@ -343,6 +362,12 @@ export default function DebugTools() {
|
||||||
<GetNetworkGraphDialogContent apiRequest={apiRequest} />
|
<GetNetworkGraphDialogContent apiRequest={apiRequest} />
|
||||||
)}
|
)}
|
||||||
{dialog === "resetRoutingData" && <ResetRoutingDataDialogContent />}
|
{dialog === "resetRoutingData" && <ResetRoutingDataDialogContent />}
|
||||||
|
{dialog === "customNodeCommand" && (
|
||||||
|
<ExecuteCustomNodeCommandDialogContent
|
||||||
|
availableCommands={apiResponse}
|
||||||
|
setCommandResponse={setApiResponse}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
{apiResponse && (
|
{apiResponse && (
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,8 @@ func (httpSvc *HttpService) RegisterSharedRoutes(e *echo.Echo) {
|
||||||
restrictedGroup.POST("/api/send-spontaneous-payment-probes", httpSvc.sendSpontaneousPaymentProbesHandler)
|
restrictedGroup.POST("/api/send-spontaneous-payment-probes", httpSvc.sendSpontaneousPaymentProbesHandler)
|
||||||
restrictedGroup.GET("/api/log/:type", httpSvc.getLogOutputHandler)
|
restrictedGroup.GET("/api/log/:type", httpSvc.getLogOutputHandler)
|
||||||
restrictedGroup.GET("/api/health", httpSvc.healthHandler)
|
restrictedGroup.GET("/api/health", httpSvc.healthHandler)
|
||||||
|
restrictedGroup.GET("/api/commands", httpSvc.getCustomNodeCommandsHandler)
|
||||||
|
restrictedGroup.POST("/api/command", httpSvc.execCustomNodeCommandHandler)
|
||||||
|
|
||||||
httpSvc.albyHttpSvc.RegisterSharedRoutes(restrictedGroup, e)
|
httpSvc.albyHttpSvc.RegisterSharedRoutes(restrictedGroup, e)
|
||||||
}
|
}
|
||||||
|
|
@ -999,6 +1001,35 @@ func (httpSvc *HttpService) getLogOutputHandler(c echo.Context) error {
|
||||||
return c.JSON(http.StatusOK, getLogResponse)
|
return c.JSON(http.StatusOK, getLogResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (httpSvc *HttpService) getCustomNodeCommandsHandler(c echo.Context) error {
|
||||||
|
nodeCommandsResponse, err := httpSvc.api.GetCustomNodeCommands()
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||||
|
Message: fmt.Sprintf("Failed to get node commands: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, nodeCommandsResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (httpSvc *HttpService) execCustomNodeCommandHandler(c echo.Context) error {
|
||||||
|
var execCommandRequest api.ExecuteCustomNodeCommandRequest
|
||||||
|
if err := c.Bind(&execCommandRequest); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, ErrorResponse{
|
||||||
|
Message: fmt.Sprintf("Bad request: %s", err.Error()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
execCommandResponse, err := httpSvc.api.ExecuteCustomNodeCommand(c.Request().Context(), execCommandRequest.Command)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||||
|
Message: fmt.Sprintf("Failed to execute command: %v", err),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, execCommandResponse)
|
||||||
|
}
|
||||||
|
|
||||||
func (httpSvc *HttpService) logoutHandler(c echo.Context) error {
|
func (httpSvc *HttpService) logoutHandler(c echo.Context) error {
|
||||||
redirectUrl := httpSvc.cfg.GetEnv().FrontendUrl
|
redirectUrl := httpSvc.cfg.GetEnv().FrontendUrl
|
||||||
if redirectUrl == "" {
|
if redirectUrl == "" {
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ func NewBreezService(mnemonic, apiKey, inviteCode, workDir string) (result lncli
|
||||||
return nil, errors.New("one or more required breez configuration are missing")
|
return nil, errors.New("one or more required breez configuration are missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
//create dir if not exists
|
// create dir if not exists
|
||||||
newpath := filepath.Join(workDir)
|
newpath := filepath.Join(workDir)
|
||||||
err = os.MkdirAll(newpath, os.ModePerm)
|
err = os.MkdirAll(newpath, os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -493,3 +493,11 @@ func (bs *BreezService) GetSupportedNIP47NotificationTypes() []string {
|
||||||
func (bs *BreezService) GetPubkey() string {
|
func (bs *BreezService) GetPubkey() string {
|
||||||
return bs.pubkey
|
return bs.pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (bs *BreezService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bs *BreezService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,16 @@ import (
|
||||||
|
|
||||||
"github.com/elnosh/gonuts/wallet"
|
"github.com/elnosh/gonuts/wallet"
|
||||||
"github.com/elnosh/gonuts/wallet/storage"
|
"github.com/elnosh/gonuts/wallet/storage"
|
||||||
"github.com/getAlby/hub/lnclient"
|
|
||||||
"github.com/getAlby/hub/logger"
|
|
||||||
decodepay "github.com/nbd-wtf/ln-decodepay"
|
decodepay "github.com/nbd-wtf/ln-decodepay"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/getAlby/hub/lnclient"
|
||||||
|
"github.com/getAlby/hub/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const nodeCommandRestore = "restore"
|
||||||
|
const exampleCommandWithArg = "example"
|
||||||
|
|
||||||
type CashuService struct {
|
type CashuService struct {
|
||||||
wallet *wallet.Wallet
|
wallet *wallet.Wallet
|
||||||
}
|
}
|
||||||
|
|
@ -29,7 +33,7 @@ func NewCashuService(workDir string, mintUrl string) (result lnclient.LNClient,
|
||||||
return nil, errors.New("no mint URL configured")
|
return nil, errors.New("no mint URL configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
//create dir if not exists
|
// create dir if not exists
|
||||||
newpath := filepath.Join(workDir)
|
newpath := filepath.Join(workDir)
|
||||||
_, err = os.Stat(newpath)
|
_, err = os.Stat(newpath)
|
||||||
isFirstSetup := err != nil && errors.Is(err, os.ErrNotExist)
|
isFirstSetup := err != nil && errors.Is(err, os.ErrNotExist)
|
||||||
|
|
@ -375,3 +379,67 @@ func (cs *CashuService) GetSupportedNIP47NotificationTypes() []string {
|
||||||
func (svc *CashuService) GetPubkey() string {
|
func (svc *CashuService) GetPubkey() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cs *CashuService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||||
|
return []lnclient.CustomNodeCommandDef{
|
||||||
|
{
|
||||||
|
Name: nodeCommandRestore,
|
||||||
|
Description: "Restore cashu tokens after the wallet had a stuck payment.",
|
||||||
|
Args: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: exampleCommandWithArg,
|
||||||
|
Description: "Example command with argument",
|
||||||
|
Args: []lnclient.CustomNodeCommandArgDef{
|
||||||
|
{
|
||||||
|
Name: "hello",
|
||||||
|
Description: "world",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *CashuService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||||
|
switch command.Name {
|
||||||
|
case nodeCommandRestore:
|
||||||
|
return cs.executeCommandRestore(ctx)
|
||||||
|
case exampleCommandWithArg:
|
||||||
|
if len(command.Args) != 1 {
|
||||||
|
return nil, errors.New("please provide an argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &lnclient.CustomNodeCommandResponse{
|
||||||
|
Response: map[string]string{
|
||||||
|
command.Args[0].Name: command.Args[0].Value,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, lnclient.ErrUnknownCustomNodeCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *CashuService) executeCommandRestore(ctx context.Context) (*lnclient.CustomNodeCommandResponse, error) {
|
||||||
|
// FIXME: needs latest Cashu changes to be merged
|
||||||
|
// mnemonic := cs.wallet.Mnemonic()
|
||||||
|
// currentMint := cs.wallet.CurrentMint()
|
||||||
|
//
|
||||||
|
// if err := cs.wallet.Shutdown(); err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if err := os.RemoveAll(cs.workDir); err != nil {
|
||||||
|
// logger.Logger.WithError(err).Error("Failed to remove wallet directory")
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// amountRestored, err := wallet.Restore(cs.workDir, mnemonic, []string{currentMint})
|
||||||
|
// if err != nil {
|
||||||
|
// logger.Logger.WithError(err).Error("Failed restore cashu wallet")
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// logger.Logger.WithField("amountRestored", amountRestored).Info("Successfully restored cashu wallet")
|
||||||
|
|
||||||
|
return lnclient.NewCustomNodeCommandResponseEmpty(), nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
//"github.com/getAlby/hub/glalby" // for local development only
|
// "github.com/getAlby/hub/glalby" // for local development only
|
||||||
|
|
||||||
"github.com/getAlby/glalby-go/glalby"
|
"github.com/getAlby/glalby-go/glalby"
|
||||||
decodepay "github.com/nbd-wtf/ln-decodepay"
|
decodepay "github.com/nbd-wtf/ln-decodepay"
|
||||||
|
|
@ -36,7 +36,7 @@ func NewGreenlightService(cfg config.Config, mnemonic, inviteCode, workDir, encr
|
||||||
return nil, errors.New("one or more required greenlight configuration are missing")
|
return nil, errors.New("one or more required greenlight configuration are missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
//create dir if not exists
|
// create dir if not exists
|
||||||
newpath := filepath.Join(workDir)
|
newpath := filepath.Join(workDir)
|
||||||
err = os.MkdirAll(newpath, os.ModePerm)
|
err = os.MkdirAll(newpath, os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -689,3 +689,11 @@ func (gs *GreenlightService) GetSupportedNIP47NotificationTypes() []string {
|
||||||
func (gs *GreenlightService) GetPubkey() string {
|
func (gs *GreenlightService) GetPubkey() string {
|
||||||
return gs.pubkey
|
return gs.pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (gs *GreenlightService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gs *GreenlightService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1736,6 +1736,14 @@ func (ls *LDKService) GetPubkey() string {
|
||||||
return ls.pubkey
|
return ls.pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ls *LDKService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ls *LDKService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getEncodedChannelMonitorsFromStaticChannelsBackup(channelsBackup *events.StaticChannelsBackupEvent) []ldk_node.KeyValue {
|
func getEncodedChannelMonitorsFromStaticChannelsBackup(channelsBackup *events.StaticChannelsBackupEvent) []ldk_node.KeyValue {
|
||||||
encodedMonitors := []ldk_node.KeyValue{}
|
encodedMonitors := []ldk_node.KeyValue{}
|
||||||
for _, monitor := range channelsBackup.Monitors {
|
for _, monitor := range channelsBackup.Monitors {
|
||||||
|
|
|
||||||
|
|
@ -1253,3 +1253,11 @@ func (svc *LNDService) GetStorageDir() (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *LNDService) UpdateLastWalletSyncRequest() {}
|
func (svc *LNDService) UpdateLastWalletSyncRequest() {}
|
||||||
|
|
||||||
|
func (svc *LNDService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *LNDService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package lnclient
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: remove JSON tags from these models (LNClient models should not be exposed directly)
|
// TODO: remove JSON tags from these models (LNClient models should not be exposed directly)
|
||||||
|
|
@ -77,6 +78,8 @@ type LNClient interface {
|
||||||
UpdateLastWalletSyncRequest()
|
UpdateLastWalletSyncRequest()
|
||||||
GetSupportedNIP47Methods() []string
|
GetSupportedNIP47Methods() []string
|
||||||
GetSupportedNIP47NotificationTypes() []string
|
GetSupportedNIP47NotificationTypes() []string
|
||||||
|
GetCustomNodeCommandDefinitions() []CustomNodeCommandDef
|
||||||
|
ExecuteCustomNodeCommand(ctx context.Context, command *CustomNodeCommandRequest) (*CustomNodeCommandResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
|
|
@ -189,6 +192,39 @@ type PaymentFailedEventProperties struct {
|
||||||
Reason string
|
Reason string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CustomNodeCommandArgDef struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomNodeCommandDef struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Args []CustomNodeCommandArgDef
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomNodeCommandArg struct {
|
||||||
|
Name string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomNodeCommandRequest struct {
|
||||||
|
Name string
|
||||||
|
Args []CustomNodeCommandArg
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomNodeCommandResponse struct {
|
||||||
|
Response interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCustomNodeCommandResponseEmpty() *CustomNodeCommandResponse {
|
||||||
|
return &CustomNodeCommandResponse{
|
||||||
|
Response: struct{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrUnknownCustomNodeCommand = errors.New("unknown custom node command")
|
||||||
|
|
||||||
// default invoice expiry in seconds (1 day)
|
// default invoice expiry in seconds (1 day)
|
||||||
const DEFAULT_INVOICE_EXPIRY = 86400
|
const DEFAULT_INVOICE_EXPIRY = 86400
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
decodepay "github.com/nbd-wtf/ln-decodepay"
|
||||||
|
|
||||||
"github.com/getAlby/hub/lnclient"
|
"github.com/getAlby/hub/lnclient"
|
||||||
"github.com/getAlby/hub/logger"
|
"github.com/getAlby/hub/logger"
|
||||||
decodepay "github.com/nbd-wtf/ln-decodepay"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
@ -541,3 +542,11 @@ func (svc *PhoenixService) GetSupportedNIP47NotificationTypes() []string {
|
||||||
func (svc *PhoenixService) GetPubkey() string {
|
func (svc *PhoenixService) GetPubkey() string {
|
||||||
return svc.pubkey
|
return svc.pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svc *PhoenixService) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *PhoenixService) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -203,3 +203,11 @@ func (mln *MockLn) GetPubkey() string {
|
||||||
|
|
||||||
return "123pubkey"
|
return "123pubkey"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mln *MockLn) GetCustomNodeCommandDefinitions() []lnclient.CustomNodeCommandDef {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mln *MockLn) ExecuteCustomNodeCommand(ctx context.Context, command *lnclient.CustomNodeCommandRequest) (*lnclient.CustomNodeCommandResponse, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
1817
tests/mocks/LNClient.go
Normal file
1817
tests/mocks/LNClient.go
Normal file
File diff suppressed because it is too large
Load diff
531
tests/mocks/Service.go
Normal file
531
tests/mocks/Service.go
Normal file
|
|
@ -0,0 +1,531 @@
|
||||||
|
// Code generated by mockery v2.51.1. DO NOT EDIT.
|
||||||
|
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
alby "github.com/getAlby/hub/alby"
|
||||||
|
config "github.com/getAlby/hub/config"
|
||||||
|
|
||||||
|
events "github.com/getAlby/hub/events"
|
||||||
|
|
||||||
|
gorm "gorm.io/gorm"
|
||||||
|
|
||||||
|
keys "github.com/getAlby/hub/service/keys"
|
||||||
|
|
||||||
|
lnclient "github.com/getAlby/hub/lnclient"
|
||||||
|
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
transactions "github.com/getAlby/hub/transactions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockService is an autogenerated mock type for the Service type
|
||||||
|
type MockService struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockService_Expecter struct {
|
||||||
|
mock *mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_m *MockService) EXPECT() *MockService_Expecter {
|
||||||
|
return &MockService_Expecter{mock: &_m.Mock}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAlbyOAuthSvc provides a mock function with no fields
|
||||||
|
func (_m *MockService) GetAlbyOAuthSvc() alby.AlbyOAuthService {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetAlbyOAuthSvc")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 alby.AlbyOAuthService
|
||||||
|
if rf, ok := ret.Get(0).(func() alby.AlbyOAuthService); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(alby.AlbyOAuthService)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockService_GetAlbyOAuthSvc_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAlbyOAuthSvc'
|
||||||
|
type MockService_GetAlbyOAuthSvc_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAlbyOAuthSvc is a helper method to define mock.On call
|
||||||
|
func (_e *MockService_Expecter) GetAlbyOAuthSvc() *MockService_GetAlbyOAuthSvc_Call {
|
||||||
|
return &MockService_GetAlbyOAuthSvc_Call{Call: _e.mock.On("GetAlbyOAuthSvc")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetAlbyOAuthSvc_Call) Run(run func()) *MockService_GetAlbyOAuthSvc_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetAlbyOAuthSvc_Call) Return(_a0 alby.AlbyOAuthService) *MockService_GetAlbyOAuthSvc_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetAlbyOAuthSvc_Call) RunAndReturn(run func() alby.AlbyOAuthService) *MockService_GetAlbyOAuthSvc_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig provides a mock function with no fields
|
||||||
|
func (_m *MockService) GetConfig() config.Config {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetConfig")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 config.Config
|
||||||
|
if rf, ok := ret.Get(0).(func() config.Config); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(config.Config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockService_GetConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetConfig'
|
||||||
|
type MockService_GetConfig_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig is a helper method to define mock.On call
|
||||||
|
func (_e *MockService_Expecter) GetConfig() *MockService_GetConfig_Call {
|
||||||
|
return &MockService_GetConfig_Call{Call: _e.mock.On("GetConfig")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetConfig_Call) Run(run func()) *MockService_GetConfig_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetConfig_Call) Return(_a0 config.Config) *MockService_GetConfig_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetConfig_Call) RunAndReturn(run func() config.Config) *MockService_GetConfig_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDB provides a mock function with no fields
|
||||||
|
func (_m *MockService) GetDB() *gorm.DB {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetDB")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 *gorm.DB
|
||||||
|
if rf, ok := ret.Get(0).(func() *gorm.DB); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*gorm.DB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockService_GetDB_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDB'
|
||||||
|
type MockService_GetDB_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDB is a helper method to define mock.On call
|
||||||
|
func (_e *MockService_Expecter) GetDB() *MockService_GetDB_Call {
|
||||||
|
return &MockService_GetDB_Call{Call: _e.mock.On("GetDB")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetDB_Call) Run(run func()) *MockService_GetDB_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetDB_Call) Return(_a0 *gorm.DB) *MockService_GetDB_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetDB_Call) RunAndReturn(run func() *gorm.DB) *MockService_GetDB_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEventPublisher provides a mock function with no fields
|
||||||
|
func (_m *MockService) GetEventPublisher() events.EventPublisher {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetEventPublisher")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 events.EventPublisher
|
||||||
|
if rf, ok := ret.Get(0).(func() events.EventPublisher); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(events.EventPublisher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockService_GetEventPublisher_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetEventPublisher'
|
||||||
|
type MockService_GetEventPublisher_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEventPublisher is a helper method to define mock.On call
|
||||||
|
func (_e *MockService_Expecter) GetEventPublisher() *MockService_GetEventPublisher_Call {
|
||||||
|
return &MockService_GetEventPublisher_Call{Call: _e.mock.On("GetEventPublisher")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetEventPublisher_Call) Run(run func()) *MockService_GetEventPublisher_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetEventPublisher_Call) Return(_a0 events.EventPublisher) *MockService_GetEventPublisher_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetEventPublisher_Call) RunAndReturn(run func() events.EventPublisher) *MockService_GetEventPublisher_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeys provides a mock function with no fields
|
||||||
|
func (_m *MockService) GetKeys() keys.Keys {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetKeys")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 keys.Keys
|
||||||
|
if rf, ok := ret.Get(0).(func() keys.Keys); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(keys.Keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockService_GetKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetKeys'
|
||||||
|
type MockService_GetKeys_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKeys is a helper method to define mock.On call
|
||||||
|
func (_e *MockService_Expecter) GetKeys() *MockService_GetKeys_Call {
|
||||||
|
return &MockService_GetKeys_Call{Call: _e.mock.On("GetKeys")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetKeys_Call) Run(run func()) *MockService_GetKeys_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetKeys_Call) Return(_a0 keys.Keys) *MockService_GetKeys_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetKeys_Call) RunAndReturn(run func() keys.Keys) *MockService_GetKeys_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLNClient provides a mock function with no fields
|
||||||
|
func (_m *MockService) GetLNClient() lnclient.LNClient {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetLNClient")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 lnclient.LNClient
|
||||||
|
if rf, ok := ret.Get(0).(func() lnclient.LNClient); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(lnclient.LNClient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockService_GetLNClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLNClient'
|
||||||
|
type MockService_GetLNClient_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLNClient is a helper method to define mock.On call
|
||||||
|
func (_e *MockService_Expecter) GetLNClient() *MockService_GetLNClient_Call {
|
||||||
|
return &MockService_GetLNClient_Call{Call: _e.mock.On("GetLNClient")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetLNClient_Call) Run(run func()) *MockService_GetLNClient_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetLNClient_Call) Return(_a0 lnclient.LNClient) *MockService_GetLNClient_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetLNClient_Call) RunAndReturn(run func() lnclient.LNClient) *MockService_GetLNClient_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransactionsService provides a mock function with no fields
|
||||||
|
func (_m *MockService) GetTransactionsService() transactions.TransactionsService {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetTransactionsService")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 transactions.TransactionsService
|
||||||
|
if rf, ok := ret.Get(0).(func() transactions.TransactionsService); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(transactions.TransactionsService)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockService_GetTransactionsService_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTransactionsService'
|
||||||
|
type MockService_GetTransactionsService_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTransactionsService is a helper method to define mock.On call
|
||||||
|
func (_e *MockService_Expecter) GetTransactionsService() *MockService_GetTransactionsService_Call {
|
||||||
|
return &MockService_GetTransactionsService_Call{Call: _e.mock.On("GetTransactionsService")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetTransactionsService_Call) Run(run func()) *MockService_GetTransactionsService_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetTransactionsService_Call) Return(_a0 transactions.TransactionsService) *MockService_GetTransactionsService_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_GetTransactionsService_Call) RunAndReturn(run func() transactions.TransactionsService) *MockService_GetTransactionsService_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRelayReady provides a mock function with no fields
|
||||||
|
func (_m *MockService) IsRelayReady() bool {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for IsRelayReady")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 bool
|
||||||
|
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockService_IsRelayReady_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsRelayReady'
|
||||||
|
type MockService_IsRelayReady_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRelayReady is a helper method to define mock.On call
|
||||||
|
func (_e *MockService_Expecter) IsRelayReady() *MockService_IsRelayReady_Call {
|
||||||
|
return &MockService_IsRelayReady_Call{Call: _e.mock.On("IsRelayReady")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_IsRelayReady_Call) Run(run func()) *MockService_IsRelayReady_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_IsRelayReady_Call) Return(_a0 bool) *MockService_IsRelayReady_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_IsRelayReady_Call) RunAndReturn(run func() bool) *MockService_IsRelayReady_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown provides a mock function with no fields
|
||||||
|
func (_m *MockService) Shutdown() {
|
||||||
|
_m.Called()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockService_Shutdown_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Shutdown'
|
||||||
|
type MockService_Shutdown_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown is a helper method to define mock.On call
|
||||||
|
func (_e *MockService_Expecter) Shutdown() *MockService_Shutdown_Call {
|
||||||
|
return &MockService_Shutdown_Call{Call: _e.mock.On("Shutdown")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_Shutdown_Call) Run(run func()) *MockService_Shutdown_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_Shutdown_Call) Return() *MockService_Shutdown_Call {
|
||||||
|
_c.Call.Return()
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_Shutdown_Call) RunAndReturn(run func()) *MockService_Shutdown_Call {
|
||||||
|
_c.Run(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartApp provides a mock function with given fields: encryptionKey
|
||||||
|
func (_m *MockService) StartApp(encryptionKey string) error {
|
||||||
|
ret := _m.Called(encryptionKey)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for StartApp")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||||
|
r0 = rf(encryptionKey)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockService_StartApp_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StartApp'
|
||||||
|
type MockService_StartApp_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartApp is a helper method to define mock.On call
|
||||||
|
// - encryptionKey string
|
||||||
|
func (_e *MockService_Expecter) StartApp(encryptionKey interface{}) *MockService_StartApp_Call {
|
||||||
|
return &MockService_StartApp_Call{Call: _e.mock.On("StartApp", encryptionKey)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_StartApp_Call) Run(run func(encryptionKey string)) *MockService_StartApp_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(string))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_StartApp_Call) Return(_a0 error) *MockService_StartApp_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_StartApp_Call) RunAndReturn(run func(string) error) *MockService_StartApp_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopApp provides a mock function with no fields
|
||||||
|
func (_m *MockService) StopApp() {
|
||||||
|
_m.Called()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockService_StopApp_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StopApp'
|
||||||
|
type MockService_StopApp_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopApp is a helper method to define mock.On call
|
||||||
|
func (_e *MockService_Expecter) StopApp() *MockService_StopApp_Call {
|
||||||
|
return &MockService_StopApp_Call{Call: _e.mock.On("StopApp")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_StopApp_Call) Run(run func()) *MockService_StopApp_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_StopApp_Call) Return() *MockService_StopApp_Call {
|
||||||
|
_c.Call.Return()
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockService_StopApp_Call) RunAndReturn(run func()) *MockService_StopApp_Call {
|
||||||
|
_c.Run(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
// The first argument is typically a *testing.T value.
|
||||||
|
func NewMockService(t interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}) *MockService {
|
||||||
|
mock := &MockService{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ReadFileTail(filePath string, maxLen int) (data []byte, err error) {
|
func ReadFileTail(filePath string, maxLen int) (data []byte, err error) {
|
||||||
|
|
@ -55,3 +57,39 @@ func Filter[T any](s []T, f func(T) bool) []T {
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ParseCommandLine(s string) ([]string, error) {
|
||||||
|
args := make([]string, 0)
|
||||||
|
var currentArg strings.Builder
|
||||||
|
inQuotes := false
|
||||||
|
escaped := false
|
||||||
|
|
||||||
|
for _, r := range s {
|
||||||
|
switch {
|
||||||
|
case escaped:
|
||||||
|
currentArg.WriteRune(r)
|
||||||
|
escaped = false
|
||||||
|
case r == '\\':
|
||||||
|
escaped = true
|
||||||
|
case r == '"':
|
||||||
|
inQuotes = !inQuotes
|
||||||
|
case unicode.IsSpace(r) && !inQuotes:
|
||||||
|
if currentArg.Len() > 0 {
|
||||||
|
args = append(args, currentArg.String())
|
||||||
|
currentArg.Reset()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
currentArg.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if escaped || inQuotes {
|
||||||
|
return nil, fmt.Errorf("unexpected end of string")
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentArg.Len() > 0 {
|
||||||
|
args = append(args, currentArg.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
102
utils/utils_test.go
Normal file
102
utils/utils_test.go
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseCommandLine(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expectedSuccess []string
|
||||||
|
expectedError string
|
||||||
|
}
|
||||||
|
|
||||||
|
// When called by the API, the first argument of the command input is actually the command name
|
||||||
|
testCases := []testCase{
|
||||||
|
{
|
||||||
|
name: "empty input",
|
||||||
|
input: "",
|
||||||
|
expectedSuccess: []string{},
|
||||||
|
expectedError: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single argument",
|
||||||
|
input: "arg1",
|
||||||
|
expectedSuccess: []string{"arg1"},
|
||||||
|
expectedError: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple arguments",
|
||||||
|
input: "arg1 arg2 arg3",
|
||||||
|
expectedSuccess: []string{"arg1", "arg2", "arg3"},
|
||||||
|
expectedError: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple arguments with extra whitespace",
|
||||||
|
input: " arg1\targ2 arg3 ",
|
||||||
|
expectedSuccess: []string{"arg1", "arg2", "arg3"},
|
||||||
|
expectedError: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple arguments with quotes and escaping",
|
||||||
|
input: `"arg 1" arg2 "arg\"3"`,
|
||||||
|
expectedSuccess: []string{"arg 1", "arg2", `arg"3`},
|
||||||
|
expectedError: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unquoted escaped whitespace",
|
||||||
|
input: `arg\ 1 arg2`,
|
||||||
|
expectedSuccess: []string{"arg 1", "arg2"},
|
||||||
|
expectedError: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "escaped JSON",
|
||||||
|
input: `{\"hello\":\"world\"}`,
|
||||||
|
expectedSuccess: []string{`{"hello":"world"}`},
|
||||||
|
expectedError: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "escaped JSON with space",
|
||||||
|
input: `"{\"hello\": \"world\"}"`,
|
||||||
|
expectedSuccess: []string{`{"hello": "world"}`},
|
||||||
|
expectedError: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unclosed quote",
|
||||||
|
input: `"arg 1", "arg2", "arg\"3`,
|
||||||
|
expectedSuccess: nil,
|
||||||
|
expectedError: "unexpected end of string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "three quotes",
|
||||||
|
input: `"""`,
|
||||||
|
expectedSuccess: nil,
|
||||||
|
expectedError: "unexpected end of string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unfinished escape",
|
||||||
|
input: `arg\`,
|
||||||
|
expectedSuccess: nil,
|
||||||
|
expectedError: "unexpected end of string",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
parsedArgs, err := ParseCommandLine(tc.input)
|
||||||
|
if tc.expectedError == "" {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tc.expectedSuccess, parsedArgs)
|
||||||
|
} else {
|
||||||
|
assert.EqualError(t, err, tc.expectedError)
|
||||||
|
assert.Empty(t, parsedArgs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -921,6 +921,38 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
|
||||||
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||||
}
|
}
|
||||||
return WailsRequestRouterResponse{Body: *nodeHealth, Error: ""}
|
return WailsRequestRouterResponse{Body: *nodeHealth, Error: ""}
|
||||||
|
case "/api/commands":
|
||||||
|
nodeCommandsResponse, err := app.api.GetCustomNodeCommands()
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.WithFields(logrus.Fields{
|
||||||
|
"route": route,
|
||||||
|
"method": method,
|
||||||
|
"body": body,
|
||||||
|
}).WithError(err).Error("Failed to get node commands")
|
||||||
|
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||||
|
}
|
||||||
|
return WailsRequestRouterResponse{Body: nodeCommandsResponse, Error: ""}
|
||||||
|
case "/api/command":
|
||||||
|
commandRequest := &api.ExecuteCustomNodeCommandRequest{}
|
||||||
|
err := json.Unmarshal([]byte(body), commandRequest)
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.WithFields(logrus.Fields{
|
||||||
|
"route": route,
|
||||||
|
"method": method,
|
||||||
|
"body": body,
|
||||||
|
}).WithError(err).Error("Failed to decode request to wails router")
|
||||||
|
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||||
|
}
|
||||||
|
commandResponse, err := app.api.ExecuteCustomNodeCommand(ctx, commandRequest.Command)
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.WithFields(logrus.Fields{
|
||||||
|
"route": route,
|
||||||
|
"method": method,
|
||||||
|
"body": body,
|
||||||
|
}).WithError(err).Error("Failed to execute command")
|
||||||
|
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
|
||||||
|
}
|
||||||
|
return WailsRequestRouterResponse{Body: commandResponse, Error: ""}
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(route, "/api/log/") {
|
if strings.HasPrefix(route, "/api/log/") {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue