Bring back the in-progress viper and go-homedir configuration loading from the original root command as a persistent pre-run on the root command, with a --config flag and a config line in sutil info. Restore the exported main.Version with a nolint directive instead of renaming it. Export CGO_ENABLED=0 from the Makefile, drop -race from make test, and remove the C toolchain from the Docker builder stage.
242 lines
6.4 KiB
Go
242 lines
6.4 KiB
Go
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
|
|
}
|