package pwgen_test import ( "errors" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/x/internal/pwgen" ) const generateRounds = 300 // testWords mixes usable words with entries the filter must drop: // capitalized, too short, too long, punctuation, digits, and non-ASCII. func testWords() []string { return []string{ "apple", "banana", "cherry", "grape", "lemon", "mango", "melon", "peach", "Capital", "ab", "abc", "toolongwordhere", "has-dash", "num3er", "éclair", } } func usableTestWords() map[string]bool { return map[string]bool{ "apple": true, "banana": true, "cherry": true, "grape": true, "lemon": true, "mango": true, "melon": true, "peach": true, } } func TestNewFiltersWords(t *testing.T) { t.Parallel() generator, err := pwgen.New(pwgen.Params{Words: testWords()}) require.NoError(t, err) assert.Equal(t, len(usableTestWords()), generator.UsableWords()) } func TestNewHonorsLengthBounds(t *testing.T) { t.Parallel() generator, err := pwgen.New(pwgen.Params{ Words: testWords(), MinWordLength: 2, MaxWordLength: 3, }) require.NoError(t, err) assert.Equal(t, 2, generator.UsableWords(), `only "ab" and "abc" fit`) } func TestNewRejectsNoUsableWords(t *testing.T) { t.Parallel() _, err := pwgen.New(pwgen.Params{Words: []string{"Capital", "ab"}}) require.ErrorIs(t, err, pwgen.ErrNoUsableWords) _, err = pwgen.New(pwgen.Params{Words: []string{}}) require.ErrorIs(t, err, pwgen.ErrNoUsableWords) } func TestNewRejectsInvalidBounds(t *testing.T) { t.Parallel() _, err := pwgen.New(pwgen.Params{ Words: testWords(), MinWordLength: 9, MaxWordLength: 4, }) require.ErrorIs(t, err, pwgen.ErrInvalidWordLength) _, err = pwgen.New(pwgen.Params{Words: testWords(), MinWordLength: -1}) require.ErrorIs(t, err, pwgen.ErrInvalidWordLength) } func TestGenerateFormat(t *testing.T) { t.Parallel() generator, err := pwgen.New(pwgen.Params{Words: testWords()}) require.NoError(t, err) usable := usableTestWords() for range generateRounds { password, err := generator.Generate() require.NoError(t, err) words, digits, delimiter := parsePassword(t, password) assert.Contains(t, pwgen.Delimiters, string(delimiter)) assert.Len(t, words, pwgen.WordCount) for _, word := range words { assert.True(t, usable[word], "unexpected word %q in %q", word, password) } for _, digit := range digits { assert.True(t, digit >= '0' && digit <= '9', "digit in %q", password) } } } func TestGenerateVariesDelimitersAndDigits(t *testing.T) { t.Parallel() generator, err := pwgen.New(pwgen.Params{Words: testWords()}) require.NoError(t, err) delimitersSeen := map[byte]bool{} digitsSeen := map[byte]bool{} passwordsSeen := map[string]bool{} for range generateRounds { password, err := generator.Generate() require.NoError(t, err) _, digits, delimiter := parsePassword(t, password) delimitersSeen[delimiter] = true passwordsSeen[password] = true for _, digit := range digits { digitsSeen[digit] = true } } assert.Greater(t, len(delimitersSeen), 1, "delimiter should vary") assert.Greater(t, len(digitsSeen), 1, "digits should vary") assert.Greater(t, len(passwordsSeen), generateRounds/2, "passwords repeat") } func TestNewWithSystemDictionary(t *testing.T) { t.Parallel() generator, err := pwgen.New(pwgen.Params{}) if errors.Is(err, pwgen.ErrNoUsableWords) { t.Skip("this system has no word list") } require.NoError(t, err) assert.Greater(t, generator.UsableWords(), 1000) password, err := generator.Generate() require.NoError(t, err) parsePassword(t, password) } // parsePassword splits a password of the form // into its words, // digits, and the delimiter, failing the test if it is not of that shape. func parsePassword(t *testing.T, password string) ([]string, []byte, byte) { t.Helper() delimiterIndex := strings.IndexFunc(password, func(r rune) bool { return (r < 'a' || r > 'z') && (r < '0' || r > '9') }) require.NotEqual(t, -1, delimiterIndex, "no delimiter in %q", password) delimiter := password[delimiterIndex] parts := strings.Split(password, string(delimiter)) require.Len(t, parts, pwgen.WordCount, "wrong shape: %q", password) words := make([]string, 0, len(parts)) digits := make([]byte, 0, len(parts)) for _, part := range parts { require.GreaterOrEqual(t, len(part), 2, "short part in %q", password) words = append(words, part[:len(part)-1]) digits = append(digits, part[len(part)-1]) } return words, digits, delimiter }