check / check (push) Waiting to run
Every way a mnemonic could be invalid was reported as a checksum failure, including an empty one. It is now refused as empty, as having a word count other than 12, 15, 18, 21 or 24, as having a word that is not on the BIP-39 word list, or as failing the checksum. A word not on the list is named by its position, never by the word itself. Model: opus-5-5
168 lines
4.9 KiB
Go
168 lines
4.9 KiB
Go
package mnemonic_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/keyfunc/internal/mnemonic"
|
|
)
|
|
|
|
// Two more mnemonics that pass the checksum, so a test can tell which
|
|
// source an answer came from.
|
|
const (
|
|
fromCommandVariable = "legal winner thank year wave sausage worth " +
|
|
"useful legal winner thank yellow"
|
|
fromVariable = "letter advice cage absurd amount doctor acoustic " +
|
|
"avoid letter advice cage above"
|
|
)
|
|
|
|
// example returns the mnemonic every BIP-39 document uses to show its
|
|
// test vectors: eleven abandons and about.
|
|
func example() string {
|
|
return strings.Repeat("abandon ", 11) + "about"
|
|
}
|
|
|
|
// broken returns a mnemonic whose last word does not match the rest.
|
|
func broken() string {
|
|
return strings.TrimSpace(strings.Repeat("abandon ", 12))
|
|
}
|
|
|
|
// prints builds a shell command that writes the given words padded
|
|
// with spaces, so the test also shows that the padding is dropped.
|
|
func prints(words string) string {
|
|
return "printf ' %s \\n' '" + words + "'"
|
|
}
|
|
|
|
func TestTheCommandOnTheCommandLineWins(t *testing.T) {
|
|
t.Setenv(mnemonic.CommandVariable, prints(fromCommandVariable))
|
|
t.Setenv(mnemonic.Variable, fromVariable)
|
|
|
|
words, err := mnemonic.Read(t.Context(), prints(example()))
|
|
require.NoError(t, err)
|
|
require.Equal(t, example(), words)
|
|
}
|
|
|
|
func TestTheCommandInTheEnvironmentComesNext(t *testing.T) {
|
|
t.Setenv(mnemonic.CommandVariable, prints(fromCommandVariable))
|
|
t.Setenv(mnemonic.Variable, fromVariable)
|
|
|
|
words, err := mnemonic.Read(t.Context(), "")
|
|
require.NoError(t, err)
|
|
require.Equal(t, fromCommandVariable, words)
|
|
}
|
|
|
|
func TestTheMnemonicInTheEnvironmentComesLast(t *testing.T) {
|
|
t.Setenv(mnemonic.CommandVariable, "")
|
|
t.Setenv(mnemonic.Variable, " "+fromVariable+" ")
|
|
|
|
words, err := mnemonic.Read(t.Context(), "")
|
|
require.NoError(t, err)
|
|
require.Equal(t, fromVariable, words)
|
|
}
|
|
|
|
func TestTheMnemonicIsNotHandedToTheCommand(t *testing.T) {
|
|
const marker = "KEYFUNC_TEST_MARKER"
|
|
|
|
t.Setenv(mnemonic.CommandVariable, prints(fromCommandVariable))
|
|
t.Setenv(mnemonic.Variable, fromVariable)
|
|
t.Setenv(marker, "reaches the command")
|
|
|
|
path := filepath.Join(t.TempDir(), "environment")
|
|
|
|
words, err := mnemonic.Read(
|
|
t.Context(), "env > '"+path+"'; "+prints(example()),
|
|
)
|
|
require.NoError(t, err)
|
|
require.Equal(t, example(), words)
|
|
|
|
//nolint:gosec // the path is a temporary file of the test's own
|
|
content, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
|
|
environment := string(content)
|
|
require.NotContains(t, environment, mnemonic.Variable+"=")
|
|
require.NotContains(t, environment, mnemonic.CommandVariable+"=")
|
|
require.Contains(t, environment, marker+"=")
|
|
}
|
|
|
|
func TestAFailingCommandSaysWhatWentWrong(t *testing.T) {
|
|
t.Setenv(mnemonic.CommandVariable, "")
|
|
t.Setenv(mnemonic.Variable, "")
|
|
|
|
_, err := mnemonic.Read(t.Context(), "echo nothing here >&2; exit 3")
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "nothing here")
|
|
}
|
|
|
|
func TestValidMnemonicsOf15To24WordsAreAccepted(t *testing.T) {
|
|
t.Setenv(mnemonic.CommandVariable, "")
|
|
|
|
// abandon repeated, then the last word that makes the checksum
|
|
// match: 15, 18, 21 and 24 words.
|
|
valid := []string{
|
|
strings.Repeat("abandon ", 14) + "address",
|
|
strings.Repeat("abandon ", 17) + "agent",
|
|
strings.Repeat("abandon ", 20) + "admit",
|
|
strings.Repeat("abandon ", 23) + "art",
|
|
}
|
|
|
|
for _, words := range valid {
|
|
t.Setenv(mnemonic.Variable, words)
|
|
|
|
got, err := mnemonic.Read(t.Context(), "")
|
|
require.NoError(t, err)
|
|
require.Equal(t, words, got)
|
|
}
|
|
}
|
|
|
|
func TestABadChecksumIsRefused(t *testing.T) {
|
|
t.Setenv(mnemonic.CommandVariable, "")
|
|
t.Setenv(mnemonic.Variable, broken())
|
|
|
|
_, err := mnemonic.Read(t.Context(), "")
|
|
require.ErrorIs(t, err, mnemonic.ErrChecksum)
|
|
}
|
|
|
|
func TestACommandThatPrintsNothingGivesAnEmptyMnemonic(t *testing.T) {
|
|
t.Setenv(mnemonic.CommandVariable, "")
|
|
t.Setenv(mnemonic.Variable, "")
|
|
|
|
_, err := mnemonic.Read(t.Context(), "true")
|
|
require.ErrorIs(t, err, mnemonic.ErrEmpty)
|
|
require.NotContains(t, err.Error(), "checksum")
|
|
}
|
|
|
|
func TestAWrongWordCountIsRefused(t *testing.T) {
|
|
t.Setenv(mnemonic.CommandVariable, "")
|
|
t.Setenv(mnemonic.Variable, strings.Repeat("abandon ", 3))
|
|
|
|
_, err := mnemonic.Read(t.Context(), "")
|
|
require.ErrorIs(t, err, mnemonic.ErrWordCount)
|
|
require.ErrorContains(t, err, "not 3")
|
|
require.NotContains(t, err.Error(), "checksum")
|
|
}
|
|
|
|
func TestAWordNotOnTheListIsNamedByPositionOnly(t *testing.T) {
|
|
const typo = "abuot"
|
|
|
|
t.Setenv(mnemonic.CommandVariable, "")
|
|
t.Setenv(mnemonic.Variable, strings.Repeat("abandon ", 11)+typo)
|
|
|
|
_, err := mnemonic.Read(t.Context(), "")
|
|
require.ErrorIs(t, err, mnemonic.ErrUnknownWord)
|
|
require.ErrorContains(t, err, "word 12 ")
|
|
require.NotContains(t, err.Error(), typo)
|
|
require.NotContains(t, err.Error(), "checksum")
|
|
}
|
|
|
|
func TestNothingToReadAndNobodyToAsk(t *testing.T) {
|
|
t.Setenv(mnemonic.CommandVariable, "")
|
|
t.Setenv(mnemonic.Variable, "")
|
|
|
|
_, err := mnemonic.Read(t.Context(), "")
|
|
require.ErrorIs(t, err, mnemonic.ErrMissing)
|
|
}
|