// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package template_test import ( "context" "encoding/base64" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "time" "github.com/ory/kratos/courier/template" "github.com/ory/kratos/driver/config" "github.com/ory/kratos/pkg" "github.com/ory/x/fetcher" lru "github.com/hashicorp/golang-lru/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ory/kratos/x" ) func TestLoadTextTemplate(t *testing.T) { executeTextTemplate := func(t *testing.T, dir, name, pattern string, model map[string]interface{}) string { _, reg := pkg.NewFastRegistryWithMocks(t) tp, err := template.LoadText(t.Context(), reg, os.DirFS(dir), name, pattern, model, "") require.NoError(t, err) return tp } executeHTMLTemplate := func(t *testing.T, dir, name, pattern string, model map[string]interface{}) string { _, reg := pkg.NewFastRegistryWithMocks(t) tp, err := template.LoadHTML(t.Context(), reg, os.DirFS(dir), name, pattern, model, "") require.NoError(t, err) return tp } t.Run("method=from bundled", func(t *testing.T) { actual := executeTextTemplate(t, "courier/builtin/templates/test_stub", "email.body.gotmpl", "", nil) assert.Contains(t, actual, "stub email") }) t.Run("method=fallback to bundled", func(t *testing.T) { template.Cache, _ = lru.New[string, template.Template](16) // prevent Cache hit actual := executeTextTemplate(t, "some/inexistent/dir", "test_stub/email.body.gotmpl", "", nil) assert.Contains(t, actual, "stub email") }) t.Run("method=with Sprig functions", func(t *testing.T) { template.Cache, _ = lru.New[string, template.Template](16) // prevent Cache hit m := map[string]interface{}{"input": "hello world"} // create a simple model actual := executeTextTemplate(t, "courier/builtin/templates/test_stub", "email.body.sprig.gotmpl", "", m) assert.Contains(t, actual, "HelloWorld,HELLOWORLD") }) t.Run("method=sprig should not support non-hermetic", func(t *testing.T) { template.Cache, _ = lru.New[string, template.Template](16) _, reg := pkg.NewFastRegistryWithMocks(t) nonhermetic := []string{"date", "date_in_zone", "date_modify", "now", "htmlDate", "htmlDateInZone", "dateInZone", "dateModify", "env", "expandenv", "getHostByName", "uuidv4", "randNumeric", "randAscii", "randAlpha", "randAlphaNum"} for _, tc := range nonhermetic { t.Run("case=should not support function: "+tc, func(t *testing.T) { _, err := template.LoadText(t.Context(), reg, x.NewStubFS(tc, []byte(fmt.Sprintf("{{ %s }}", tc))), tc, "", map[string]interface{}{}, "") assert.ErrorContains(t, err, fmt.Sprintf("function %q not defined", tc)) }) } }) t.Run("method=html with nested templates", func(t *testing.T) { template.Cache, _ = lru.New[string, template.Template](16) // prevent Cache hit m := map[string]interface{}{"lang": "en_US"} // create a simple model actual := executeHTMLTemplate(t, "courier/builtin/templates/test_stub", "email.body.html.gotmpl", "email.body.html*", m) assert.Contains(t, actual, "lang=en_US") }) t.Run("method=Cache works", func(t *testing.T) { dir := os.TempDir() name := x.NewUUID().String() + ".body.gotmpl" fp := filepath.Join(dir, name) require.NoError(t, os.WriteFile(fp, []byte("cached stub body"), 0o600)) assert.Contains(t, executeTextTemplate(t, dir, name, "", nil), "cached stub body") require.NoError(t, os.RemoveAll(fp)) assert.Contains(t, executeTextTemplate(t, dir, name, "", nil), "cached stub body") }) t.Run("method=remote resource", func(t *testing.T) { _, reg := pkg.NewFastRegistryWithMocks(t) ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) t.Run("case=base64 encoded data", func(t *testing.T) { t.Run("html template", func(t *testing.T) { m := map[string]interface{}{"lang": "en_US"} f, err := os.ReadFile("courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl") require.NoError(t, err) b64 := base64.StdEncoding.EncodeToString(f) tp, err := template.LoadHTML(ctx, reg, nil, "", "", m, "base64://"+b64) require.NoError(t, err) assert.Contains(t, tp, "lang=en_US") }) t.Run("case=plaintext", func(t *testing.T) { m := map[string]interface{}{"Body": "something"} f, err := os.ReadFile("courier/builtin/templates/test_stub/email.body.plaintext.gotmpl") require.NoError(t, err) b64 := base64.StdEncoding.EncodeToString(f) tp, err := template.LoadText(ctx, reg, nil, "", "", m, "base64://"+b64) require.NoError(t, err) assert.Contains(t, tp, "stub email body something") }) }) t.Run("case=file resource", func(t *testing.T) { t.Run("case=html template", func(t *testing.T) { m := map[string]interface{}{"lang": "en_US"} tp, err := template.LoadHTML(ctx, reg, nil, "", "", m, "file://courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl") require.NoError(t, err) assert.Contains(t, tp, "lang=en_US") }) t.Run("case=plaintext", func(t *testing.T) { m := map[string]interface{}{"Body": "something"} tp, err := template.LoadText(ctx, reg, nil, "", "", m, "file://courier/builtin/templates/test_stub/email.body.plaintext.gotmpl") require.NoError(t, err) assert.Contains(t, tp, "stub email body something") }) }) t.Run("case=http resource", func(t *testing.T) { router := http.NewServeMux() router.HandleFunc("GET /html", func(writer http.ResponseWriter, request *http.Request) { http.ServeFile(writer, request, "courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl") }) router.HandleFunc("GET /plaintext", func(writer http.ResponseWriter, request *http.Request) { http.ServeFile(writer, request, "courier/builtin/templates/test_stub/email.body.plaintext.gotmpl") }) ts := httptest.NewServer(router) t.Cleanup(ts.Close) t.Run("case=html template", func(t *testing.T) { m := map[string]interface{}{"lang": "en_US"} tp, err := template.LoadHTML(ctx, reg, nil, "", "", m, ts.URL+"/html") require.NoError(t, err) assert.Contains(t, tp, "lang=en_US") }) t.Run("case=plaintext", func(t *testing.T) { m := map[string]interface{}{"Body": "something"} tp, err := template.LoadText(ctx, reg, nil, "", "", m, ts.URL+"/plaintext") require.NoError(t, err) assert.Contains(t, tp, "stub email body something") }) }) t.Run("case=unsupported resource", func(t *testing.T) { tp, err := template.LoadHTML(ctx, reg, nil, "", "", map[string]interface{}{}, "grpc://unsupported-url") require.ErrorIs(t, err, fetcher.ErrUnknownScheme) require.Empty(t, tp) tp, err = template.LoadText(ctx, reg, nil, "", "", map[string]interface{}{}, "grpc://unsupported-url") require.ErrorIs(t, err, fetcher.ErrUnknownScheme) require.Empty(t, tp) }) t.Run("case=disallowed resources", func(t *testing.T) { require.NoError(t, reg.Config().Set(ctx, config.ViperKeyClientHTTPNoPrivateIPRanges, true)) reg.HTTPClient(ctx).RetryMax = 1 reg.HTTPClient(ctx).RetryWaitMax = time.Millisecond _, err := template.LoadHTML(ctx, reg, nil, "", "", map[string]interface{}{}, "http://localhost:8080/1234") assert.ErrorContains(t, err, "no such host") _, err = template.LoadText(ctx, reg, nil, "", "", map[string]interface{}{}, "http://localhost:8080/1234") assert.ErrorContains(t, err, "no such host") }) t.Run("method=cache works", func(t *testing.T) { tp1, err := template.LoadText(ctx, reg, nil, "", "", map[string]interface{}{}, "base64://e3sgJGwgOj0gY2F0ICJsYW5nPSIgLmxhbmcgfX0Ke3sgbm9zcGFjZSAkbCB9fQ==") require.NoError(t, err) tp2, err := template.LoadText(ctx, reg, nil, "", "", map[string]interface{}{}, "base64://c3R1YiBlbWFpbCBib2R5IHt7IC5Cb2R5IH19") require.NoError(t, err) assert.NotEqualf(t, tp1, tp2, "Expected remote template 1 and remote template 2 to not be equal") }) }) }