825 lines
19 KiB
Go
825 lines
19 KiB
Go
|
package random
|
|||
|
|
|||
|
import (
|
|||
|
"context"
|
|||
|
"crypto/rand"
|
|||
|
"encoding/json"
|
|||
|
"fmt"
|
|||
|
"io"
|
|||
|
"math"
|
|||
|
MRAND "math/rand"
|
|||
|
"reflect"
|
|||
|
"sort"
|
|||
|
"testing"
|
|||
|
"time"
|
|||
|
)
|
|||
|
|
|||
|
func TestStringGenerator_Generate_successful(t *testing.T) {
|
|||
|
type testCase struct {
|
|||
|
timeout time.Duration
|
|||
|
generator *StringGenerator
|
|||
|
}
|
|||
|
|
|||
|
tests := map[string]testCase{
|
|||
|
"common rules": {
|
|||
|
timeout: 1 * time.Second,
|
|||
|
generator: &StringGenerator{
|
|||
|
Length: 20,
|
|||
|
Rules: []Rule{
|
|||
|
CharsetRule{
|
|||
|
Charset: LowercaseRuneset,
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
CharsetRule{
|
|||
|
Charset: UppercaseRuneset,
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
CharsetRule{
|
|||
|
Charset: NumericRuneset,
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
CharsetRule{
|
|||
|
Charset: ShortSymbolRuneset,
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
},
|
|||
|
charset: AlphaNumericShortSymbolRuneset,
|
|||
|
},
|
|||
|
},
|
|||
|
"charset not explicitly specified": {
|
|||
|
timeout: 1 * time.Second,
|
|||
|
generator: &StringGenerator{
|
|||
|
Length: 20,
|
|||
|
Rules: []Rule{
|
|||
|
CharsetRule{
|
|||
|
Charset: LowercaseRuneset,
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
CharsetRule{
|
|||
|
Charset: UppercaseRuneset,
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
CharsetRule{
|
|||
|
Charset: NumericRuneset,
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
for name, test := range tests {
|
|||
|
t.Run(name, func(t *testing.T) {
|
|||
|
// One context to rule them all, one context to find them, one context to bring them all and in the darkness bind them.
|
|||
|
ctx, cancel := context.WithTimeout(context.Background(), test.timeout)
|
|||
|
defer cancel()
|
|||
|
|
|||
|
runeset := map[rune]bool{}
|
|||
|
runesFound := []rune{}
|
|||
|
|
|||
|
for i := 0; i < 100; i++ {
|
|||
|
actual, err := test.generator.Generate(ctx, nil)
|
|||
|
if err != nil {
|
|||
|
t.Fatalf("no error expected, but got: %s", err)
|
|||
|
}
|
|||
|
for _, r := range actual {
|
|||
|
if runeset[r] {
|
|||
|
continue
|
|||
|
}
|
|||
|
runeset[r] = true
|
|||
|
runesFound = append(runesFound, r)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
sort.Sort(runes(runesFound))
|
|||
|
|
|||
|
expectedCharset := getChars(test.generator.Rules)
|
|||
|
|
|||
|
if !reflect.DeepEqual(runesFound, expectedCharset) {
|
|||
|
t.Fatalf("Didn't find all characters from the charset\nActual : [%s]\nExpected: [%s]", string(runesFound), string(expectedCharset))
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func TestStringGenerator_Generate_errors(t *testing.T) {
|
|||
|
type testCase struct {
|
|||
|
timeout time.Duration
|
|||
|
generator *StringGenerator
|
|||
|
rng io.Reader
|
|||
|
}
|
|||
|
|
|||
|
tests := map[string]testCase{
|
|||
|
"already timed out": {
|
|||
|
timeout: 0,
|
|||
|
generator: &StringGenerator{
|
|||
|
Length: 20,
|
|||
|
Rules: []Rule{
|
|||
|
testCharsetRule{
|
|||
|
fail: false,
|
|||
|
},
|
|||
|
},
|
|||
|
charset: AlphaNumericShortSymbolRuneset,
|
|||
|
},
|
|||
|
rng: rand.Reader,
|
|||
|
},
|
|||
|
"impossible rules": {
|
|||
|
timeout: 10 * time.Millisecond, // Keep this short so the test doesn't take too long
|
|||
|
generator: &StringGenerator{
|
|||
|
Length: 20,
|
|||
|
Rules: []Rule{
|
|||
|
testCharsetRule{
|
|||
|
fail: true,
|
|||
|
},
|
|||
|
},
|
|||
|
charset: AlphaNumericShortSymbolRuneset,
|
|||
|
},
|
|||
|
rng: rand.Reader,
|
|||
|
},
|
|||
|
"bad RNG reader": {
|
|||
|
timeout: 10 * time.Millisecond, // Keep this short so the test doesn't take too long
|
|||
|
generator: &StringGenerator{
|
|||
|
Length: 20,
|
|||
|
Rules: []Rule{},
|
|||
|
charset: AlphaNumericShortSymbolRuneset,
|
|||
|
},
|
|||
|
rng: badReader{},
|
|||
|
},
|
|||
|
"0 length": {
|
|||
|
timeout: 10 * time.Millisecond,
|
|||
|
generator: &StringGenerator{
|
|||
|
Length: 0,
|
|||
|
Rules: []Rule{
|
|||
|
CharsetRule{
|
|||
|
Charset: []rune("abcde"),
|
|||
|
MinChars: 0,
|
|||
|
},
|
|||
|
},
|
|||
|
charset: []rune("abcde"),
|
|||
|
},
|
|||
|
rng: rand.Reader,
|
|||
|
},
|
|||
|
"-1 length": {
|
|||
|
timeout: 10 * time.Millisecond,
|
|||
|
generator: &StringGenerator{
|
|||
|
Length: -1,
|
|||
|
Rules: []Rule{
|
|||
|
CharsetRule{
|
|||
|
Charset: []rune("abcde"),
|
|||
|
MinChars: 0,
|
|||
|
},
|
|||
|
},
|
|||
|
charset: []rune("abcde"),
|
|||
|
},
|
|||
|
rng: rand.Reader,
|
|||
|
},
|
|||
|
"no charset": {
|
|||
|
timeout: 10 * time.Millisecond,
|
|||
|
generator: &StringGenerator{
|
|||
|
Length: 20,
|
|||
|
Rules: []Rule{},
|
|||
|
},
|
|||
|
rng: rand.Reader,
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
for name, test := range tests {
|
|||
|
t.Run(name, func(t *testing.T) {
|
|||
|
// One context to rule them all, one context to find them, one context to bring them all and in the darkness bind them.
|
|||
|
ctx, cancel := context.WithTimeout(context.Background(), test.timeout)
|
|||
|
defer cancel()
|
|||
|
|
|||
|
actual, err := test.generator.Generate(ctx, test.rng)
|
|||
|
if err == nil {
|
|||
|
t.Fatalf("Expected error but none found")
|
|||
|
}
|
|||
|
if actual != "" {
|
|||
|
t.Fatalf("Random string returned: %s", actual)
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func TestRandomRunes_deterministic(t *testing.T) {
|
|||
|
// These tests are to ensure that the charset selection doesn't do anything weird like selecting the same character
|
|||
|
// over and over again. The number of test cases here should be kept to a minimum since they are sensitive to changes
|
|||
|
type testCase struct {
|
|||
|
rngSeed int64
|
|||
|
charset string
|
|||
|
length int
|
|||
|
expected string
|
|||
|
}
|
|||
|
|
|||
|
tests := map[string]testCase{
|
|||
|
"small charset": {
|
|||
|
rngSeed: 1585593298447807000,
|
|||
|
charset: "abcde",
|
|||
|
length: 20,
|
|||
|
expected: "ddddddcdebbeebdbdbcd",
|
|||
|
},
|
|||
|
"common charset": {
|
|||
|
rngSeed: 1585593298447807001,
|
|||
|
charset: AlphaNumericShortSymbolCharset,
|
|||
|
length: 20,
|
|||
|
expected: "ON6lVjnBs84zJbUBVEzb",
|
|||
|
},
|
|||
|
"max size charset": {
|
|||
|
rngSeed: 1585593298447807002,
|
|||
|
charset: " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" +
|
|||
|
"`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ" +
|
|||
|
"ġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠ" +
|
|||
|
"šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠",
|
|||
|
length: 20,
|
|||
|
expected: "tųŎ℄ņ℃Œ.@řHš-ℍ}ħGIJLℏ",
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
for name, test := range tests {
|
|||
|
t.Run(name, func(t *testing.T) {
|
|||
|
rng := MRAND.New(MRAND.NewSource(test.rngSeed))
|
|||
|
runes, err := randomRunes(rng, []rune(test.charset), test.length)
|
|||
|
if err != nil {
|
|||
|
t.Fatalf("Expected no error, but found: %s", err)
|
|||
|
}
|
|||
|
|
|||
|
str := string(runes)
|
|||
|
|
|||
|
if str != test.expected {
|
|||
|
t.Fatalf("Actual: %s Expected: %s", str, test.expected)
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func TestRandomRunes_successful(t *testing.T) {
|
|||
|
type testCase struct {
|
|||
|
charset []rune // Assumes no duplicate runes
|
|||
|
length int
|
|||
|
}
|
|||
|
|
|||
|
tests := map[string]testCase{
|
|||
|
"small charset": {
|
|||
|
charset: []rune("abcde"),
|
|||
|
length: 20,
|
|||
|
},
|
|||
|
"common charset": {
|
|||
|
charset: AlphaNumericShortSymbolRuneset,
|
|||
|
length: 20,
|
|||
|
},
|
|||
|
"max size charset": {
|
|||
|
charset: []rune(
|
|||
|
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" +
|
|||
|
"`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ" +
|
|||
|
"ġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠ" +
|
|||
|
"šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠",
|
|||
|
),
|
|||
|
length: 20,
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
for name, test := range tests {
|
|||
|
t.Run(name, func(t *testing.T) {
|
|||
|
runeset := map[rune]bool{}
|
|||
|
runesFound := []rune{}
|
|||
|
|
|||
|
for i := 0; i < 10000; i++ {
|
|||
|
actual, err := randomRunes(rand.Reader, test.charset, test.length)
|
|||
|
if err != nil {
|
|||
|
t.Fatalf("no error expected, but got: %s", err)
|
|||
|
}
|
|||
|
for _, r := range actual {
|
|||
|
if runeset[r] {
|
|||
|
continue
|
|||
|
}
|
|||
|
runeset[r] = true
|
|||
|
runesFound = append(runesFound, r)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
sort.Sort(runes(runesFound))
|
|||
|
|
|||
|
// Sort the input too just to ensure that they can be compared
|
|||
|
sort.Sort(runes(test.charset))
|
|||
|
|
|||
|
if !reflect.DeepEqual(runesFound, test.charset) {
|
|||
|
t.Fatalf("Didn't find all characters from the charset\nActual : [%s]\nExpected: [%s]", string(runesFound), string(test.charset))
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func TestRandomRunes_errors(t *testing.T) {
|
|||
|
type testCase struct {
|
|||
|
charset []rune
|
|||
|
length int
|
|||
|
rng io.Reader
|
|||
|
}
|
|||
|
|
|||
|
tests := map[string]testCase{
|
|||
|
"nil charset": {
|
|||
|
charset: nil,
|
|||
|
length: 20,
|
|||
|
rng: rand.Reader,
|
|||
|
},
|
|||
|
"empty charset": {
|
|||
|
charset: []rune{},
|
|||
|
length: 20,
|
|||
|
rng: rand.Reader,
|
|||
|
},
|
|||
|
"charset is too long": {
|
|||
|
charset: []rune(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" +
|
|||
|
"`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ" +
|
|||
|
"ġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠ" +
|
|||
|
"šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠" +
|
|||
|
"Σ",
|
|||
|
),
|
|||
|
length:20,
|
|||
|
rng: rand.Reader,
|
|||
|
},
|
|||
|
"length is zero": {
|
|||
|
charset: []rune("abcde"),
|
|||
|
length: 0,
|
|||
|
rng: rand.Reader,
|
|||
|
},
|
|||
|
"length is negative": {
|
|||
|
charset: []rune("abcde"),
|
|||
|
length: -3,
|
|||
|
rng: rand.Reader,
|
|||
|
},
|
|||
|
"reader failed": {
|
|||
|
charset: []rune("abcde"),
|
|||
|
length: 20,
|
|||
|
rng: badReader{},
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
for name, test := range tests {
|
|||
|
t.Run(name, func(t *testing.T) {
|
|||
|
actual, err := randomRunes(test.rng, test.charset, test.length)
|
|||
|
if err == nil {
|
|||
|
t.Fatalf("Expected error but none found")
|
|||
|
}
|
|||
|
if actual != nil {
|
|||
|
t.Fatalf("Expected no value, but found [%s]", string(actual))
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func BenchmarkStringGenerator_Generate(b *testing.B) {
|
|||
|
lengths := []int{
|
|||
|
8, 12, 16, 20, 24, 28,
|
|||
|
}
|
|||
|
|
|||
|
type testCase struct {
|
|||
|
generator StringGenerator
|
|||
|
}
|
|||
|
|
|||
|
benches := map[string]testCase{
|
|||
|
"no rules": {
|
|||
|
generator: StringGenerator{
|
|||
|
charset: AlphaNumericFullSymbolRuneset,
|
|||
|
Rules: []Rule{},
|
|||
|
},
|
|||
|
},
|
|||
|
"default generator": {
|
|||
|
generator: DefaultStringGenerator,
|
|||
|
},
|
|||
|
"large symbol set": {
|
|||
|
generator: StringGenerator{
|
|||
|
charset: AlphaNumericFullSymbolRuneset,
|
|||
|
Rules: []Rule{
|
|||
|
CharsetRule{
|
|||
|
Charset: LowercaseRuneset,
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
CharsetRule{
|
|||
|
Charset: UppercaseRuneset,
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
CharsetRule{
|
|||
|
Charset: NumericRuneset,
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
CharsetRule{
|
|||
|
Charset: FullSymbolRuneset,
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
"max symbol set": {
|
|||
|
generator: StringGenerator{
|
|||
|
charset: []rune(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" +
|
|||
|
"`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ" +
|
|||
|
"ġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠ" +
|
|||
|
"šŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſ℀℁ℂ℃℄℅℆ℇ℈℉ℊℋℌℍℎℏℐℑℒℓ℔ℕ№℗℘ℙℚℛℜℝ℞℟℠",
|
|||
|
),
|
|||
|
Rules: []Rule{
|
|||
|
CharsetRule{
|
|||
|
Charset: LowercaseRuneset,
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
CharsetRule{
|
|||
|
Charset: UppercaseRuneset,
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
CharsetRule{
|
|||
|
Charset: []rune("ĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒ"),
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
"restrictive charset rules": {
|
|||
|
generator: StringGenerator{
|
|||
|
charset: AlphaNumericShortSymbolRuneset,
|
|||
|
Rules: []Rule{
|
|||
|
CharsetRule{
|
|||
|
Charset: []rune("A"),
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
CharsetRule{
|
|||
|
Charset: []rune("1"),
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
CharsetRule{
|
|||
|
Charset: []rune("a"),
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
CharsetRule{
|
|||
|
Charset: []rune("-"),
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
for name, bench := range benches {
|
|||
|
b.Run(name, func(b *testing.B) {
|
|||
|
for _, length := range lengths {
|
|||
|
bench.generator.Length = length
|
|||
|
b.Run(fmt.Sprintf("length=%d", length), func(b *testing.B) {
|
|||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|||
|
defer cancel()
|
|||
|
|
|||
|
b.ResetTimer()
|
|||
|
for i := 0; i < b.N; i++ {
|
|||
|
str, err := bench.generator.Generate(ctx, nil)
|
|||
|
if err != nil {
|
|||
|
b.Fatalf("Failed to generate string: %s", err)
|
|||
|
}
|
|||
|
if str == "" {
|
|||
|
b.Fatalf("Didn't error but didn't generate a string")
|
|||
|
}
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
// Mimic what the SQLCredentialsProducer is doing
|
|||
|
b.Run("SQLCredentialsProducer", func(b *testing.B) {
|
|||
|
sg := StringGenerator{
|
|||
|
Length: 16, // 16 because the SQLCredentialsProducer prepends 4 characters to a 20 character password
|
|||
|
charset: AlphaNumericRuneset,
|
|||
|
Rules: nil,
|
|||
|
}
|
|||
|
|
|||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|||
|
defer cancel()
|
|||
|
|
|||
|
b.ResetTimer()
|
|||
|
for i := 0; i < b.N; i++ {
|
|||
|
str, err := sg.Generate(ctx, nil)
|
|||
|
if err != nil {
|
|||
|
b.Fatalf("Failed to generate string: %s", err)
|
|||
|
}
|
|||
|
if str == "" {
|
|||
|
b.Fatalf("Didn't error but didn't generate a string")
|
|||
|
}
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
// Ensure the StringGenerator can be properly JSON-ified
|
|||
|
func TestStringGenerator_JSON(t *testing.T) {
|
|||
|
expected := StringGenerator{
|
|||
|
Length: 20,
|
|||
|
charset: deduplicateRunes([]rune("teststring" + ShortSymbolCharset)),
|
|||
|
Rules: []Rule{
|
|||
|
testCharsetRule{
|
|||
|
String: "teststring",
|
|||
|
Integer: 123,
|
|||
|
},
|
|||
|
CharsetRule{
|
|||
|
Charset: ShortSymbolRuneset,
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
b, err := json.Marshal(expected)
|
|||
|
if err != nil {
|
|||
|
t.Fatalf("Failed to marshal to JSON: %s", err)
|
|||
|
}
|
|||
|
|
|||
|
parser := PolicyParser{
|
|||
|
RuleRegistry: Registry{
|
|||
|
Rules: map[string]ruleConstructor{
|
|||
|
"testrule": newTestRule,
|
|||
|
"charset": ParseCharset,
|
|||
|
},
|
|||
|
},
|
|||
|
}
|
|||
|
actual, err := parser.ParsePolicy(string(b))
|
|||
|
if err != nil {
|
|||
|
t.Fatalf("Failed to parse JSON: %s", err)
|
|||
|
}
|
|||
|
|
|||
|
if !reflect.DeepEqual(actual, expected) {
|
|||
|
t.Fatalf("Actual: %#v\nExpected: %#v", actual, expected)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
type badReader struct{}
|
|||
|
|
|||
|
func (badReader) Read([]byte) (int, error) {
|
|||
|
return 0, fmt.Errorf("test error")
|
|||
|
}
|
|||
|
|
|||
|
func TestValidate(t *testing.T) {
|
|||
|
type testCase struct {
|
|||
|
generator StringGenerator
|
|||
|
expectErr bool
|
|||
|
}
|
|||
|
|
|||
|
tests := map[string]testCase{
|
|||
|
"default generator": {
|
|||
|
generator: DefaultStringGenerator,
|
|||
|
expectErr: false,
|
|||
|
},
|
|||
|
"length is 0": {
|
|||
|
generator: StringGenerator{
|
|||
|
Length: 0,
|
|||
|
},
|
|||
|
expectErr: true,
|
|||
|
},
|
|||
|
"length is negative": {
|
|||
|
generator: StringGenerator{
|
|||
|
Length: -2,
|
|||
|
},
|
|||
|
expectErr: true,
|
|||
|
},
|
|||
|
"nil charset, no rules": {
|
|||
|
generator: StringGenerator{
|
|||
|
Length: 5,
|
|||
|
charset: nil,
|
|||
|
},
|
|||
|
expectErr: true,
|
|||
|
},
|
|||
|
"zero length charset, no rules": {
|
|||
|
generator: StringGenerator{
|
|||
|
Length: 5,
|
|||
|
charset: []rune{},
|
|||
|
},
|
|||
|
expectErr: true,
|
|||
|
},
|
|||
|
"rules require password longer than length": {
|
|||
|
generator: StringGenerator{
|
|||
|
Length: 5,
|
|||
|
charset: []rune("abcde"),
|
|||
|
Rules: []Rule{
|
|||
|
CharsetRule{
|
|||
|
Charset: []rune("abcde"),
|
|||
|
MinChars: 6,
|
|||
|
},
|
|||
|
},
|
|||
|
},
|
|||
|
expectErr: true,
|
|||
|
},
|
|||
|
"charset has non-printable characters": {
|
|||
|
generator: StringGenerator{
|
|||
|
Length: 0,
|
|||
|
charset: []rune{
|
|||
|
'a',
|
|||
|
'b',
|
|||
|
0, // Null character
|
|||
|
'd',
|
|||
|
'e',
|
|||
|
},
|
|||
|
},
|
|||
|
expectErr: true,
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
for name, test := range tests {
|
|||
|
t.Run(name, func(t *testing.T) {
|
|||
|
err := test.generator.validateConfig()
|
|||
|
if test.expectErr && err == nil {
|
|||
|
t.Fatalf("err expected, got nil")
|
|||
|
}
|
|||
|
if !test.expectErr && err != nil {
|
|||
|
t.Fatalf("no error expected, got: %s", err)
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
type testNonCharsetRule struct {
|
|||
|
String string `mapstructure:"string" json:"string"`
|
|||
|
}
|
|||
|
|
|||
|
func (tr testNonCharsetRule) Pass([]rune) bool { return true }
|
|||
|
func (tr testNonCharsetRule) Type() string { return "testNonCharsetRule" }
|
|||
|
|
|||
|
func TestGetChars(t *testing.T) {
|
|||
|
type testCase struct {
|
|||
|
rules []Rule
|
|||
|
expected []rune
|
|||
|
}
|
|||
|
|
|||
|
tests := map[string]testCase{
|
|||
|
"nil rules": {
|
|||
|
rules: nil,
|
|||
|
expected: []rune(nil),
|
|||
|
},
|
|||
|
"empty rules": {
|
|||
|
rules: []Rule{},
|
|||
|
expected: []rune(nil),
|
|||
|
},
|
|||
|
"rule without chars": {
|
|||
|
rules: []Rule{
|
|||
|
testNonCharsetRule{
|
|||
|
String: "teststring",
|
|||
|
},
|
|||
|
},
|
|||
|
expected: []rune(nil),
|
|||
|
},
|
|||
|
"rule with chars": {
|
|||
|
rules: []Rule{
|
|||
|
CharsetRule{
|
|||
|
Charset: []rune("abcdefghij"),
|
|||
|
MinChars: 1,
|
|||
|
},
|
|||
|
},
|
|||
|
expected: []rune("abcdefghij"),
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
for name, test := range tests {
|
|||
|
t.Run(name, func(t *testing.T) {
|
|||
|
actual := getChars(test.rules)
|
|||
|
if !reflect.DeepEqual(actual, test.expected) {
|
|||
|
t.Fatalf("Actual: %v\nExpected: %v", actual, test.expected)
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func TestDeduplicateRunes(t *testing.T) {
|
|||
|
type testCase struct {
|
|||
|
input []rune
|
|||
|
expected []rune
|
|||
|
}
|
|||
|
|
|||
|
tests := map[string]testCase{
|
|||
|
"empty string": {
|
|||
|
input: []rune(""),
|
|||
|
expected: []rune(nil),
|
|||
|
},
|
|||
|
"no duplicates": {
|
|||
|
input: []rune("abcde"),
|
|||
|
expected: []rune("abcde"),
|
|||
|
},
|
|||
|
"in order duplicates": {
|
|||
|
input: []rune("aaaabbbbcccccccddddeeeee"),
|
|||
|
expected: []rune("abcde"),
|
|||
|
},
|
|||
|
"out of order duplicates": {
|
|||
|
input: []rune("abcdeabcdeabcdeabcde"),
|
|||
|
expected: []rune("abcde"),
|
|||
|
},
|
|||
|
"unicode no duplicates": {
|
|||
|
input: []rune("日本語"),
|
|||
|
expected: []rune("日本語"),
|
|||
|
},
|
|||
|
"unicode in order duplicates": {
|
|||
|
input: []rune("日日日日本本本語語語語語"),
|
|||
|
expected: []rune("日本語"),
|
|||
|
},
|
|||
|
"unicode out of order duplicates": {
|
|||
|
input: []rune("日本語日本語日本語日本語"),
|
|||
|
expected: []rune("日本語"),
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
for name, test := range tests {
|
|||
|
t.Run(name, func(t *testing.T) {
|
|||
|
actual := deduplicateRunes(test.input)
|
|||
|
if !reflect.DeepEqual(actual, test.expected) {
|
|||
|
t.Fatalf("Actual: %#v\nExpected:%#v", actual, test.expected)
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func TestRandomRunes_Bias(t *testing.T) {
|
|||
|
type testCase struct {
|
|||
|
charset []rune
|
|||
|
maxStdDev float64
|
|||
|
}
|
|||
|
|
|||
|
tests := map[string]testCase{
|
|||
|
"small charset": {
|
|||
|
charset: []rune("abcde"),
|
|||
|
maxStdDev: 2700,
|
|||
|
},
|
|||
|
"lowercase characters": {
|
|||
|
charset: LowercaseRuneset,
|
|||
|
maxStdDev: 1000,
|
|||
|
},
|
|||
|
"alphabetical characters": {
|
|||
|
charset: AlphabeticRuneset,
|
|||
|
maxStdDev: 800,
|
|||
|
},
|
|||
|
"alphanumeric": {
|
|||
|
charset: AlphaNumericRuneset,
|
|||
|
maxStdDev: 800,
|
|||
|
},
|
|||
|
"alphanumeric with symbol": {
|
|||
|
charset: AlphaNumericShortSymbolRuneset,
|
|||
|
maxStdDev: 800,
|
|||
|
},
|
|||
|
"charset evenly divisible into 256": {
|
|||
|
charset: append(AlphaNumericRuneset, '!', '@'),
|
|||
|
maxStdDev: 800,
|
|||
|
},
|
|||
|
"large charset": {
|
|||
|
charset: FullSymbolRuneset,
|
|||
|
maxStdDev: 800,
|
|||
|
},
|
|||
|
"just under half size charset": {
|
|||
|
charset: []rune(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" +
|
|||
|
"`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğ"),
|
|||
|
maxStdDev: 800,
|
|||
|
},
|
|||
|
"half size charset": {
|
|||
|
charset: []rune(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_" +
|
|||
|
"`abcdefghijklmnopqrstuvwxyz{|}~ĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠ"),
|
|||
|
maxStdDev: 800,
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
for name, test := range tests {
|
|||
|
t.Run(fmt.Sprintf("%s (%d chars)", name, len(test.charset)), func(t *testing.T) {
|
|||
|
runeCounts := map[rune]int{}
|
|||
|
|
|||
|
generations := 50000
|
|||
|
length := 100
|
|||
|
for i := 0; i < generations; i++ {
|
|||
|
str, err := randomRunes(nil, test.charset, length)
|
|||
|
if err != nil {
|
|||
|
t.Fatal(err)
|
|||
|
}
|
|||
|
for _, r := range str {
|
|||
|
runeCounts[r]++
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
chars := charCounts{}
|
|||
|
|
|||
|
var sum float64
|
|||
|
for r, count := range runeCounts {
|
|||
|
chars = append(chars, charCount{r, count})
|
|||
|
sum += float64(count)
|
|||
|
}
|
|||
|
|
|||
|
mean := sum / float64(len(runeCounts))
|
|||
|
var stdDev float64
|
|||
|
for _, count := range runeCounts {
|
|||
|
stdDev += math.Pow(float64(count)-mean, 2)
|
|||
|
}
|
|||
|
|
|||
|
stdDev = math.Sqrt(stdDev / float64(len(runeCounts)))
|
|||
|
t.Logf("Mean : %10.4f", mean)
|
|||
|
|
|||
|
if stdDev > test.maxStdDev {
|
|||
|
t.Fatalf("Standard deviation is too large: %.2f > %.2f", stdDev, test.maxStdDev)
|
|||
|
}
|
|||
|
})
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
type charCount struct {
|
|||
|
r rune
|
|||
|
count int
|
|||
|
}
|
|||
|
|
|||
|
type charCounts []charCount
|
|||
|
|
|||
|
func (s charCounts) Len() int { return len(s) }
|
|||
|
func (s charCounts) Less(i, j int) bool { return s[i].r < s[j].r }
|
|||
|
func (s charCounts) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|