package cli_test import ( "bytes" "errors" "os" "path/filepath" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/x/internal/cli" "sneak.berlin/go/x/internal/pwgen" "sneak.berlin/go/x/pkg/bip39wordlist" "sneak.berlin/go/x/pkg/cbk" ) const ( testVersion = "v1.2.3-test" // vectorRepeats is how many times "abandon" appears in the standard // BIP39 test vector for all-zero 128-bit entropy, which ends with // "about". vectorRepeats = bip39wordlist.MinWordCount - 1 ) // vectorMnemonic returns the standard BIP39 test vector for all-zero // 128-bit entropy. func vectorMnemonic() string { return strings.Repeat("abandon ", vectorRepeats) + "about" } func TestVersionCommand(t *testing.T) { t.Parallel() output, err := execute(t, "", "version") require.NoError(t, err) assert.Equal(t, testVersion+"\n", output) } func TestVersionFlag(t *testing.T) { t.Parallel() output, err := execute(t, "", "--version") require.NoError(t, err) assert.Contains(t, output, testVersion) } func TestInfoCommand(t *testing.T) { t.Parallel() output, err := execute(t, "", "info") require.NoError(t, err) assert.Contains(t, output, "version: "+testVersion+"\n") assert.Contains(t, output, "go: go") assert.Contains(t, output, "module: sneak.berlin/go/x\n") assert.Contains(t, output, "config: (none)\n") } func TestConfigFileDiscoveredInConfigDir(t *testing.T) { t.Parallel() configDir := t.TempDir() path := filepath.Join(configDir, ".sneak.yaml") require.NoError(t, os.WriteFile(path, []byte("example: 1\n"), 0o600)) params := cli.Params{Version: testVersion, ConfigDir: configDir} output, err := executeWithParams(t, params, "", "info") require.NoError(t, err) assert.Contains(t, output, "Using config file: "+path+"\n") assert.Contains(t, output, "config: "+path+"\n") } func TestConfigFlagLoadsNamedFile(t *testing.T) { t.Parallel() path := filepath.Join(t.TempDir(), "elsewhere.yaml") require.NoError(t, os.WriteFile(path, []byte("example: 1\n"), 0o600)) output, err := execute(t, "", "--config", path, "info") require.NoError(t, err) assert.Contains(t, output, "Using config file: "+path+"\n") assert.Contains(t, output, "config: "+path+"\n") } func TestConfigFlagRejectsMissingFile(t *testing.T) { t.Parallel() path := filepath.Join(t.TempDir(), "missing.yaml") _, err := execute(t, "", "--config", path, "version") require.Error(t, err) } func TestConfigRejectsMalformedFile(t *testing.T) { t.Parallel() configDir := t.TempDir() path := filepath.Join(configDir, ".sneak.yaml") require.NoError(t, os.WriteFile(path, []byte("not: [valid\n"), 0o600)) params := cli.Params{Version: testVersion, ConfigDir: configDir} _, err := executeWithParams(t, params, "", "version") require.Error(t, err) } func TestGenerateRandomSeededIsDeterministic(t *testing.T) { t.Parallel() first, err := execute(t, "", "generate", "random", "--count", "4096", "seed") require.NoError(t, err) assert.Len(t, first, 4096) second, err := execute(t, "", "generate", "random", "--count", "4096", "seed") require.NoError(t, err) assert.Equal(t, first, second) other, err := execute(t, "", "gen", "random", "-c", "4096", "other seed") require.NoError(t, err) assert.NotEqual(t, first, other) } func TestGenerateRandomUnseeded(t *testing.T) { t.Parallel() first, err := execute(t, "", "generate", "random", "--count", "1024") require.NoError(t, err) assert.Len(t, first, 1024) second, err := execute(t, "", "generate", "random", "--count", "1024") require.NoError(t, err) assert.NotEqual(t, first, second) } func TestGenerateRandomRejectsExtraArguments(t *testing.T) { t.Parallel() _, err := execute(t, "", "generate", "random", "one", "two") require.Error(t, err) } func TestGenerateMnemonic(t *testing.T) { t.Parallel() output, err := execute(t, "", "generate", "mnemonic") require.NoError(t, err) mnemonic := strings.TrimSuffix(output, "\n") require.NoError(t, bip39wordlist.ValidateWordlist(mnemonic)) assert.Len(t, strings.Fields(mnemonic), bip39wordlist.MaxWordCount) output, err = execute(t, "", "generate", "mnemonic", "--words", "12") require.NoError(t, err) assert.Len(t, strings.Fields(output), bip39wordlist.MinWordCount) _, err = execute(t, "", "generate", "mnemonic", "--words", "13") require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount) } func TestGeneratePassword(t *testing.T) { t.Parallel() _, err := pwgen.New(pwgen.Params{Words: nil, MinWordLength: 0, MaxWordLength: 0}) if errors.Is(err, pwgen.ErrNoUsableWords) { t.Skip("this system has no word list") } require.NoError(t, err) output, err := execute(t, "", "generate", "password") require.NoError(t, err) password := strings.TrimSuffix(output, "\n") assert.NotContains(t, password, "\n") assert.GreaterOrEqual(t, len(password), 3*(pwgen.DefaultMinWordLength+1)+2) assert.True(t, strings.ContainsAny(password, "0123456789"), password) assert.True(t, strings.ContainsAny(password, pwgen.Delimiters), password) } func TestCBKDerive(t *testing.T) { t.Parallel() expected, err := cbk.FromMnemonic(vectorMnemonic()) require.NoError(t, err) stdin := " " + strings.ToUpper(vectorMnemonic()) + "\n" output, err := execute(t, stdin, "cbk", "derive") require.NoError(t, err) assert.Equal(t, "public: "+expected.PublicKeyHex()+"\n"+ "private: "+expected.PrivateKeyHex()+"\n", output, ) output, err = execute(t, vectorMnemonic(), "cbk", "derive", "--public") require.NoError(t, err) assert.Equal(t, expected.PublicKeyHex()+"\n", output) } func TestCBKDeriveRejectsInvalidMnemonic(t *testing.T) { t.Parallel() _, err := execute(t, "not a mnemonic\n", "cbk", "derive") require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount) } // execute runs sutil with the given stdin and arguments, looking for config // in an empty temporary directory, and returns everything it wrote to // stdout and stderr. func execute(t *testing.T, stdin string, args ...string) (string, error) { t.Helper() params := cli.Params{Version: testVersion, ConfigDir: t.TempDir()} return executeWithParams(t, params, stdin, args...) } // executeWithParams is execute with explicit Params. func executeWithParams( t *testing.T, params cli.Params, stdin string, args ...string, ) (string, error) { t.Helper() root := cli.New(params) var output bytes.Buffer root.SetOut(&output) root.SetErr(&output) root.SetIn(strings.NewReader(stdin)) root.SetArgs(args) err := root.Execute() return output.String(), err }