From c7f333b5de5a7e76d34ce39b521738751b515f74 Mon Sep 17 00:00:00 2001 From: Michael Golowka <72365+pcman312@users.noreply.github.com> Date: Fri, 11 Dec 2020 13:23:08 -0700 Subject: [PATCH] Add template helper library (#10500) --- go.sum | 2 - sdk/go.sum | 9 - sdk/helper/template/funcs.go | 68 +++++ sdk/helper/template/funcs_test.go | 356 +++++++++++++++++++++++++++ sdk/helper/template/template.go | 140 +++++++++++ sdk/helper/template/template_test.go | 226 +++++++++++++++++ 6 files changed, 790 insertions(+), 11 deletions(-) create mode 100644 sdk/helper/template/funcs.go create mode 100644 sdk/helper/template/funcs_test.go create mode 100644 sdk/helper/template/template.go create mode 100644 sdk/helper/template/template_test.go diff --git a/go.sum b/go.sum index ad2532642721..f865a6a9058e 100644 --- a/go.sum +++ b/go.sum @@ -601,8 +601,6 @@ github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8 github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/hcl v1.0.1-0.20201015155626-f6b4bdc86722 h1:IqlCLKd1vAIwMfU9ZEbITnuhQr24N/GUP0K2+E6SZ34= -github.com/hashicorp/hcl v1.0.1-0.20201015155626-f6b4bdc86722/go.mod h1:gwlu9+/P9MmKtYrMsHeFRZPXj2CTPm11TDnMeaRHS7g= github.com/hashicorp/hcl v1.0.1-vault h1:UiJeEzCWAYdVaJr8Xo4lBkTozlW1+1yxVUnpbS1xVEk= github.com/hashicorp/hcl v1.0.1-vault/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= diff --git a/sdk/go.sum b/sdk/go.sum index 556433fbeaf7..259bd7dd17f8 100644 --- a/sdk/go.sum +++ b/sdk/go.sum @@ -157,8 +157,6 @@ github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.1 h1:DMo4fmknnz0E0evoNYnV48RjWndOsmd6OW+09R3cEP8= github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -173,14 +171,9 @@ github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8 github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/vault/api v0.0.0-20201001211907-38d91b749c77 h1:ksVfuH3dKQPzqBH+LDVYMXeYC8rzuz526qBIQrIps7U= -github.com/hashicorp/vault/api v0.0.0-20201001211907-38d91b749c77/go.mod h1:R3Umvhlxi2TN7Ex2hzOowyeNb+SfbVWI973N+ctaFMk= -github.com/hashicorp/vault/api v0.0.0-20201001212527-2e121bafe1e4 h1:jNcfITMv2iB4wm+VGeB3uWGf8Gz3YXFHQJOAQZPjpVU= -github.com/hashicorp/vault/api v0.0.0-20201001212527-2e121bafe1e4/go.mod h1:R3Umvhlxi2TN7Ex2hzOowyeNb+SfbVWI973N+ctaFMk= github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f h1:PYtnlUZzFSZxPcq7mYp5oC9N+BcJ8IKYf6/EG0GHM2Y= github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f/go.mod h1:euTFbi2YJgwcju3imEt919lhJKF68nN1cQPq3aA+kBE= github.com/hashicorp/vault/sdk v0.1.14-0.20200519221530-14615acda45f/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10= -github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -414,8 +407,6 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= diff --git a/sdk/helper/template/funcs.go b/sdk/helper/template/funcs.go new file mode 100644 index 000000000000..8adb5109cc7a --- /dev/null +++ b/sdk/helper/template/funcs.go @@ -0,0 +1,68 @@ +package template + +import ( + "crypto/sha256" + "fmt" + "strconv" + "strings" + "time" + + UUID "github.com/hashicorp/go-uuid" +) + +func nowSeconds() string { + return strconv.FormatInt(time.Now().Unix(), 10) +} + +func nowNano() string { + return strconv.FormatInt(time.Now().UnixNano(), 10) +} + +func truncate(maxLen int, str string) (string, error) { + if maxLen <= 0 { + return "", fmt.Errorf("max length must be > 0 but was %d", maxLen) + } + if len(str) > maxLen { + return str[:maxLen], nil + } + return str, nil +} + +const ( + sha256HashLen = 8 +) + +func truncateSHA256(maxLen int, str string) (string, error) { + if maxLen <= 8 { + return "", fmt.Errorf("max length must be > 8 but was %d", maxLen) + } + + if len(str) <= maxLen { + return str, nil + } + + truncIndex := maxLen - sha256HashLen + hash := hashSHA256(str[truncIndex:]) + result := fmt.Sprintf("%s%s", str[:truncIndex], hash[:sha256HashLen]) + return result, nil +} + +func hashSHA256(str string) string { + return fmt.Sprintf("%x", sha256.Sum256([]byte(str))) +} + +func uppercase(str string) string { + return strings.ToUpper(str) +} + +func lowercase(str string) string { + return strings.ToLower(str) +} + +func replace(find string, replace string, str string) string { + return strings.ReplaceAll(str, find, replace) +} + +func uuid() (string, error) { + return UUID.GenerateUUID() +} diff --git a/sdk/helper/template/funcs_test.go b/sdk/helper/template/funcs_test.go new file mode 100644 index 000000000000..c6c87b444259 --- /dev/null +++ b/sdk/helper/template/funcs_test.go @@ -0,0 +1,356 @@ +package template + +import ( + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestNowSeconds(t *testing.T) { + now := time.Now().Unix() + for i := 0; i < 100; i++ { + str := nowSeconds() + actual, err := strconv.Atoi(str) + require.NoError(t, err) + // Make sure the value generated is from now (or later if the clock ticked over) + require.GreaterOrEqual(t, int64(actual), now) + } +} + +func TestNowNano(t *testing.T) { + now := time.Now().UnixNano() + for i := 0; i < 100; i++ { + str := nowNano() + actual, err := strconv.Atoi(str) + require.NoError(t, err) + // Make sure the value generated is from now (or later if the clock ticked over) + require.GreaterOrEqual(t, int64(actual), now) + } +} + +func TestTruncate(t *testing.T) { + type testCase struct { + maxLen int + input string + expected string + expectErr bool + } + + tests := map[string]testCase{ + "negative max length": { + maxLen: -1, + input: "foobarbaz", + expected: "", + expectErr: true, + }, + "zero max length": { + maxLen: 0, + input: "foobarbaz", + expected: "", + expectErr: true, + }, + "one max length": { + maxLen: 1, + input: "foobarbaz", + expected: "f", + expectErr: false, + }, + "half max length": { + maxLen: 5, + input: "foobarbaz", + expected: "fooba", + expectErr: false, + }, + "max length one less than length": { + maxLen: 8, + input: "foobarbaz", + expected: "foobarba", + expectErr: false, + }, + "max length equals string length": { + maxLen: 9, + input: "foobarbaz", + expected: "foobarbaz", + expectErr: false, + }, + "max length greater than string length": { + maxLen: 10, + input: "foobarbaz", + expected: "foobarbaz", + expectErr: false, + }, + "max length significantly greater than string length": { + maxLen: 100, + input: "foobarbaz", + expected: "foobarbaz", + expectErr: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual, err := truncate(test.maxLen, test.input) + if test.expectErr && err == nil { + t.Fatalf("err expected, got nil") + } + if !test.expectErr && err != nil { + t.Fatalf("no error expected, got: %s", err) + } + + require.Equal(t, test.expected, actual) + }) + } +} + +func TestTruncateSHA256(t *testing.T) { + type testCase struct { + maxLen int + input string + expected string + expectErr bool + } + + tests := map[string]testCase{ + "negative max length": { + maxLen: -1, + input: "thisisareallylongstring", + expected: "", + expectErr: true, + }, + "zero max length": { + maxLen: 0, + input: "thisisareallylongstring", + expected: "", + expectErr: true, + }, + "8 max length": { + maxLen: 8, + input: "thisisareallylongstring", + expected: "", + expectErr: true, + }, + "nine max length": { + maxLen: 9, + input: "thisisareallylongstring", + expected: "t4bb25641", + expectErr: false, + }, + "half max length": { + maxLen: 12, + input: "thisisareallylongstring", + expected: "this704cd12b", + expectErr: false, + }, + "max length one less than length": { + maxLen: 22, + input: "thisisareallylongstring", + expected: "thisisareallyl7f978be6", + expectErr: false, + }, + "max length equals string length": { + maxLen: 23, + input: "thisisareallylongstring", + expected: "thisisareallylongstring", + expectErr: false, + }, + "max length greater than string length": { + maxLen: 24, + input: "thisisareallylongstring", + expected: "thisisareallylongstring", + expectErr: false, + }, + "max length significantly greater than string length": { + maxLen: 100, + input: "thisisareallylongstring", + expected: "thisisareallylongstring", + expectErr: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual, err := truncateSHA256(test.maxLen, test.input) + if test.expectErr && err == nil { + t.Fatalf("err expected, got nil") + } + if !test.expectErr && err != nil { + t.Fatalf("no error expected, got: %s", err) + } + + require.Equal(t, test.expected, actual) + }) + } +} + +func TestSHA256(t *testing.T) { + type testCase struct { + input string + expected string + } + + tests := map[string]testCase{ + "empty string": { + input: "", + expected: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }, + "foobar": { + input: "foobar", + expected: "c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2", + }, + "mystring": { + input: "mystring", + expected: "bd3ff47540b31e62d4ca6b07794e5a886b0f655fc322730f26ecd65cc7dd5c90", + }, + "very long string": { + input: "Nullam pharetra mattis laoreet. Mauris feugiat, tortor in malesuada convallis, " + + "eros nunc dapibus erat, eget malesuada purus leo id lorem. Morbi pharetra, libero at malesuada bibendum, " + + "dui quam tristique libero, bibendum cursus diam quam at sem. Vivamus vestibulum orci vel odio posuere, " + + "quis tincidunt ipsum lacinia. Donec elementum a orci quis lobortis. Etiam bibendum ullamcorper varius. " + + "Mauris tempor eros est, at porta erat rutrum ac. Aliquam erat volutpat. Sed sagittis leo non bibendum " + + "lacinia. Praesent id justo iaculis, mattis libero vel, feugiat dui. Morbi id diam non magna imperdiet " + + "imperdiet. Ut tortor arcu, mollis ac maximus ac, sagittis commodo augue. Ut semper, diam pulvinar porta " + + "dignissim, massa ex condimentum enim, sed euismod urna quam vitae ex. Sed id neque vitae magna sagittis " + + "pretium. Suspendisse potenti.", + expected: "3e2a996c20b7a02378204f0843507d335e1ba203df2c4ded8d839d44af24482f", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := hashSHA256(test.input) + require.Equal(t, test.expected, actual) + }) + } +} + +func TestUppercase(t *testing.T) { + type testCase struct { + input string + expected string + } + + tests := map[string]testCase{ + "empty string": { + input: "", + expected: "", + }, + "lowercase": { + input: "foobar", + expected: "FOOBAR", + }, + "uppercase": { + input: "FOOBAR", + expected: "FOOBAR", + }, + "mixed case": { + input: "fOoBaR", + expected: "FOOBAR", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := uppercase(test.input) + require.Equal(t, test.expected, actual) + }) + } +} + +func TestLowercase(t *testing.T) { + type testCase struct { + input string + expected string + } + + tests := map[string]testCase{ + "empty string": { + input: "", + expected: "", + }, + "lowercase": { + input: "foobar", + expected: "foobar", + }, + "uppercase": { + input: "FOOBAR", + expected: "foobar", + }, + "mixed case": { + input: "fOoBaR", + expected: "foobar", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := lowercase(test.input) + require.Equal(t, test.expected, actual) + }) + } +} + +func TestReplace(t *testing.T) { + type testCase struct { + input string + find string + replace string + expected string + } + + tests := map[string]testCase{ + "empty string": { + input: "", + find: "", + replace: "", + expected: "", + }, + "search not found": { + input: "foobar", + find: ".", + replace: "_", + expected: "foobar", + }, + "single character found": { + input: "foo.bar", + find: ".", + replace: "_", + expected: "foo_bar", + }, + "multiple characters found": { + input: "foo.bar.baz", + find: ".", + replace: "_", + expected: "foo_bar_baz", + }, + "find and remove": { + input: "foo.bar", + find: ".", + replace: "", + expected: "foobar", + }, + "find full string": { + input: "foobarbaz", + find: "bar", + replace: "_", + expected: "foo_baz", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := replace(test.find, test.replace, test.input) + require.Equal(t, test.expected, actual) + }) + } +} + +func TestUUID(t *testing.T) { + re := "^[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12}$" + for i := 0; i < 100; i++ { + id, err := uuid() + require.NoError(t, err) + require.Regexp(t, re, id) + } +} diff --git a/sdk/helper/template/template.go b/sdk/helper/template/template.go new file mode 100644 index 000000000000..181332ec313a --- /dev/null +++ b/sdk/helper/template/template.go @@ -0,0 +1,140 @@ +package template + +import ( + "fmt" + "strings" + "text/template" + + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/vault/sdk/helper/base62" +) + +type Opt func(*StringTemplate) error + +func Template(rawTemplate string) Opt { + return func(up *StringTemplate) error { + up.rawTemplate = rawTemplate + return nil + } +} + +// Function allows the user to specify functions for use in the template. If the name provided is a function that +// already exists in the function map, this will override the previously specified function. +func Function(name string, f interface{}) Opt { + return func(up *StringTemplate) error { + if name == "" { + return fmt.Errorf("missing function name") + } + if f == nil { + return fmt.Errorf("missing function") + } + up.funcMap[name] = f + return nil + } +} + +// StringTemplate creates strings based on the provided template. +// This uses the go templating language, so anything that adheres to that language will function in this struct. +// There are several custom functions available for use in the template: +// - random (rand) +// - Randomly generated characters. This uses the charset specified in RandomCharset. Must include a length. +// Example: {{ rand 20 }} +// - truncate (trunc) +// - Truncates the previous value to the specified length. Must include a maximum length. +// Example: {{ .DisplayName | truncate 10 }} +// - truncate_sha256 (trunc_sha256) +// - Truncates the previous value to the specified length. If the original length is greater than the length +// specified, the remaining characters will be sha256 hashed and appended to the end. The hash will be only the first 8 characters The maximum length will +// be no longer than the length specified. +// Example: {{ .DisplayName | truncate_sha256 30 }} +// - uppercase (upper) +// - Uppercases the previous value. +// Example: {{ .RoleName | uppercase }} +// - lowercase (lower) +// - Lowercases the previous value. +// Example: {{ .DisplayName | lowercase }} +// - replace +// - Performs a string find & replace +// Example: {{ .DisplayName | replace - _ }} +// - sha256 +// - SHA256 hashes the previous value. +// Example: {{ .DisplayName | sha256 }} +// - timestamp (now_seconds) +// - Provides the current unix time in seconds. +// Example: {{ now_seconds}} +// - now_nano +// - Provides the current unix time in nanoseconds. +// Example: {{ now_nano}} +// - uuid +// - Generates a UUID +// Example: {{ uuid }} +type StringTemplate struct { + rawTemplate string + tmpl *template.Template + funcMap template.FuncMap +} + +// NewTemplate creates a StringTemplate. No arguments are required +// as this has reasonable defaults for all values. +// The default template is specified in the DefaultTemplate constant. +func NewTemplate(opts ...Opt) (up StringTemplate, err error) { + up = StringTemplate{ + funcMap: map[string]interface{}{ + "random": base62.Random, + "rand": base62.Random, + "truncate": truncate, + "trunc": truncate, + "truncate_sha256": truncateSHA256, + "trunc_sha256": truncateSHA256, + "uppercase": uppercase, + "upper": uppercase, + "lowercase": lowercase, + "lower": lowercase, + "replace": replace, + "sha256": hashSHA256, + + "timestamp": nowSeconds, + "now_seconds": nowSeconds, + "now_nano": nowNano, + "uuid": uuid, + }, + } + + merr := &multierror.Error{} + for _, opt := range opts { + merr = multierror.Append(merr, opt(&up)) + } + + err = merr.ErrorOrNil() + if err != nil { + return up, err + } + + if up.rawTemplate == "" { + return StringTemplate{}, fmt.Errorf("missing template") + } + + tmpl, err := template.New("template"). + Funcs(up.funcMap). + Parse(up.rawTemplate) + if err != nil { + return StringTemplate{}, fmt.Errorf("unable to parse template: %w", err) + } + up.tmpl = tmpl + + return up, nil +} + +// Generate based on the provided template +func (up StringTemplate) Generate(data interface{}) (string, error) { + if up.tmpl == nil || up.rawTemplate == "" { + return "", fmt.Errorf("failed to generate: template not initialized") + } + str := &strings.Builder{} + err := up.tmpl.Execute(str, data) + if err != nil { + return "", fmt.Errorf("unable to apply template: %w", err) + } + + return str.String(), nil +} diff --git a/sdk/helper/template/template_test.go b/sdk/helper/template/template_test.go new file mode 100644 index 000000000000..460c6befd4cb --- /dev/null +++ b/sdk/helper/template/template_test.go @@ -0,0 +1,226 @@ +package template + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGenerate(t *testing.T) { + type testCase struct { + template string + additionalOpts []Opt + data interface{} + + expected string + expectErr bool + } + + tests := map[string]testCase{ + "template without arguments": { + template: "this is a template", + data: nil, + expected: "this is a template", + expectErr: false, + }, + "template with arguments but no data": { + template: "this is a {{.String}}", + data: nil, + expected: "this is a ", + expectErr: false, + }, + "template with arguments": { + template: "this is a {{.String}}", + data: struct { + String string + }{ + String: "foobar", + }, + expected: "this is a foobar", + expectErr: false, + }, + "template with builtin functions": { + template: `{{.String | truncate 10}} +{{.String | trunc 8}} +{{.String | uppercase}} +{{.String | upper}} +{{.String | lowercase}} +{{.String | lower}} +{{.String | replace " " "."}} +{{.String | sha256}} +{{.String | truncate_sha256 20}} +{{.String | trunc_sha256 25}}`, + data: struct { + String string + }{ + String: "Some string with Multiple Capitals LETTERS", + }, + expected: `Some strin +Some str +SOME STRING WITH MULTIPLE CAPITALS LETTERS +SOME STRING WITH MULTIPLE CAPITALS LETTERS +some string with multiple capitals letters +some string with multiple capitals letters +Some.string.with.Multiple.Capitals.LETTERS +da9872dd96609c72897defa11fe81017a62c3f44339d9d3b43fe37540ede3601 +Some string 6841cf80 +Some string with 058f6eeb`, + expectErr: false, + }, + "custom function": { + template: "{{foo}}", + additionalOpts: []Opt{ + Function("foo", func() string { + return "custom-foo" + }), + }, + expected: "custom-foo", + expectErr: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + opts := append(test.additionalOpts, Template(test.template)) + st, err := NewTemplate(opts...) + require.NoError(t, err) + + actual, err := st.Generate(test.data) + if test.expectErr && err == nil { + t.Fatalf("err expected, got nil") + } + if !test.expectErr && err != nil { + t.Fatalf("no error expected, got: %s", err) + } + + require.Equal(t, test.expected, actual) + }) + } + + t.Run("random", func(t *testing.T) { + for i := 1; i < 100; i++ { + st, err := NewTemplate( + Template(fmt.Sprintf("{{random %d}}", i)), + ) + require.NoError(t, err) + + actual, err := st.Generate(nil) + require.NoError(t, err) + + require.Regexp(t, fmt.Sprintf("^[a-zA-Z0-9]{%d}$", i), actual) + } + }) + + t.Run("rand", func(t *testing.T) { + for i := 1; i < 100; i++ { + st, err := NewTemplate( + Template(fmt.Sprintf("{{random %d}}", i)), + ) + require.NoError(t, err) + + actual, err := st.Generate(nil) + require.NoError(t, err) + + require.Regexp(t, fmt.Sprintf("^[a-zA-Z0-9]{%d}$", i), actual) + } + }) + + t.Run("timestamp", func(t *testing.T) { + for i := 0; i < 100; i++ { + st, err := NewTemplate( + Template("{{timestamp}}"), + ) + require.NoError(t, err) + + actual, err := st.Generate(nil) + require.NoError(t, err) + + require.Regexp(t, "^[0-9]+$", actual) + } + }) + + t.Run("now_seconds", func(t *testing.T) { + for i := 0; i < 100; i++ { + st, err := NewTemplate( + Template("{{now_seconds}}"), + ) + require.NoError(t, err) + + actual, err := st.Generate(nil) + require.NoError(t, err) + + require.Regexp(t, "^[0-9]+$", actual) + } + }) + + t.Run("now_nano", func(t *testing.T) { + for i := 0; i < 100; i++ { + st, err := NewTemplate( + Template("{{now_nano}}"), + ) + require.NoError(t, err) + + actual, err := st.Generate(nil) + require.NoError(t, err) + + require.Regexp(t, "^[0-9]+$", actual) + } + }) +} + +func TestBadConstructorArguments(t *testing.T) { + type testCase struct { + opts []Opt + } + + tests := map[string]testCase{ + "missing template": { + opts: nil, + }, + "missing custom function name": { + opts: []Opt{ + Template("foo bar"), + Function("", func() string { + return "foo" + }), + }, + }, + "missing custom function": { + opts: []Opt{ + Template("foo bar"), + Function("foo", nil), + }, + }, + "bad template": { + opts: []Opt{ + Template("{{.String"), + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + st, err := NewTemplate(test.opts...) + require.Error(t, err) + + str, err := st.Generate(nil) + require.Error(t, err) + require.Equal(t, "", str) + }) + } + + t.Run("erroring custom function", func(t *testing.T) { + st, err := NewTemplate( + Template("{{foo}}"), + Function("foo", func() (string, error) { + return "", fmt.Errorf("an error!") + }), + ) + require.NoError(t, err) + + str, err := st.Generate(nil) + require.Error(t, err) + require.Equal(t, "", str) + }) +}