mirror of
https://github.com/wneessen/apg-go.git
synced 2024-11-08 23:42:53 +01:00
Winni Neessen
79f921f9ad
This commit introduces the .gitignore configuration file and adds SPDX headers to several files including test and documentation files. The headers provide license information in a standardized format which can be easily picked up by automated tools for license compliance checks. Additionally, it deleted a .idea/.gitignore file, which is a project specific IDE configuration file not necessary for the repository. It also introduced a README.md file providing more insightful information about the project.
516 lines
13 KiB
Go
516 lines
13 KiB
Go
// SPDX-FileCopyrightText: 2021-2024 Winni Neessen <wn@neessen.dev>
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package apg
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestGenerator_CoinFlip(t *testing.T) {
|
|
g := New(NewConfig())
|
|
cf := g.CoinFlip()
|
|
if cf < 0 || cf > 1 {
|
|
t.Errorf("CoinFlip failed(), expected 0 or 1, got: %d", cf)
|
|
}
|
|
}
|
|
|
|
func TestGenerator_CoinFlipBool(t *testing.T) {
|
|
g := New(NewConfig())
|
|
gt := false
|
|
for i := 0; i < 500_000; i++ {
|
|
cf := g.CoinFlipBool()
|
|
if cf {
|
|
gt = true
|
|
break
|
|
}
|
|
}
|
|
if !gt {
|
|
t.Error("CoinFlipBool likely not working, expected at least one true value in 500k tries, got none")
|
|
}
|
|
}
|
|
|
|
func TestGenerator_RandNum(t *testing.T) {
|
|
tt := []struct {
|
|
name string
|
|
v int64
|
|
max int64
|
|
min int64
|
|
sf bool
|
|
}{
|
|
{"RandNum up to 1000", 1000, 1000, 0, false},
|
|
{"RandNum should be 1", 1, 1, 0, false},
|
|
{"RandNum should fail on 1", 0, 0, 0, true},
|
|
{"RandNum should fail on negative", -1, 0, 0, true},
|
|
}
|
|
|
|
g := New(NewConfig())
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rn, err := g.RandNum(tc.v)
|
|
if err == nil && tc.sf {
|
|
t.Errorf("random number generation was supposed to fail, but didn't, got: %d", rn)
|
|
}
|
|
if err != nil && !tc.sf {
|
|
t.Errorf("random number generation failed: %s", err)
|
|
}
|
|
if rn > tc.max {
|
|
t.Errorf("random number generation returned too big number expected below: %d, got: %d",
|
|
tc.max, rn)
|
|
}
|
|
if rn < tc.min {
|
|
t.Errorf("random number generation returned too small number, expected min: %d, got: %d",
|
|
tc.min, rn)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGenerator_RandomBytes(t *testing.T) {
|
|
tt := []struct {
|
|
name string
|
|
l int64
|
|
sf bool
|
|
}{
|
|
{"1 bytes of randomness", 1, false},
|
|
{"100 bytes of randomness", 100, false},
|
|
{"1024 bytes of randomness", 1024, false},
|
|
{"4096 bytes of randomness", 4096, false},
|
|
{"-1 bytes of randomness", -1, true},
|
|
}
|
|
|
|
g := New(NewConfig())
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rb, err := g.RandomBytes(tc.l)
|
|
if err != nil && !tc.sf {
|
|
t.Errorf("random byte generation failed: %s", err)
|
|
return
|
|
}
|
|
if tc.sf {
|
|
return
|
|
}
|
|
bl := len(rb)
|
|
if int64(bl) != tc.l {
|
|
t.Errorf("lenght of provided bytes does not match requested length: got: %d, expected: %d",
|
|
bl, tc.l)
|
|
}
|
|
if bytes.Equal(rb, make([]byte, tc.l)) {
|
|
t.Errorf("random byte generation failed. returned slice is empty")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGenerator_RandomString(t *testing.T) {
|
|
g := New(NewConfig())
|
|
var l int64 = 32 * 1024
|
|
tt := []struct {
|
|
name string
|
|
cr string
|
|
nr string
|
|
sf bool
|
|
}{
|
|
{
|
|
"CharRange:AlphaLower", CharRangeAlphaLower,
|
|
CharRangeAlphaUpper + CharRangeNumeric + CharRangeSpecial, false,
|
|
},
|
|
{
|
|
"CharRange:AlphaUpper", CharRangeAlphaUpper,
|
|
CharRangeAlphaLower + CharRangeNumeric + CharRangeSpecial, false,
|
|
},
|
|
{
|
|
"CharRange:Number", CharRangeNumeric,
|
|
CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeSpecial, false,
|
|
},
|
|
{
|
|
"CharRange:Special", CharRangeSpecial,
|
|
CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeNumeric, false,
|
|
},
|
|
{
|
|
"CharRange:Invalid", "",
|
|
CharRangeAlphaLower + CharRangeAlphaUpper + CharRangeNumeric + CharRangeSpecial, true,
|
|
},
|
|
}
|
|
for _, tc := range tt {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
rs, err := g.RandomStringFromCharRange(l, tc.cr)
|
|
if err != nil && !tc.sf {
|
|
t.Errorf("RandomStringFromCharRange failed: %s", err)
|
|
}
|
|
if int64(len(rs)) != l && !tc.sf {
|
|
t.Errorf("RandomStringFromCharRange failed. Expected length: %d, got: %d", l, len(rs))
|
|
}
|
|
if strings.ContainsAny(rs, tc.nr) {
|
|
t.Errorf("RandomStringFromCharRange failed. Unexpected character found in returned string: %s", rs)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetCharRangeFromConfig(t *testing.T) {
|
|
config := NewConfig()
|
|
generator := New(config)
|
|
testCases := []struct {
|
|
Name string
|
|
ConfigMode ModeMask
|
|
ExpectedRange string
|
|
}{
|
|
{
|
|
Name: "LowerCaseHumanReadable",
|
|
ConfigMode: ModeLowerCase | ModeHumanReadable,
|
|
ExpectedRange: CharRangeAlphaLowerHuman,
|
|
},
|
|
{
|
|
Name: "LowerCaseNonHumanReadable",
|
|
ConfigMode: ModeLowerCase,
|
|
ExpectedRange: CharRangeAlphaLower,
|
|
},
|
|
{
|
|
Name: "NumericHumanReadable",
|
|
ConfigMode: ModeNumeric | ModeHumanReadable,
|
|
ExpectedRange: CharRangeNumericHuman,
|
|
},
|
|
{
|
|
Name: "NumericNonHumanReadable",
|
|
ConfigMode: ModeNumeric,
|
|
ExpectedRange: CharRangeNumeric,
|
|
},
|
|
{
|
|
Name: "SpecialHumanReadable",
|
|
ConfigMode: ModeSpecial | ModeHumanReadable,
|
|
ExpectedRange: CharRangeSpecialHuman,
|
|
},
|
|
{
|
|
Name: "SpecialNonHumanReadable",
|
|
ConfigMode: ModeSpecial,
|
|
ExpectedRange: CharRangeSpecial,
|
|
},
|
|
{
|
|
Name: "UpperCaseHumanReadable",
|
|
ConfigMode: ModeUpperCase | ModeHumanReadable,
|
|
ExpectedRange: CharRangeAlphaUpperHuman,
|
|
},
|
|
{
|
|
Name: "UpperCaseNonHumanReadable",
|
|
ConfigMode: ModeUpperCase,
|
|
ExpectedRange: CharRangeAlphaUpper,
|
|
},
|
|
{
|
|
Name: "MultipleModes",
|
|
ConfigMode: ModeLowerCase | ModeNumeric | ModeUpperCase | ModeHumanReadable,
|
|
ExpectedRange: CharRangeAlphaLowerHuman + CharRangeNumericHuman + CharRangeAlphaUpperHuman,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.Name, func(t *testing.T) {
|
|
generator.config.Mode = tc.ConfigMode
|
|
actualRange := generator.GetCharRangeFromConfig()
|
|
if actualRange != tc.ExpectedRange {
|
|
t.Errorf("Expected range %s, got %s", tc.ExpectedRange, actualRange)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetPasswordLength(t *testing.T) {
|
|
config := NewConfig()
|
|
generator := New(config)
|
|
testCases := []struct {
|
|
Name string
|
|
ConfigFixedLength int64
|
|
ConfigMinLength int64
|
|
ConfigMaxLength int64
|
|
ExpectedLength int64
|
|
ExpectedError error
|
|
}{
|
|
{
|
|
Name: "FixedLength",
|
|
ConfigFixedLength: 10,
|
|
ConfigMinLength: 5,
|
|
ConfigMaxLength: 15,
|
|
ExpectedLength: 10,
|
|
ExpectedError: nil,
|
|
},
|
|
{
|
|
Name: "MinLengthEqualToMaxLength",
|
|
ConfigFixedLength: 0,
|
|
ConfigMinLength: 8,
|
|
ConfigMaxLength: 8,
|
|
ExpectedLength: 8,
|
|
ExpectedError: nil,
|
|
},
|
|
{
|
|
Name: "MinLengthGreaterThanMaxLength",
|
|
ConfigFixedLength: 0,
|
|
ConfigMinLength: 12,
|
|
ConfigMaxLength: 5,
|
|
ExpectedLength: 12,
|
|
ExpectedError: nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.Name, func(t *testing.T) {
|
|
generator.config.FixedLength = tc.ConfigFixedLength
|
|
generator.config.MinLength = tc.ConfigMinLength
|
|
generator.config.MaxLength = tc.ConfigMaxLength
|
|
length, err := generator.GetPasswordLength()
|
|
|
|
if err != nil && !errors.Is(err, tc.ExpectedError) {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
} else if err == nil && tc.ExpectedError != nil {
|
|
t.Errorf("Expected error %v, got nil", tc.ExpectedError)
|
|
} else if err == nil && length != tc.ExpectedLength {
|
|
t.Errorf("Expected length %d, got %d", tc.ExpectedLength, length)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGenerateCoinFlip tries to test the coinflip. Randomness is hard to
|
|
// test, since it's supposed to be not prodictable. We think that in 100k
|
|
// tries at least one of each two results should be returned, even though
|
|
// it is possible that not. Therefore we only throw a warning
|
|
func TestGenerateCoinFlip(t *testing.T) {
|
|
config := NewConfig()
|
|
generator := New(config)
|
|
foundTails := false
|
|
foundHeads := false
|
|
for range 100_000 {
|
|
res, err := generator.generateCoinFlip()
|
|
if err != nil {
|
|
t.Errorf("generateCoinFlip() failed: %s", err)
|
|
return
|
|
}
|
|
switch res {
|
|
case "Tails":
|
|
foundTails = true
|
|
case "Heads":
|
|
foundHeads = true
|
|
}
|
|
}
|
|
if !foundTails && !foundHeads {
|
|
t.Logf("WARNING: generateCoinFlip() was supposed to find heads and tails "+
|
|
"in 100_000 tries but didn't. Heads: %t, Tails: %t", foundHeads, foundTails)
|
|
}
|
|
}
|
|
|
|
func TestGeneratePronounceable(t *testing.T) {
|
|
config := NewConfig()
|
|
generator := New(config)
|
|
foundSylables := 0
|
|
for range 100 {
|
|
res, err := generator.generatePronounceable()
|
|
if err != nil {
|
|
t.Errorf("generatePronounceable() failed: %s", err)
|
|
return
|
|
}
|
|
for _, syl := range KoremutakeSyllables {
|
|
if strings.Contains(res, syl) {
|
|
foundSylables++
|
|
}
|
|
}
|
|
}
|
|
if foundSylables < 100 {
|
|
t.Errorf("generatePronounceable() failed, expected at least 1 sylable, got none")
|
|
}
|
|
|
|
}
|
|
|
|
func TestCheckMinimumRequirements(t *testing.T) {
|
|
config := NewConfig()
|
|
generator := New(config)
|
|
testCases := []struct {
|
|
Name string
|
|
Password string
|
|
ConfigMinLowerCase int64
|
|
ConfigMinNumeric int64
|
|
ConfigMinSpecial int64
|
|
ConfigMinUpperCase int64
|
|
ExpectedResult bool
|
|
}{
|
|
{
|
|
Name: "Meets all requirements",
|
|
Password: "Th1sIsA$trongP@ssword",
|
|
ConfigMinLowerCase: 2,
|
|
ConfigMinNumeric: 1,
|
|
ConfigMinSpecial: 1,
|
|
ConfigMinUpperCase: 1,
|
|
ExpectedResult: true,
|
|
},
|
|
{
|
|
Name: "Missing lowercase",
|
|
Password: "THISIS@STRONGPASSWORD",
|
|
ConfigMinLowerCase: 2,
|
|
ConfigMinNumeric: 1,
|
|
ConfigMinSpecial: 1,
|
|
ConfigMinUpperCase: 1,
|
|
ExpectedResult: false,
|
|
},
|
|
{
|
|
Name: "Missing numeric",
|
|
Password: "ThisIsA$trongPassword",
|
|
ConfigMinLowerCase: 2,
|
|
ConfigMinNumeric: 1,
|
|
ConfigMinSpecial: 1,
|
|
ConfigMinUpperCase: 1,
|
|
ExpectedResult: false,
|
|
},
|
|
{
|
|
Name: "Missing special",
|
|
Password: "ThisIsALowercaseNumericPassword",
|
|
ConfigMinLowerCase: 2,
|
|
ConfigMinNumeric: 1,
|
|
ConfigMinSpecial: 1,
|
|
ConfigMinUpperCase: 1,
|
|
ExpectedResult: false,
|
|
},
|
|
{
|
|
Name: "Missing uppercase",
|
|
Password: "thisisanumericspecialpassword",
|
|
ConfigMinLowerCase: 2,
|
|
ConfigMinNumeric: 1,
|
|
ConfigMinSpecial: 1,
|
|
ConfigMinUpperCase: 1,
|
|
ExpectedResult: false,
|
|
},
|
|
{
|
|
Name: "Bare minimum",
|
|
Password: "a1!",
|
|
ConfigMinLowerCase: 1,
|
|
ConfigMinNumeric: 1,
|
|
ConfigMinSpecial: 1,
|
|
ConfigMinUpperCase: 0,
|
|
ExpectedResult: true,
|
|
},
|
|
{
|
|
Name: "Empty password",
|
|
Password: "",
|
|
ConfigMinLowerCase: 1,
|
|
ConfigMinNumeric: 1,
|
|
ConfigMinSpecial: 1,
|
|
ConfigMinUpperCase: 1,
|
|
ExpectedResult: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.Name, func(t *testing.T) {
|
|
generator.config.MinLowerCase = tc.ConfigMinLowerCase
|
|
generator.config.MinNumeric = tc.ConfigMinNumeric
|
|
generator.config.MinSpecial = tc.ConfigMinSpecial
|
|
generator.config.MinUpperCase = tc.ConfigMinUpperCase
|
|
result := generator.checkMinimumRequirements(tc.Password)
|
|
if result != tc.ExpectedResult {
|
|
t.Errorf("Expected result %v, got %v", tc.ExpectedResult, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGenerateRandom(t *testing.T) {
|
|
config := NewConfig(WithAlgorithm(AlgoRandom), WithNumberPass(1),
|
|
WithMinLength(1), WithMaxLength(1))
|
|
config.MinNumeric = 1
|
|
generator := New(config)
|
|
pw, err := generator.generateRandom()
|
|
if err != nil {
|
|
t.Errorf("generateRandom() failed: %s", err)
|
|
}
|
|
if len(pw) > 1 {
|
|
t.Errorf("expected password with length 1 but got: %d", len(pw))
|
|
}
|
|
n, err := strconv.Atoi(pw)
|
|
if err != nil {
|
|
t.Errorf("expected password to be a number but got an error: %s", err)
|
|
}
|
|
if n < 0 || n > 9 {
|
|
t.Errorf("expected password to be a number between 0 and 9, got: %d", n)
|
|
}
|
|
}
|
|
|
|
func TestGenerate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
algorithm Algorithm
|
|
expectedErr error
|
|
}{
|
|
{
|
|
name: "Pronounceable",
|
|
algorithm: AlgoPronounceable,
|
|
},
|
|
{
|
|
name: "CoinFlip",
|
|
algorithm: AlgoCoinFlip,
|
|
},
|
|
{
|
|
name: "Random",
|
|
algorithm: AlgoRandom,
|
|
},
|
|
{
|
|
name: "Unsupported",
|
|
algorithm: AlgoUnsupported,
|
|
expectedErr: fmt.Errorf("unsupported algorithm"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := NewConfig(WithAlgorithm(tt.algorithm))
|
|
g := New(config)
|
|
_, err := g.Generate()
|
|
if tt.expectedErr != nil {
|
|
if err == nil || err.Error() != tt.expectedErr.Error() {
|
|
t.Errorf("Expected error: %s, got: %s", tt.expectedErr, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Errorf("Unexpected error: %s", err)
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkGenerator_CoinFlip(b *testing.B) {
|
|
b.ReportAllocs()
|
|
g := New(NewConfig())
|
|
for i := 0; i < b.N; i++ {
|
|
_ = g.CoinFlip()
|
|
}
|
|
}
|
|
|
|
func BenchmarkGenerator_RandomBytes(b *testing.B) {
|
|
b.ReportAllocs()
|
|
g := New(NewConfig())
|
|
var l int64 = 1024
|
|
for i := 0; i < b.N; i++ {
|
|
_, err := g.RandomBytes(l)
|
|
if err != nil {
|
|
b.Errorf("failed to generate random bytes: %s", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkGenerator_RandomString(b *testing.B) {
|
|
b.ReportAllocs()
|
|
g := New(NewConfig())
|
|
cr := CharRangeAlphaUpper + CharRangeAlphaLower + CharRangeNumeric + CharRangeSpecial
|
|
for i := 0; i < b.N; i++ {
|
|
_, err := g.RandomStringFromCharRange(32, cr)
|
|
if err != nil {
|
|
b.Errorf("RandomStringFromCharRange() failed: %s", err)
|
|
}
|
|
}
|
|
}
|