package template
import (
"strings"
"testing"
"time"
)
func TestApplyTemplate(t *testing.T) {
tests := []struct {
name string
template string
vars map[string]string
input string
want string
wantErr bool
errContains string
}{
{
name: "simple variable",
template: "Hello {{name}}!",
vars: map[string]string{"name": "World"},
want: "Hello World!",
},
{
name: "multiple variables",
template: "{{greeting}} {{name}}!",
vars: map[string]string{
"greeting": "Hello",
"name": "World",
},
want: "Hello World!",
},
{
name: "special input variable",
template: "Content: {{input}}",
input: "test content",
want: "Content: test content",
},
{
name: "nested variables",
template: "{{outer{{inner}}}}",
vars: map[string]string{
"inner": "foo",
"outerfoo": "result",
},
want: "result",
},
{
name: "simple text plugin",
template: "{{plugin:text:upper:hello}}",
want: "HELLO",
},
{
name: "text plugin with variable",
template: "{{plugin:text:upper:{{name}}}}",
vars: map[string]string{"name": "world"},
want: "WORLD",
},
{
name: "plugin with dynamic operation",
template: "{{plugin:text:{{operation}}:hello}}",
vars: map[string]string{"operation": "upper"},
want: "HELLO",
},
{
name: "multiple plugins",
template: "A:{{plugin:text:upper:hello}} B:{{plugin:text:lower:WORLD}}",
want: "A:HELLO B:world",
},
{
name: "nested plugins",
template: "{{plugin:text:upper:{{plugin:text:lower:HELLO}}}}",
want: "HELLO",
},
{
name: "missing variable",
template: "Hello {{name}}!",
wantErr: true,
errContains: "missing required variable",
},
{
name: "unknown plugin",
template: "{{plugin:invalid:op:value}}",
wantErr: true,
errContains: "unknown plugin namespace",
},
{
name: "unknown plugin operation",
template: "{{plugin:text:invalid:value}}",
wantErr: true,
errContains: "unknown text operation",
},
{
name: "nested plugin error",
template: "{{plugin:text:upper:{{plugin:invalid:op:value}}}}",
wantErr: true,
errContains: "unknown plugin namespace",
},
{
name: "empty template",
template: "",
want: "",
},
{
name: "no substitutions needed",
template: "plain text",
want: "plain text",
},
{
name: "open braces with no close braces",
template: "x{{{{a}}",
vars: map[string]string{"a": "V"},
want: "x{{V",
},
{
name: "close braces with no open braces",
template: "{{a}}}}",
vars: map[string]string{"a": "V"},
want: "V}}",
},
{
name: "triple braces",
template: "{{{a}}}",
vars: map[string]string{"a": "V"},
want: "{V}",
},
{
name: "triple open braces",
template: "{{{a}}",
vars: map[string]string{"a": "V"},
want: "{V",
},
{
name: "single braces in token",
template: "{{a}{b}}",
want: "{{a}{b}}",
},
{
name: "value is not a variable name",
template: "{{{{inner}}}}",
vars: map[string]string{"inner": "a", "a": "V"},
want: "{{a}}",
},
{
name: "nested variable name with template text",
template: "{{x_{{inner}}}}",
vars: map[string]string{"inner": "a", "x_a": "V"},
want: "V",
},
{
name: "empty token",
template: "{{}}",
want: "{{}}",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ApplyTemplate(tt.template, tt.vars, tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ApplyTemplate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" {
if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("error %q should contain %q", err.Error(), tt.errContains)
}
return
}
if got != tt.want {
t.Errorf("ApplyTemplate() = %q, want %q", got, tt.want)
}
})
}
}
func TestApplyTemplateLongBraceRun(t *testing.T) {
content := strings.Repeat("{", 1<<20)
if out, err := applyWithin(t, content, nil); err != nil || out != content {
t.Errorf("ApplyTemplate() changed the text, error = %v", err)
}
}
func TestCyclicVariablesStayLiteral(t *testing.T) {
tests := []struct {
name string
vars map[string]string
want string
}{
{"self reference", map[string]string{"a": "{{a}}"}, "{{a}}"},
{"mutual cycle", map[string]string{"a": "{{b}}", "b": "{{a}}"}, "{{b}}"},
{"expanding self reference", map[string]string{"a": "{{a}}{{a}}"}, "{{a}}{{a}}"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := applyWithin(t, "{{a}}", tt.vars)
if err != nil || got != tt.want {
t.Errorf("ApplyTemplate() = %q, %v, want %q", got, err, tt.want)
}
})
}
}
func TestApplyTemplateInput(t *testing.T) {
t.Setenv("FABRIC_TEST_VALUE", "ENV_VALUE_FROM_PLUGIN")
vars := map[string]string{"name": "world", "empty": ""}
tests := []struct{ name, input, want string }{
{"variable", "Hello {{name}}", "Hello world"},
{"plugin token", "{{plugin:sys:env:FABRIC_TEST_VALUE}}", "{{plugin:sys:env:FABRIC_TEST_VALUE}}"},
{"extension token", "{{ext:anything:run:x}}", "{{ext:anything:run:x}}"},
{"nested plugin tokens", "{{plugin:text:upper:{{plugin:sys:env:FABRIC_TEST_VALUE}}}}", "{{plugin:text:upper:{{plugin:sys:env:FABRIC_TEST_VALUE}}}}"},
{"variable in plugin token", "{{plugin:text:upper:{{name}}}}", "{{plugin:text:upper:world}}"},
{"token made with input", "{{{{input}}plugin:sys:env:FABRIC_TEST_VALUE}}", "{{plugin:sys:env:FABRIC_TEST_VALUE}}"},
{"token made with empty variable", "{{{{empty}}plugin:sys:env:FABRIC_TEST_VALUE}}", "{{plugin:sys:env:FABRIC_TEST_VALUE}}"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ApplyTemplateInput(tt.input, vars)
if err != nil || got != tt.want {
t.Errorf("ApplyTemplateInput() = %q, %v, want %q", got, err, tt.want)
}
})
}
if _, err := ApplyTemplateInput("{{unknown}}", vars); err == nil || !strings.Contains(err.Error(), "missing required variable") {
t.Errorf("ApplyTemplateInput() unknown variable: error = %v, want missing required variable", err)
}
}
func applyWithin(t *testing.T, content string, vars map[string]string) (string, error) {
t.Helper()
type result struct {
out string
err error
}
done := make(chan result, 1)
go func() {
out, err := ApplyTemplate(content, vars, "")
done <- result{out, err}
}()
select {
case r := <-done:
return r.out, r.err
case <-time.After(5 * time.Second):
t.Fatal("ApplyTemplate() did not stop in 5 seconds")
return "", nil
}
}