Files

177 lines
4.5 KiB
Go

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
// <word><digit><delim><word><digit><delim><word><digit> 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
}