package view import ( "bytes" "html/template" "net/http" "net/http/httptest" "strings" "testing" "github.com/eugenioenko/autentico/pkg/config" "github.com/stretchr/testify/assert" ) func TestParseTemplate(t *testing.T) { // Test parsing a few templates names := []string{"login", "signup", "error", "onboard"} for _, name := range names { tmpl, err := ParseTemplate(name) assert.NoError(t, err, "failed to parse template: %s", name) assert.NotNil(t, tmpl) } // Test invalid template _, err := ParseTemplate("nonexistent") assert.Error(t, err) } func TestTemplateHelpersExecution(t *testing.T) { config.Bootstrap.AppOAuthPath = "/custom-oauth" funcs := template.FuncMap{ "authURL": func(path string) string { return config.GetBootstrap().AppOAuthPath + path }, } t.Run("authURL", func(t *testing.T) { tHelper, err := template.New("test").Funcs(funcs).Parse(`{{authURL "/foo"}}`) assert.NoError(t, err) var buf bytes.Buffer err = tHelper.Execute(&buf, nil) assert.NoError(t, err) assert.Equal(t, "/custom-oauth/foo", buf.String()) }) } // TestHasThemeCssFunc covers the template func that gates the theme.css link. func TestHasThemeCssFunc(t *testing.T) { prev := config.Values.ThemeCssResolved t.Cleanup(func() { config.Values.ThemeCssResolved = prev }) tmpl, err := template.New("t").Funcs(template.FuncMap{ "hasThemeCss": func() bool { return config.Get().ThemeCssResolved != "" }, }).Parse(`{{if hasThemeCss}}yes{{else}}no{{end}}`) assert.NoError(t, err) var buf bytes.Buffer config.Values.ThemeCssResolved = "body{}" _ = tmpl.Execute(&buf, nil) assert.Equal(t, "yes", buf.String()) config.Values.ThemeCssResolved = "" buf.Reset() _ = tmpl.Execute(&buf, nil) assert.Equal(t, "no", buf.String()) } // TestLayoutThemeCSSRegression is the regression guard for issue #218. An admin // who sets theme_css_inline must never be able to inject body{color:red}` tmpl, err := ParseTemplate("error") assert.NoError(t, err) var buf bytes.Buffer err = tmpl.ExecuteTemplate(&buf, "layout", map[string]any{ "ThemeTitle": "T", "ThemeLogoUrl": "", "Error": "boom", "CspNonce": "test-nonce", }) assert.NoError(t, err) out := buf.String() // None of the admin-supplied CSS content must appear in the rendered HTML — // it's served separately at /oauth2/static/theme.css. assert.NotContains(t, out, "alert(1)", "admin CSS content must not be inlined into the page") assert.NotContains(t, out, "", URL: "https://example.com"}, } tmpl, err := ParseTemplate("error") assert.NoError(t, err) var buf bytes.Buffer err = tmpl.ExecuteTemplate(&buf, "layout", map[string]any{ "ThemeTitle": "T", "ThemeLogoUrl": "", "Error": "test", "CspNonce": "test-nonce", }) assert.NoError(t, err) out := buf.String() assert.NotContains(t, out, "") assert.Contains(t, out, "<script>") }) } func TestThemeCSSHandler(t *testing.T) { prev := config.Values.ThemeCssResolved t.Cleanup(func() { config.Values.ThemeCssResolved = prev }) t.Run("serves resolved css with etag", func(t *testing.T) { config.Values.ThemeCssResolved = "body{color:red}" req := httptest.NewRequest(http.MethodGet, "/oauth2/static/theme.css", nil) rr := httptest.NewRecorder() ThemeCSSHandler().ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, "text/css; charset=utf-8", rr.Header().Get("Content-Type")) assert.NotEmpty(t, rr.Header().Get("ETag")) assert.Equal(t, "body{color:red}", rr.Body.String()) }) t.Run("empty css still returns 200 with text/css", func(t *testing.T) { config.Values.ThemeCssResolved = "" req := httptest.NewRequest(http.MethodGet, "/oauth2/static/theme.css", nil) rr := httptest.NewRecorder() ThemeCSSHandler().ServeHTTP(rr, req) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, "text/css; charset=utf-8", rr.Header().Get("Content-Type")) assert.Empty(t, rr.Body.String()) }) t.Run("304 on matching If-None-Match", func(t *testing.T) { config.Values.ThemeCssResolved = "body{}" req := httptest.NewRequest(http.MethodGet, "/oauth2/static/theme.css", nil) rr := httptest.NewRecorder() ThemeCSSHandler().ServeHTTP(rr, req) etag := rr.Header().Get("ETag") req2 := httptest.NewRequest(http.MethodGet, "/oauth2/static/theme.css", nil) req2.Header.Set("If-None-Match", etag) rr2 := httptest.NewRecorder() ThemeCSSHandler().ServeHTTP(rr2, req2) assert.Equal(t, http.StatusNotModified, rr2.Code) assert.Empty(t, rr2.Body.String()) }) }