177 lines
4.5 KiB
Go
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
|
|
}
|