diff --git a/README.md b/README.md index d2f4726..365760d 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,22 @@ alt="GitHub issues">

+## [`rands`](https://pkg.go.dev/github.com/jimeh/rands) package + +`rands` is intended for use in production code where random data generation is +required. All functions have a error return value which should be checked. + +For tests there is the `randsmust` package, which has all the same functions but +with single return values, and they panic in the event of an error. + +### Import + +``` +import "github.com/jimeh/rands" +``` + +### Usage + ```go s, err := rands.Base64(16) // => CYxqEdUB1Rzno3SyZu2g/g== s, err := rands.Base64URL(16) // => zlqw9aFqcFggbk2asn3_aQ @@ -60,17 +76,54 @@ n, err := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233 b, err := rands.Bytes(8) // => [0 220 137 243 135 204 34 63] ``` -## Import +## [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/must) package + +`randsmust` is specifically intended as an alternative to `rands` for use in +tests. All functions return a single value, and panic in the event of an error. +This makes them easy to use when building structs in test cases that need random +data. + +For production code, make sure to use the `rands` package and check returned +errors. + +### Import ``` -import "github.com/jimeh/rands" +import "github.com/jimeh/rands/randsmust" +``` + +### Usage + +```go +s := randsmust.Base64(16) // => d1wm/wS6AQGduO3uaey1Cg== +s := randsmust.Base64URL(16) // => 4pHWVcddXsL_45vhOfCdng +s := randsmust.Hex(16) // => b5552558bc009264d129c422a666fe56 +s := randsmust.Alphanumeric(16) // => j5WkpNKmW8K701XF +s := randsmust.Alphabetic(16) // => OXxsqfFjNLvmZqDb +s := randsmust.Upper(16) // => AOTLYQRCVNMEPRCX +s := randsmust.UpperNumeric(16) // => 1NTY6KATDVAXBTY2 +s := randsmust.Lower(16) // => xmftrwvurrritqfu +s := randsmust.LowerNumeric(16) // => yszg56fzeql7pjpl +s := randsmust.Numeric(16) // => 0761782105447226 + +s := randsmust.String(16, "abcdefABCDEF") // => dfAbBfaDDdDFDaEa +s := randsmust.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) // => 十十千口三十十下九上千口七夕土口 + +s := randsmust.DNSLabel(16) // => pu31o0gqyk76x35f +s := randsmust.UUID() // => d616c873-f3dd-4690-bcd6-ed307eec1105 + +n := randsmust.Int(2147483647) // => 1293388115 +n := randsmust.Int64(int64(9223372036854775807)) // => 6168113630900161239 + +b := randsmust.Bytes(8) // => [205 128 54 95 0 95 53 51] ``` ## Documentation -Please see the -[Go Reference](https://pkg.go.dev/github.com/jimeh/rands#section-documentation) -for documentation and examples. +Please see the Go Reference for documentation and examples: + +- [`rands`](https://pkg.go.dev/github.com/jimeh/rands) +- [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/must) ## Benchmarks @@ -79,4 +132,4 @@ https://jimeh.me/rands/dev/bench/ ## License -[MIT](https://github.com/jimeh/rands/blob/master/LICENSE) +[MIT](https://github.com/jimeh/rands/blob/main/LICENSE) diff --git a/rands.go b/rands.go index beb394e..a21dd3e 100644 --- a/rands.go +++ b/rands.go @@ -8,6 +8,9 @@ // rands is intended for use in production code where random data generation is // required. All functions have a error return value, which should be // checked. +// +// For tests there is the randsmust package, which has all the same functions +// but with single return values, and they panic in the event of an error. package rands import "errors" diff --git a/randsmust/bytes.go b/randsmust/bytes.go new file mode 100644 index 0000000..cfe163c --- /dev/null +++ b/randsmust/bytes.go @@ -0,0 +1,13 @@ +package randsmust + +import "github.com/jimeh/rands" + +// Bytes generates a byte slice of n number of random bytes. +func Bytes(n int) []byte { + r, err := rands.Bytes(n) + if err != nil { + panic(err) + } + + return r +} diff --git a/randsmust/bytes_example_test.go b/randsmust/bytes_example_test.go new file mode 100644 index 0000000..edba62d --- /dev/null +++ b/randsmust/bytes_example_test.go @@ -0,0 +1,12 @@ +package randsmust_test + +import ( + "fmt" + + "github.com/jimeh/rands/randsmust" +) + +func ExampleBytes() { + b := randsmust.Bytes(8) + fmt.Printf("%+v\n", b) // => [6 99 106 54 163 188 28 152] +} diff --git a/randsmust/bytes_test.go b/randsmust/bytes_test.go new file mode 100644 index 0000000..983ca56 --- /dev/null +++ b/randsmust/bytes_test.go @@ -0,0 +1,19 @@ +package randsmust + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBytes(t *testing.T) { + t.Parallel() + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Bytes(tt.n) + + assert.Len(t, got, tt.n) + }) + } +} diff --git a/randsmust/ints.go b/randsmust/ints.go new file mode 100644 index 0000000..83ffcac --- /dev/null +++ b/randsmust/ints.go @@ -0,0 +1,23 @@ +package randsmust + +import "github.com/jimeh/rands" + +// Int generates a random int ranging between 0 and max. +func Int(max int) int { + r, err := rands.Int(max) + if err != nil { + panic(err) + } + + return r +} + +// Int64 generates a random int64 ranging between 0 and max. +func Int64(max int64) int64 { + r, err := rands.Int64(max) + if err != nil { + panic(err) + } + + return r +} diff --git a/randsmust/ints_example_test.go b/randsmust/ints_example_test.go new file mode 100644 index 0000000..6f7b572 --- /dev/null +++ b/randsmust/ints_example_test.go @@ -0,0 +1,17 @@ +package randsmust_test + +import ( + "fmt" + + "github.com/jimeh/rands/randsmust" +) + +func ExampleInt() { + n := randsmust.Int(2147483647) + fmt.Printf("%d\n", n) // => 1616989970 +} + +func ExampleInt64() { + n := randsmust.Int64(int64(9223372036854775807)) + fmt.Printf("%d\n", n) // => 1599573251306894157 +} diff --git a/randsmust/ints_test.go b/randsmust/ints_test.go new file mode 100644 index 0000000..ecc410a --- /dev/null +++ b/randsmust/ints_test.go @@ -0,0 +1,144 @@ +package randsmust + +import ( + "testing" + + "github.com/jimeh/rands" + "github.com/stretchr/testify/assert" +) + +var testIntCases = []struct { + name string + max int + panicErrIs error + panicStr string +}{ + { + name: "n=-2394345", + max: -2394345, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-409600", + max: -409600, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-1024", + max: -1024, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-128", + max: -128, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-32", + max: -32, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-16", + max: -16, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-8", + max: -8, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-7", + max: -7, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-2", + max: -2, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=-1", + max: -1, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + { + name: "n=0", + max: 0, + panicErrIs: rands.ErrInvalidMaxInt, + panicStr: "rands: max cannot be less than 1", + }, + {name: "n=1", max: 1}, + {name: "n=2", max: 2}, + {name: "n=7", max: 7}, + {name: "n=8", max: 8}, + {name: "n=16", max: 16}, + {name: "n=32", max: 32}, + {name: "n=128", max: 128}, + {name: "n=1024", max: 1024}, + {name: "n=409600", max: 409600}, + {name: "n=2394345", max: 2394345}, +} + +func TestInt(t *testing.T) { + t.Parallel() + + for _, tt := range testIntCases { + t.Run(tt.name, func(t *testing.T) { + var got int + p := recoverPanic(func() { + got = Int(tt.max) + }) + + if tt.panicErrIs == nil || tt.panicStr == "" { + assert.GreaterOrEqual(t, got, 0) + assert.LessOrEqual(t, got, tt.max) + } + + if tt.panicErrIs != nil { + assert.ErrorIs(t, p.(error), tt.panicErrIs) + } + + if tt.panicStr != "" { + assert.EqualError(t, p.(error), tt.panicStr) + } + }) + } +} + +func TestInt64(t *testing.T) { + t.Parallel() + + for _, tt := range testIntCases { + t.Run(tt.name, func(t *testing.T) { + var got int64 + p := recoverPanic(func() { + got = Int64(int64(tt.max)) + }) + + if tt.panicErrIs == nil || tt.panicStr == "" { + assert.GreaterOrEqual(t, got, int64(0)) + assert.LessOrEqual(t, got, int64(tt.max)) + } + + if tt.panicErrIs != nil { + assert.ErrorIs(t, p.(error), tt.panicErrIs) + } + + if tt.panicStr != "" { + assert.EqualError(t, p.(error), tt.panicStr) + } + }) + } +} diff --git a/randsmust/randsmust.go b/randsmust/randsmust.go new file mode 100644 index 0000000..66bb288 --- /dev/null +++ b/randsmust/randsmust.go @@ -0,0 +1,15 @@ +// Package randsmust provides a suite of functions that use crypto/rand to +// generate cryptographically secure random strings in various formats, as well +// as ints and bytes. +// +// All functions which produce strings from a alphabet of characters uses +// rand.Int() to ensure a uniform distribution of all possible values. +// +// randsmust is specifically intended as an alternative to rands for use in +// tests. All functions return a single value, and panic in the event of an +// error. This makes them easy to use when building structs in test cases that +// need random data. +// +// For production code, make sure to use the rands package and check returned +// errors. +package randsmust diff --git a/randsmust/randsmust_test.go b/randsmust/randsmust_test.go new file mode 100644 index 0000000..e486934 --- /dev/null +++ b/randsmust/randsmust_test.go @@ -0,0 +1,30 @@ +package randsmust + +var testCases = []struct { + name string + n int +}{ + {name: "n=0", n: 0}, + {name: "n=1", n: 1}, + {name: "n=2", n: 2}, + {name: "n=7", n: 7}, + {name: "n=8", n: 8}, + {name: "n=16", n: 16}, + {name: "n=32", n: 32}, + {name: "n=128", n: 128}, + {name: "n=1024", n: 1024}, + {name: "n=409600", n: 409600}, + {name: "n=2394345", n: 2394345}, +} + +func recoverPanic(f func()) (p interface{}) { + defer func() { + if r := recover(); r != nil { + p = r + } + }() + + f() + + return +} diff --git a/randsmust/strings.go b/randsmust/strings.go new file mode 100644 index 0000000..9ba687f --- /dev/null +++ b/randsmust/strings.go @@ -0,0 +1,192 @@ +package randsmust + +import ( + "github.com/jimeh/rands" +) + +// Base64 generates a random base64 encoded string of n number of bytes. +// +// Length of the returned string is about one third greater than the value of n, +// and it may contain characters A-Z, a-z, 0-9, "+", "/", and "=". +func Base64(n int) string { + r, err := rands.Base64(n) + if err != nil { + panic(err) + } + + return r +} + +// Base64URL generates a URL-safe un-padded random base64 encoded string of n +// number of bytes. +// +// Length of the returned string is about one third greater than the value of n, +// and it may contain characters A-Z, a-z, 0-9, "-", and "_". +func Base64URL(n int) string { + r, err := rands.Base64URL(n) + if err != nil { + panic(err) + } + + return r +} + +// Hex generates a random hexadecimal encoded string of n number of bytes. +// +// Length of the returned string is twice the value of n, and it may contain +// characters 0-9 and a-f. +func Hex(n int) string { + r, err := rands.Hex(n) + if err != nil { + panic(err) + } + + return r +} + +// Alphanumeric generates a random alphanumeric string of n length. +// +// The returned string may contain A-Z, a-z, and 0-9. +func Alphanumeric(n int) string { + r, err := rands.Alphanumeric(n) + if err != nil { + panic(err) + } + + return r +} + +// Alphabetic generates a random alphabetic string of n length. +// +// The returned string may contain A-Z, and a-z. +func Alphabetic(n int) string { + r, err := rands.Alphabetic(n) + if err != nil { + panic(err) + } + + return r +} + +// Numeric generates a random numeric string of n length. +// +// The returned string may contain 0-9. +func Numeric(n int) string { + r, err := rands.Numeric(n) + if err != nil { + panic(err) + } + + return r +} + +// Upper generates a random uppercase alphabetic string of n length. +// +// The returned string may contain A-Z. +func Upper(n int) string { + r, err := rands.Upper(n) + if err != nil { + panic(err) + } + + return r +} + +// UpperNumeric generates a random uppercase alphanumeric string of n length. +// +// The returned string may contain A-Z and 0-9. +func UpperNumeric(n int) string { + r, err := rands.UpperNumeric(n) + if err != nil { + panic(err) + } + + return r +} + +// Lower generates a random lowercase alphabetic string of n length. +// +// The returned string may contain a-z. +func Lower(n int) string { + r, err := rands.Lower(n) + if err != nil { + panic(err) + } + + return r +} + +// LowerNumeric generates a random lowercase alphanumeric string of n length. +// +// The returned string may contain A-Z and 0-9. +func LowerNumeric(n int) string { + r, err := rands.LowerNumeric(n) + if err != nil { + panic(err) + } + + return r +} + +// String generates a random string of n length using the given ASCII alphabet. +// +// The specified alphabet determines what characters are used in the returned +// random string. The alphabet can only contain ASCII characters, use +// UnicodeString() if you need a alphabet with Unicode characters. +func String(n int, alphabet string) string { + r, err := rands.String(n, alphabet) + if err != nil { + panic(err) + } + + return r +} + +// UnicodeString generates a random string of n length using the given Unicode +// alphabet. +// +// The specified alphabet determines what characters are used in the returned +// random string. The length of the returned string will be n or greater +// depending on the byte-length of characters which were randomly selected from +// the alphabet. +func UnicodeString(n int, alphabet []rune) string { + r, err := rands.UnicodeString(n, alphabet) + if err != nil { + panic(err) + } + + return r +} + +// DNSLabel returns a random string of n length in a DNS label compliant format +// as defined in RFC 1035, section 2.3.1. +// +// It also adheres to RFC 5891, section 4.2.3.1. +// +// In summary, the generated random string will: +// +// - be between 1 and 63 characters in length, other n values returns a error +// - first character will be one of a-z +// - last character will be one of a-z or 0-9 +// - in-between first and last characters consist of a-z, 0-9, or "-" +// - potentially contain two or more consecutive "-", except the 3rd and 4th +// characters, as that would violate RFC 5891, section 4.2.3.1. +func DNSLabel(n int) string { + r, err := rands.DNSLabel(n) + if err != nil { + panic(err) + } + + return r +} + +// UUID returns a random UUID v4 in string format as defined by RFC 4122, +// section 4.4. +func UUID() string { + r, err := rands.UUID() + if err != nil { + panic(err) + } + + return r +} diff --git a/randsmust/strings_example_test.go b/randsmust/strings_example_test.go new file mode 100644 index 0000000..01e78fd --- /dev/null +++ b/randsmust/strings_example_test.go @@ -0,0 +1,77 @@ +package randsmust_test + +import ( + "fmt" + + "github.com/jimeh/rands/randsmust" +) + +func ExampleBase64() { + s := randsmust.Base64(16) + fmt.Println(s) // => rGnZOxJunCd5h+piBpOfDA== +} + +func ExampleBase64URL() { + s := randsmust.Base64URL(16) + fmt.Println(s) // => NlXKmutou2knLU8q7Hlp5Q +} + +func ExampleHex() { + s := randsmust.Hex(16) + fmt.Println(s) // => 1013ec67a802be177d3e37f46951e97f +} + +func ExampleAlphanumeric() { + s := randsmust.Alphanumeric(16) + fmt.Println(s) // => mjT119HdPslVfvUE +} + +func ExampleAlphabetic() { + s := randsmust.Alphabetic(16) + fmt.Println(s) // => RLaRaTVqcrxvNkiz +} + +func ExampleUpper() { + s := randsmust.Upper(16) + fmt.Println(s) // => CANJDLMHANPQNXUE +} + +func ExampleUpperNumeric() { + s := randsmust.UpperNumeric(16) + fmt.Println(s) // => EERZHC96KOIRU9DM +} + +func ExampleLower() { + s := randsmust.Lower(16) + fmt.Println(s) // => aoybqdwigyezucjy +} + +func ExampleLowerNumeric() { + s := randsmust.LowerNumeric(16) + fmt.Println(s) // => hs8l2l0750med3g2 +} + +func ExampleNumeric() { + s := randsmust.Numeric(16) + fmt.Println(s) // => 3126402104379869 +} + +func ExampleString() { + s := randsmust.String(16, "abcdefABCDEF") + fmt.Println(s) // => cbdCAbABECaADcaB +} + +func ExampleUnicodeString() { + s := randsmust.UnicodeString(16, []rune("九七二人入八力十下三千上口土夕大")) + fmt.Println(s) // => 下夕七下千千力入八三力夕千三土七 +} + +func ExampleDNSLabel() { + s := randsmust.DNSLabel(16) + fmt.Println(s) // => urqkt-remuwz5083 +} + +func ExampleUUID() { + s := randsmust.UUID() + fmt.Println(s) // => 5baa35a6-9a46-49b4-91d0-9530173e118d +} diff --git a/randsmust/strings_test.go b/randsmust/strings_test.go new file mode 100644 index 0000000..cebaafe --- /dev/null +++ b/randsmust/strings_test.go @@ -0,0 +1,558 @@ +package randsmust + +import ( + "encoding/base64" + "encoding/hex" + "regexp" + "strings" + "testing" + + "github.com/jimeh/rands" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHex(t *testing.T) { + t.Parallel() + + allowed := "0123456789abcdef" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Hex(tt.n) + + assert.Len(t, got, tt.n*2) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestBase64(t *testing.T) { + t.Parallel() + + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + "0123456789+/=" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Base64(tt.n) + + b, err := base64.StdEncoding.DecodeString(got) + require.NoError(t, err) + + assert.Len(t, b, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestBase64URL(t *testing.T) { + t.Parallel() + + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + "0123456789-_" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Base64URL(tt.n) + + b, err := base64.RawURLEncoding.DecodeString(got) + require.NoError(t, err) + + assert.Len(t, b, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestAlphanumeric(t *testing.T) { + t.Parallel() + + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Alphanumeric(tt.n) + + assert.Len(t, got, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestAlphabetic(t *testing.T) { + t.Parallel() + + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Alphabetic(tt.n) + + assert.Len(t, got, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestNumeric(t *testing.T) { + t.Parallel() + + allowed := "0123456789" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Numeric(tt.n) + + assert.Len(t, got, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestUpper(t *testing.T) { + t.Parallel() + + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Upper(tt.n) + + assert.Len(t, got, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestUpperNumeric(t *testing.T) { + t.Parallel() + + allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := UpperNumeric(tt.n) + + assert.Len(t, got, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestLower(t *testing.T) { + t.Parallel() + + allowed := "abcdefghijklmnopqrstuvwxyz" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := Lower(tt.n) + + assert.Len(t, got, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +func TestLowerNumeric(t *testing.T) { + t.Parallel() + + allowed := "abcdefghijklmnopqrstuvwxyz0123456789" + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + got := LowerNumeric(tt.n) + + assert.Len(t, got, tt.n) + assertAllowedChars(t, allowed, got) + }) + } +} + +var stringTestCases = []struct { + name string + n int + alphabet string + panicErrIs error + panicStr string +}{ + { + name: "greek", + n: 32, + alphabet: "αβγδεζηθικλμνξοπρστυφχψωςΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΩ" + + "άίόύώέϊϋΐΰΆΈΌΏΎΊ", + panicErrIs: rands.ErrNonASCIIAlphabet, + panicStr: "rands: alphabet contains non-ASCII characters", + }, + { + name: "chinese", + n: 32, + alphabet: "的一是不了人我在有他这为之大来以个中上们到说国和地也子" + + "时道出而要于就下得可你年生", + panicErrIs: rands.ErrNonASCIIAlphabet, + panicStr: "rands: alphabet contains non-ASCII characters", + }, + { + name: "japanese", + n: 32, + alphabet: "一九七二人入八力十下三千上口土夕大女子小山川五天中六円" + + "手文日月木水火犬王正出本右四", + panicErrIs: rands.ErrNonASCIIAlphabet, + panicStr: "rands: alphabet contains non-ASCII characters", + }, + { + name: "n=0", + n: 0, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=1", + n: 1, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=2", + n: 2, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=7", + n: 7, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=8", + n: 8, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=16", + n: 16, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=32", + n: 32, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=128", + n: 128, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=1024", + n: 1024, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=409600", + n: 409600, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "n=2394345", + n: 2394345, + alphabet: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-", + }, + { + name: "uppercase", + n: 16, + alphabet: "ABCDEFGHJKMNPRSTUVWXYZ", + }, + { + name: "lowercase", + n: 16, + alphabet: "abcdefghjkmnprstuvwxyz", + }, +} + +func TestString(t *testing.T) { + t.Parallel() + + for _, tt := range stringTestCases { + t.Run(tt.name, func(t *testing.T) { + var got string + p := recoverPanic(func() { + got = String(tt.n, tt.alphabet) + }) + + if tt.panicErrIs == nil || tt.panicStr == "" { + assert.Len(t, []rune(got), tt.n) + assertAllowedChars(t, tt.alphabet, got) + } + + if tt.panicErrIs != nil { + assert.ErrorIs(t, p.(error), tt.panicErrIs) + } + + if tt.panicStr != "" { + assert.EqualError(t, p.(error), tt.panicStr) + } + }) + } +} + +var unicodeStringTestCases = []struct { + name string + n int + alphabet string +}{ + { + name: "n=0", + n: 0, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=1", + n: 1, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=2", + n: 2, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=7", + n: 7, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=8", + n: 8, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=16", + n: 16, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=32", + n: 32, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=128", + n: 128, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=1024", + n: 1024, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=409600", + n: 409600, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "n=2394345", + n: 2394345, + alphabet: "αβγδεζηθικλμν时道出而要于就下得可你年" + + "手文日月木水火犬王正出本右", + }, + { + name: "latin", + n: 32, + alphabet: "ABCDEFGHJKMNPRSTUVWXYZabcdefghjkmnprstuvwxyz", + }, + { + name: "greek", + n: 32, + alphabet: "αβγδεζηθικλμνξοπρστυφχψωςΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΩ" + + "άίόύώέϊϋΐΰΆΈΌΏΎΊ", + }, + { + name: "chinese", + n: 32, + alphabet: "的一是不了人我在有他这为之大来以个中上们到说国和地也子" + + "时道出而要于就下得可你年生", + }, + { + name: "japanese", + n: 32, + alphabet: "一九七二人入八力十下三千上口土夕大女子小山川五天中六円" + + "手文日月木水火犬王正出本右四", + }, +} + +func TestUnicodeString(t *testing.T) { + t.Parallel() + + for _, tt := range unicodeStringTestCases { + t.Run(tt.name, func(t *testing.T) { + got := UnicodeString(tt.n, []rune(tt.alphabet)) + + assert.Len(t, []rune(got), tt.n) + assertAllowedChars(t, tt.alphabet, got) + }) + } +} + +var dnsLabelTestCases = []struct { + name string + n int + panicErrIs error + panicStr string +}{ + { + name: "n=-128", + n: -128, + panicErrIs: rands.ErrDNSLabelLength, + panicStr: "rands: DNS labels must be between 1 and 63 characters " + + "in length", + }, + { + name: "n=0", + n: 0, + panicErrIs: rands.ErrDNSLabelLength, + panicStr: "rands: DNS labels must be between 1 and 63 characters " + + "in length", + }, + {name: "n=1", n: 1}, + {name: "n=2", n: 2}, + {name: "n=3", n: 3}, + {name: "n=4", n: 4}, + {name: "n=5", n: 5}, + {name: "n=6", n: 6}, + {name: "n=7", n: 7}, + {name: "n=8", n: 8}, + {name: "n=16", n: 16}, + {name: "n=32", n: 32}, + {name: "n=63", n: 63}, + { + name: "n=64", + n: 64, + panicErrIs: rands.ErrDNSLabelLength, + panicStr: "rands: DNS labels must be between 1 and 63 characters " + + "in length", + }, + { + name: "n=128", + n: 128, + panicErrIs: rands.ErrDNSLabelLength, + panicStr: "rands: DNS labels must be between 1 and 63 characters " + + "in length", + }, +} + +func TestDNSLabel(t *testing.T) { + t.Parallel() + + for _, tt := range dnsLabelTestCases { + t.Run(tt.name, func(t *testing.T) { + // generate lots of labels to increase the chances of catching any + // obscure bugs + for i := 0; i < 10000; i++ { + var got string + p := recoverPanic(func() { + got = DNSLabel(tt.n) + }) + + if tt.panicErrIs == nil || tt.panicStr == "" { + require.Len(t, got, tt.n) + asserDNSLabel(t, got) + } + + if tt.panicErrIs != nil { + require.ErrorIs(t, p.(error), tt.panicErrIs) + } + + if tt.panicStr != "" { + require.EqualError(t, p.(error), tt.panicStr) + } + } + }) + } +} + +func TestUUID(t *testing.T) { + t.Parallel() + + m := regexp.MustCompile( + `^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$`, + ) + + for i := 0; i < 10000; i++ { + got := UUID() + require.Regexp(t, m, got) + + raw := strings.ReplaceAll(got, "-", "") + b := make([]byte, 16) + _, err := hex.Decode(b, []byte(raw)) + require.NoError(t, err) + + require.Equal(t, 4, int(b[6]>>4), "version is not 4") + require.Equal(t, byte(0x80), b[8]&0xc0, + "variant is not RFC 4122", + ) + } +} + +// +// Helpers +// + +var ( + dnsLabelHeadRx = regexp.MustCompile(`^[a-z]$`) + dnsLabelBodyRx = regexp.MustCompile(`^[a-z0-9-]+$`) + dnsLabelTailRx = regexp.MustCompile(`^[a-z0-9]$`) +) + +func asserDNSLabel(t *testing.T, label string) { + require.LessOrEqualf(t, len(label), 63, + `DNS label "%s" is longer than 63 characters`, label, + ) + + require.GreaterOrEqualf(t, len(label), 1, + `DNS label "%s" is shorter than 1 character`, label, + ) + + if len(label) >= 1 { + require.Regexpf(t, dnsLabelHeadRx, string(label[0]), + `DNS label "%s" must start with a-z`, label, + ) + } + if len(label) >= 2 { + require.Regexpf(t, dnsLabelTailRx, string(label[len(label)-1]), + `DNS label "%s" must end with a-z0-9`, label, + ) + } + if len(label) >= 3 { + require.Regexpf(t, dnsLabelBodyRx, label[1:len(label)-1], + `DNS label "%s" body must only contain a-z0-9-`, label) + } + if len(label) >= 4 { + require.NotEqualf(t, "--", label[2:4], + `DNS label "%s" cannot contain "--" as 3rd and 4th char`, label, + ) + } +} + +func assertAllowedChars(t *testing.T, allowed string, s string) { + invalid := "" + for _, c := range s { + if !strings.Contains(allowed, string(c)) && + !strings.Contains(invalid, string(c)) { + invalid += string(c) + } + } + + assert.Truef( + t, len(invalid) == 0, "string contains invalid chars: %s", invalid, + ) +}