diff --git a/itest/chantools_harness.go b/itest/chantools_harness.go new file mode 100644 index 0000000..c776da7 --- /dev/null +++ b/itest/chantools_harness.go @@ -0,0 +1,149 @@ +package itest + +import ( + "bufio" + "bytes" + "errors" + "io" + "os" + "os/exec" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// ChantoolsProcess wraps a running chantools process for integration testing. +type ChantoolsProcess struct { + cmd *exec.Cmd + stdin io.WriteCloser + stdout *os.File + stdoutReader *bufio.Reader + stderr *bufio.Reader +} + +// StartChantools starts the chantools binary with the given arguments. +func StartChantools(t *testing.T, args ...string) *ChantoolsProcess { + t.Helper() + + args = append([]string{"--nologfile"}, args...) + cmd := exec.Command("chantools", args...) + stdin, err := cmd.StdinPipe() + require.NoError(t, err) + + stdoutPipe, err := cmd.StdoutPipe() + require.NoError(t, err) + stderrPipe, err := cmd.StderrPipe() + require.NoError(t, err) + + stdoutFile, ok := stdoutPipe.(*os.File) + require.True(t, ok) + + require.NoError(t, cmd.Start()) + + return &ChantoolsProcess{ + cmd: cmd, + stdin: stdin, + stdout: stdoutFile, + stdoutReader: bufio.NewReader(stdoutFile), + stderr: bufio.NewReader(stderrPipe), + } +} + +// WriteInput writes input to the process's stdin. +func (p *ChantoolsProcess) WriteInput(t *testing.T, input string) { + t.Helper() + + _, err := io.WriteString(p.stdin, input) + require.NoError(t, err, "failed to write input to chantools") +} + +// ReadAllOutput reads all output from the process's stdout until EOF. +func (p *ChantoolsProcess) ReadAllOutput(t *testing.T) string { + t.Helper() + + resp, err := io.ReadAll(p.stdout) + require.NoError(t, err, "failed to read chantools output") + + log.Debugf("[CHANTOOLS]: %s", resp) + + return string(resp) +} + +// ReadOutputUntil reads from stdout until the given substring is found or +// timeout. +func (p *ChantoolsProcess) ReadOutputUntil(t *testing.T, substr string, + timeout time.Duration) string { + + t.Helper() + + var out bytes.Buffer + deadline := time.Now().Add(timeout) + for { + if time.Now().After(deadline) { + t.Fatal("timeout waiting for chantools output") + } + line, err := p.stdoutReader.ReadString('\n') + out.WriteString(line) + + log.Debugf("[CHANTOOLS]: %s", line) + + if strings.Contains(out.String(), substr) { + return out.String() + } + + require.NoError(t, err) + } +} + +// ReadAvailableOutput reads as many bytes as possible from stdout until the +// timeout elapses. +func (p *ChantoolsProcess) ReadAvailableOutput(t *testing.T, + timeout time.Duration) string { + + t.Helper() + + err := p.stdout.SetReadDeadline(time.Now().Add(timeout)) + require.NoError(t, err, "failed to set read deadline") + + defer func() { + _ = p.stdout.SetReadDeadline(time.Time{}) + }() + + var out bytes.Buffer + for { + buf := make([]byte, 1024) + n, err := p.stdoutReader.Read(buf) + if n > 0 { + chunk := string(buf[:n]) + out.WriteString(chunk) + } + if err != nil { + if errors.Is(err, io.EOF) || + errors.Is(err, os.ErrDeadlineExceeded) { + + break + } + + time.Sleep(50 * time.Millisecond) + } + } + + log.Debugf("[CHANTOOLS]: %s", out.String()) + return out.String() +} + +// Wait waits for the process to exit. +func (p *ChantoolsProcess) Wait(t *testing.T) { + t.Helper() + + require.NoError(t, p.cmd.Wait()) +} + +// Kill kills the process. +func (p *ChantoolsProcess) Kill(t *testing.T) { + t.Helper() + + require.NoError(t, p.cmd.Process.Kill()) +} diff --git a/itest/log.go b/itest/log.go new file mode 100644 index 0000000..b63cff5 --- /dev/null +++ b/itest/log.go @@ -0,0 +1,16 @@ +package itest + +import ( + "os" + + "github.com/btcsuite/btclog/v2" +) + +var log btclog.Logger + +//nolint:gochecknoinits +func init() { + logger := btclog.NewSLogger(btclog.NewDefaultHandler(os.Stdout)) + logger.SetLevel(btclog.LevelTrace) + log = logger.SubSystem("ITEST") +} diff --git a/itest/standalone_test.go b/itest/standalone_test.go new file mode 100644 index 0000000..beff118 --- /dev/null +++ b/itest/standalone_test.go @@ -0,0 +1,57 @@ +package itest + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +const ( + testXPriv = "xprv9s21ZrQH143K2ZzuN99NjD7oJruBomAVbNDXzPmhHYcwf8WXCsML" + + "63azyS1rzzfpUsLifeDkM4Q6U9PF9RP7frSGKkMfDTDiiyQjH2PUj2z" + + testMnemonic = "about wolf boost other battle asthma refuse wedding " + + "few purchase track one smooth tunnel immune glass infant " + + "tag manual multiply diagram orient wrist agent" +) + +var ( + readTimeout = 100 * time.Millisecond + defaultTimeout = 5 * time.Second +) + +func TestChantoolsShowRootKeyXPriv(t *testing.T) { + proc := StartChantools(t, "showrootkey", "--rootkey", testXPriv) + defer proc.Kill(t) + + output := proc.ReadOutputUntil( + t, "Your BIP32 HD root key is:", defaultTimeout, + ) + require.Contains(t, output, "Your BIP32 HD root key is: "+testXPriv) +} + +func TestChantoolsShowRootKeyMnemonic(t *testing.T) { + proc := StartChantools(t, "showrootkey") + defer proc.Kill(t) + + go func() { + errString, err := proc.stderr.ReadString('\n') + log.Errorf("chantools stderr: %v, error: %v", errString, err) + }() + + mnemonicPrompt := proc.ReadAvailableOutput(t, readTimeout) + require.Contains(t, mnemonicPrompt, "Input your 24-word mnemonic") + proc.WriteInput(t, testMnemonic+"\n") + + passphrasePrompt := proc.ReadAvailableOutput(t, readTimeout) + require.Contains( + t, passphrasePrompt, "Input your cipher seed passphrase", + ) + proc.WriteInput(t, "\n") + + output := proc.ReadOutputUntil( + t, "Your BIP32 HD root key is:", defaultTimeout, + ) + require.Contains(t, output, "Your BIP32 HD root key is: "+testXPriv) +} diff --git a/lnd/aezeed.go b/lnd/aezeed.go index 34b1168..db14ac2 100644 --- a/lnd/aezeed.go +++ b/lnd/aezeed.go @@ -2,6 +2,7 @@ package lnd import ( "bufio" + "bytes" "errors" "fmt" "os" @@ -139,16 +140,15 @@ func ReadPassphrase(verb string) ([]byte, error) { // The environment variable didn't contain anything, we'll read the // passphrase from the terminal. case passphrase == "": - fmt.Printf("Input your cipher seed passphrase (press enter "+ - "if your seed %s a passphrase): ", verb) var err error - passphraseBytes, err = terminal.ReadPassword( - int(syscall.Stdin), //nolint + passphraseBytes, err = PasswordFromConsole( + fmt.Sprintf("Input your cipher seed passphrase "+ + "(press enter if your seed %s a passphrase): ", + verb), ) if err != nil { return nil, err } - fmt.Println() // There was a password in the environment, just convert it to bytes. default: @@ -160,13 +160,15 @@ func ReadPassphrase(verb string) ([]byte, error) { // PasswordFromConsole reads a password from the console or stdin. func PasswordFromConsole(userQuery string) ([]byte, error) { + fmt.Print(userQuery) + // Read from terminal (if there is one). if terminal.IsTerminal(int(syscall.Stdin)) { //nolint - fmt.Print(userQuery) pw, err := terminal.ReadPassword(int(syscall.Stdin)) //nolint if err != nil { return nil, err } + fmt.Println() return pw, nil } @@ -177,7 +179,9 @@ func PasswordFromConsole(userQuery string) ([]byte, error) { if err != nil { return nil, err } - return pw, nil + + fmt.Println() + return bytes.TrimSpace(pw), nil } // OpenWallet opens a lnd compatible wallet and returns it, along with the