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())
})
}