Repository: cosmos/relayer
Pull request #607 (2022-03-22 13:58:42)

Add tests for cmd/keys, cmd/chains
These are just some basic tests around adding and removing chains and keys, so far.


Patch:
diff --git a/cmd/keys.go b/cmd/keys.go
index 07ed358..1104293 100644
--- a/cmd/keys.go
+++ b/cmd/keys.go
@@ -76,7 +76,7 @@ $ %s k a ibc-2 testkey`, appName, appName, appName)),
 
 			ko, err := chain.ChainProvider.AddKey(keyName)
 			if err != nil {
-				return err
+				return fmt.Errorf("failed to add key: %w", err)
 			}
 
 			out, err := json.Marshal(&ko)
@@ -200,7 +200,9 @@ func keysListCmd(a *appState) *cobra.Command {
 $ %s keys list ibc-0
 $ %s k l ibc-1`, appName, appName)),
 		RunE: func(cmd *cobra.Command, args []string) error {
-			chain, err := a.Config.Chains.Get(args[0])
+			chainID := args[0]
+
+			chain, err := a.Config.Chains.Get(chainID)
 			if err != nil {
 				return err
 			}
@@ -210,8 +212,12 @@ $ %s k l ibc-1`, appName, appName)),
 				return err
 			}
 
+			if len(info) == 0 {
+				fmt.Fprintf(cmd.ErrOrStderr(), "warning: no keys found for chain %s (do you need to run 'rly keys add %s'?)\n", chainID, chainID)
+			}
+
 			for key, val := range info {
-				fmt.Printf("key(%s) -> %s\n", key, val)
+				fmt.Fprintf(cmd.OutOrStdout(), "key(%s) -> %s\n", key, val)
 			}
 
 			return nil
@@ -288,7 +294,7 @@ $ %s k e ibc-2 testkey`, appName, appName)),
 				return err
 			}
 
-			fmt.Println(info)
+			fmt.Fprintln(cmd.OutOrStdout(), info)
 			return nil
 		},
 	}
