From 22fe517baa8b6939503c0c804dd71628f7d473a3 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Fri, 17 Dec 2021 01:52:07 +0000 Subject: [PATCH] feat(randsmust): add randsmust 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. Internally the package simply calls the equivalent function from the rands package, and panics if a error is returned. --- README.md | 65 +++- rands.go | 3 + randsmust/bytes.go | 13 + randsmust/bytes_example_test.go | 12 + randsmust/bytes_test.go | 19 + randsmust/ints.go | 23 ++ randsmust/ints_example_test.go | 17 + randsmust/ints_test.go | 144 ++++++++ randsmust/randsmust.go | 15 + randsmust/randsmust_test.go | 30 ++ randsmust/strings.go | 192 ++++++++++ randsmust/strings_example_test.go | 77 +++++ randsmust/strings_test.go | 558 ++++++++++++++++++++++++++++++ 13 files changed, 1162 insertions(+), 6 deletions(-) create mode 100644 randsmust/bytes.go create mode 100644 randsmust/bytes_example_test.go create mode 100644 randsmust/bytes_test.go create mode 100644 randsmust/ints.go create mode 100644 randsmust/ints_example_test.go create mode 100644 randsmust/ints_test.go create mode 100644 randsmust/randsmust.go create mode 100644 randsmust/randsmust_test.go create mode 100644 randsmust/strings.go create mode 100644 randsmust/strings_example_test.go create mode 100644 randsmust/strings_test.go 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, + ) +}