diff --git a/relayer/provider/cosmos/provider.go b/relayer/provider/cosmos/provider.go
index a2bd609..def0d54 100644
--- a/relayer/provider/cosmos/provider.go
+++ b/relayer/provider/cosmos/provider.go
@@ -3,7 +3,6 @@ package cosmos
 import (
 	"context"
 	"fmt"
-	"github.com/cosmos/cosmos-sdk/types/module"
 	"math"
 	"os"
 	"reflect"
@@ -12,6 +11,7 @@ import (
 
 	"github.com/cosmos/cosmos-sdk/client"
 	"github.com/cosmos/cosmos-sdk/client/tx"
+	"github.com/cosmos/cosmos-sdk/types/module"
 
 	"github.com/avast/retry-go"
 	"github.com/cosmos/cosmos-sdk/codec"
@@ -120,7 +120,7 @@ type CosmosProviderConfig struct {
 
 func (pc CosmosProviderConfig) Validate() error {
 	if _, err := time.ParseDuration(pc.Timeout); err != nil {
-		return err
+		return fmt.Errorf("invalid Timeout: %w", err)
 	}
 	return nil
 }


Test patch:
diff --git a/cmd/chains_test.go b/cmd/chains_test.go
index 6e554d3..30a5f6e 100644
--- a/cmd/chains_test.go
+++ b/cmd/chains_test.go
@@ -1,12 +1,20 @@
 package cmd_test
 
 import (
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"regexp"
 	"testing"
 
+	"github.com/cosmos/relayer/cmd"
+	"github.com/cosmos/relayer/relayer/provider/cosmos"
 	"github.com/stretchr/testify/require"
 )
 
 func TestChainsList_Empty(t *testing.T) {
+	t.Parallel()
+
 	sys := NewSystem(t)
 
 	_ = sys.MustRun(t, "config", "init")
@@ -17,3 +25,97 @@ func TestChainsList_Empty(t *testing.T) {
 	require.Empty(t, res.Stdout.String())
 	require.Contains(t, res.Stderr.String(), "no chains found")
 }
+
+func TestChainsAdd_File(t *testing.T) {
+	t.Parallel()
+
+	sys := NewSystem(t)
+
+	_ = sys.MustRun(t, "config", "init")
+
+	sys.MustAddChain(t, cmd.ProviderConfigWrapper{
+		Type: "cosmos",
+		Value: cosmos.CosmosProviderConfig{
+			ChainID:        "testcosmos",
+			KeyringBackend: "test",
+			Timeout:        "10s",
+		},
+	})
+
+	// List the chain back in plaintext.
+	res := sys.MustRun(t, "chains", "list")
+	require.Regexp(t, regexp.MustCompile(`^\s*\d+: testcosmos\s+-> type\(cosmos\) key\(✘\) bal\(✘\) path\(✘\)`), res.Stdout.String())
+	require.Empty(t, res.Stderr.String())
+}
+
+func TestChainsAdd_URL(t *testing.T) {
+	t.Parallel()
+
+	sys := NewSystem(t)
+
+	_ = sys.MustRun(t, "config", "init")
+
+	// Start an HTTP server to use for chains add --url.
+	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path != "/chain.json" {
+			http.NotFound(w, r)
+			return
+		}
+
+		pcw := cmd.ProviderConfigWrapper{
+			Type: "cosmos",
+			Value: cosmos.CosmosProviderConfig{
+				ChainID:        "testcosmos",
+				KeyringBackend: "test",
+				Timeout:        "10s",
+			},
+		}
+
+		enc := json.NewEncoder(w)
+		enc.Encode(pcw)
+	})
+	srv := httptest.NewServer(h)
+	defer srv.Close()
+
+	// Add the chain at the URL and path of the test server.
+	res := sys.MustRun(t, "chains", "add", "--url", srv.URL+"/chain.json")
+	require.Empty(t, res.Stdout.String())
+	require.Empty(t, res.Stderr.String())
+
+	// List the chain back in plaintext.
+	res = sys.MustRun(t, "chains", "list")
+	require.Regexp(t, regexp.MustCompile(`^\s*\d+: testcosmos\s+-> type\(cosmos\) key\(✘\) bal\(✘\) path\(✘\)`), res.Stdout.String())
+	require.Empty(t, res.Stderr.String())
+}
+
+func TestChainsAdd_Delete(t *testing.T) {
+	t.Parallel()
+
+	sys := NewSystem(t)
+
+	_ = sys.MustRun(t, "config", "init")
+
+	sys.MustAddChain(t, cmd.ProviderConfigWrapper{
+		Type: "cosmos",
+		Value: cosmos.CosmosProviderConfig{
+			ChainID:        "testcosmos",
+			KeyringBackend: "test",
+			Timeout:        "10s",
+		},
+	})
+
+	// List the chain back in plaintext.
+	res := sys.MustRun(t, "chains", "list")
+	require.Contains(t, res.Stdout.String(), "testcosmos")
+	require.Empty(t, res.Stderr.String())
+
+	// Delete the added chain. Silent output.
+	res = sys.MustRun(t, "chains", "delete", "testcosmos")
+	require.Empty(t, res.Stdout.String())
+	require.Empty(t, res.Stderr.String())
+
+	// Listing again gives a warning about no chains.
+	res = sys.MustRun(t, "chains", "list")
+	require.Empty(t, res.Stdout.String())
+	require.Contains(t, res.Stderr.String(), "no chains found")
+}
diff --git a/cmd/keys_test.go b/cmd/keys_test.go
new file mode 100644
index 0000000..1322fd4
--- /dev/null
+++ b/cmd/keys_test.go
@@ -0,0 +1,105 @@
+package cmd_test
+
+import (
+	"testing"
+
+	"github.com/cosmos/cosmos-sdk/client/keys"
+	"github.com/cosmos/cosmos-sdk/crypto/keyring"
+	"github.com/cosmos/relayer/cmd"
+	"github.com/cosmos/relayer/relayer/provider/cosmos"
+	"github.com/stretchr/testify/require"
+)
+
+func TestKeysList_Empty(t *testing.T) {
+	t.Parallel()
+
+	sys := NewSystem(t)
+
+	_ = sys.MustRun(t, "config", "init")
+
+	sys.MustAddChain(t, cmd.ProviderConfigWrapper{
+		Type: "cosmos",
+		Value: cosmos.CosmosProviderConfig{
+			ChainID:        "testcosmos",
+			KeyringBackend: "test",
+			Timeout:        "10s",
+		},
+	})
+
+	res := sys.MustRun(t, "keys", "list", "testcosmos")
+	require.Empty(t, res.Stdout.String())
+	require.Contains(t, res.Stderr.String(), "no keys found for chain testcosmos")
+}
+
+func TestKeysRestore_Delete(t *testing.T) {
+	t.Parallel()
+
+	sys := NewSystem(t)
+
+	_ = sys.MustRun(t, "config", "init")
+
+	sys.MustAddChain(t, cmd.ProviderConfigWrapper{
+		Type: "cosmos",
+		Value: cosmos.CosmosProviderConfig{
+			AccountPrefix:  "cosmos",
+			ChainID:        "testcosmos",
+			KeyringBackend: "test",
+			Timeout:        "10s",
+		},
+	})
+
+	// Restore a key with mnemonic to the chain.
+	res := sys.MustRun(t, "keys", "restore", "testcosmos", "default", ZeroMnemonic)
+	require.Equal(t, res.Stdout.String(), ZeroCosmosAddr+"\n")
+	require.Empty(t, res.Stderr.String())
+
+	// Restored key must show up in list.
+	res = sys.MustRun(t, "keys", "list", "testcosmos")
+	require.Equal(t, res.Stdout.String(), "key(default) -> "+ZeroCosmosAddr+"\n")
+	require.Empty(t, res.Stderr.String())
+
+	// Deleting the key must succeed.
+	res = sys.MustRun(t, "keys", "delete", "testcosmos", "default", "-y")
+	require.Empty(t, res.Stdout.String())
+	require.Equal(t, res.Stderr.String(), "key default deleted\n")
+
+	// Listing the keys again gives the no keys warning.
+	res = sys.MustRun(t, "keys", "list", "testcosmos")
+	require.Empty(t, res.Stdout.String())
+	require.Contains(t, res.Stderr.String(), "no keys found for chain testcosmos")
+}
+
+func TestKeysExport(t *testing.T) {
+	t.Parallel()
+
+	sys := NewSystem(t)
+
+	_ = sys.MustRun(t, "config", "init")
+
+	sys.MustAddChain(t, cmd.ProviderConfigWrapper{
+		Type: "cosmos",
+		Value: cosmos.CosmosProviderConfig{
+			AccountPrefix:  "cosmos",
+			ChainID:        "testcosmos",
+			KeyringBackend: "test",
+			Timeout:        "10s",
+		},
+	})
+
+	// Restore a key with mnemonic to the chain.
+	res := sys.MustRun(t, "keys", "restore", "testcosmos", "default", ZeroMnemonic)
+	require.Equal(t, res.Stdout.String(), ZeroCosmosAddr+"\n")
+	require.Empty(t, res.Stderr.String())
+
+	// Export the key.
+	res = sys.MustRun(t, "keys", "export", "testcosmos", "default")
+	armorOut := res.Stdout.String()
+	require.Contains(t, armorOut, "BEGIN TENDERMINT PRIVATE KEY")
+	require.Empty(t, res.Stderr.String())
+
+	// Import the key to a temporary keyring.
+	kr := keyring.NewInMemory()
+	require.NoError(t, kr.ImportPrivKey("temp", armorOut, keys.DefaultKeyPass))
+
+	// TODO: confirm the imported address matches?
+}
diff --git a/cmd/system_test.go b/cmd/system_test.go
index 962a5b6..3b4ec02 100644
--- a/cmd/system_test.go
+++ b/cmd/system_test.go
@@ -2,10 +2,13 @@ package cmd_test
 
 import (
 	"bytes"
+	"encoding/json"
 	"io"
+	"os"
 	"testing"
 
 	"github.com/cosmos/relayer/cmd"
+	"github.com/stretchr/testify/require"
 )
 
 // System is a system under test.
@@ -85,3 +88,29 @@ func (s *System) MustRunWithInput(t *testing.T, in io.Reader, args ...string) Ru
 
 	return res
 }
+
+// MustAddChain serializes pcw to disk and calls "chains add --file".
+func (s *System) MustAddChain(t *testing.T, pcw cmd.ProviderConfigWrapper) {
+	t.Helper()
+
+	// Temporary file for the new chain.
+	tmpdir := t.TempDir()
+	f, err := os.CreateTemp(tmpdir, "chain*.json")
+	require.NoError(t, err)
+	defer f.Close() // Defer close in case the test fails before we explicitly close.
+
+	enc := json.NewEncoder(f)
+	require.NoError(t, enc.Encode(pcw))
+	f.Close() // Now that the content has been written, close the file so it is readable by the 'chains add' call.
+
+	// Add the chain. Output is expected to be silent.
+	res := s.MustRun(t, "chains", "add", "--file", f.Name())
+	require.Empty(t, res.Stdout.String())
+	require.Empty(t, res.Stderr.String())
+}
+
+// A fixed mnemonic and its resulting cosmos address, helpful for tests that need a mnemonic.
+const (
+	ZeroMnemonic   = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"
+	ZeroCosmosAddr = "cosmos1r5v5srda7xfth3hn2s26txvrcrntldjumt8mhl"
+)