8.8 KB
views_test.go
package application
import (
"bytes"
"embed"
"errors"
"io/fs"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
var (
//go:embed resources/testdata/views
testViewsRaw embed.FS
testViews, _ = fs.Sub(testViewsRaw, "resources/testdata")
)
func TestParseBaseTemplates(t *testing.T) {
app := &App{
funcs: make(map[string]any),
}
base := app.parseBaseTemplates(testViews)
// Should have loaded layout and partial templates
if base.Lookup("base.html") == nil {
t.Error("expected base.html layout to be loaded")
}
if base.Lookup("card.html") == nil {
t.Error("expected card.html partial to be loaded")
}
// Should NOT have loaded page templates
if base.Lookup("home.html") != nil {
t.Error("expected home.html to NOT be loaded at startup")
}
if base.Lookup("about.html") != nil {
t.Error("expected about.html to NOT be loaded at startup")
}
}
func TestLoadView(t *testing.T) {
app := &App{
funcs: make(map[string]any),
viewsFS: testViews,
}
app.base = app.parseBaseTemplates(testViews)
// Should load view on-demand
tmpl, err := app.loadView("pages/home.html")
if err != nil {
t.Fatalf("unexpected error loading view: %v", err)
}
// Should have access to layout
if tmpl.Lookup("base.html") == nil {
t.Error("expected layout to be accessible in loaded view")
}
// Should have access to partial
if tmpl.Lookup("card.html") == nil {
t.Error("expected partial to be accessible in loaded view")
}
// Should have the view itself
if tmpl.Lookup("pages/home.html") == nil {
t.Error("expected view to be accessible")
}
}
func TestLoadViewNotFound(t *testing.T) {
app := &App{
funcs: make(map[string]any),
viewsFS: testViews,
}
app.base = app.parseBaseTemplates(testViews)
_, err := app.loadView("nonexistent.html")
if err == nil {
t.Error("expected error for nonexistent view")
}
if !strings.Contains(err.Error(), "view not found") {
t.Errorf("expected 'view not found' error, got: %v", err)
}
}
func TestContentBlockIsolation(t *testing.T) {
app := &App{
funcs: make(map[string]any),
viewsFS: testViews,
}
app.base = app.parseBaseTemplates(testViews)
// Load first page with {{define "content"}}
homeTmpl, err := app.loadView("pages/home.html")
if err != nil {
t.Fatalf("failed to load home.html: %v", err)
}
// Load second page with its own {{define "content"}}
aboutTmpl, err := app.loadView("pages/about.html")
if err != nil {
t.Fatalf("failed to load about.html: %v", err)
}
// Execute both and verify content is different (no conflict)
var homeBuf, aboutBuf bytes.Buffer
if err := homeTmpl.ExecuteTemplate(&homeBuf, "pages/home.html", nil); err != nil {
t.Fatalf("failed to execute home template: %v", err)
}
if err := aboutTmpl.ExecuteTemplate(&aboutBuf, "pages/about.html", nil); err != nil {
t.Fatalf("failed to execute about template: %v", err)
}
homeOutput := homeBuf.String()
aboutOutput := aboutBuf.String()
// Verify each has its own content
if !strings.Contains(homeOutput, "Welcome Home") {
t.Errorf("home page should contain 'Welcome Home', got: %s", homeOutput)
}
if !strings.Contains(aboutOutput, "About Us") {
t.Errorf("about page should contain 'About Us', got: %s", aboutOutput)
}
// Verify they don't have each other's content
if strings.Contains(homeOutput, "About Us") {
t.Error("home page should not contain about page content")
}
if strings.Contains(aboutOutput, "Welcome Home") {
t.Error("about page should not contain home page content")
}
}
func TestViewServeHTTP(t *testing.T) {
app := &App{
funcs: make(map[string]any),
viewsFS: testViews,
controllers: make(map[string]Controller),
}
app.base = app.parseBaseTemplates(testViews)
view := app.Serve("pages/home.html", nil)
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
view.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, "Welcome Home") {
t.Errorf("expected body to contain 'Welcome Home', got: %s", body)
}
contentType := rec.Header().Get("Content-Type")
if contentType != "text/html; charset=utf-8" {
t.Errorf("expected Content-Type 'text/html; charset=utf-8', got: %s", contentType)
}
}
func TestViewWithBouncer(t *testing.T) {
app := &App{
funcs: make(map[string]any),
viewsFS: testViews,
controllers: make(map[string]Controller),
}
app.base = app.parseBaseTemplates(testViews)
blocked := false
bouncer := func(app *App, w http.ResponseWriter, r *http.Request) bool {
blocked = true
http.Error(w, "Forbidden", http.StatusForbidden)
return false
}
view := app.Serve("pages/home.html", bouncer)
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
view.ServeHTTP(rec, req)
if !blocked {
t.Error("expected bouncer to be called")
}
if rec.Code != http.StatusForbidden {
t.Errorf("expected status 403, got %d", rec.Code)
}
}
func TestPartialsAccessibleInViews(t *testing.T) {
app := &App{
funcs: make(map[string]any),
viewsFS: testViews,
}
app.base = app.parseBaseTemplates(testViews)
// Load a view that uses a partial
tmpl, err := app.loadView("pages/with-partial.html")
if err != nil {
t.Fatalf("failed to load view: %v", err)
}
var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, "pages/with-partial.html", nil); err != nil {
t.Fatalf("failed to execute template: %v", err)
}
output := buf.String()
if !strings.Contains(output, "Card Content") {
t.Errorf("expected partial content 'Card Content' in output, got: %s", output)
}
}
func TestLayoutsAccessibleInViews(t *testing.T) {
app := &App{
funcs: make(map[string]any),
viewsFS: testViews,
}
app.base = app.parseBaseTemplates(testViews)
// Load a view that extends a layout
tmpl, err := app.loadView("pages/with-layout.html")
if err != nil {
t.Fatalf("failed to load view: %v", err)
}
var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, "base.html", nil); err != nil {
t.Fatalf("failed to execute template: %v", err)
}
output := buf.String()
if !strings.Contains(output, "<!DOCTYPE html>") {
t.Errorf("expected layout DOCTYPE in output, got: %s", output)
}
if !strings.Contains(output, "Layout Page Content") {
t.Errorf("expected page content in layout output, got: %s", output)
}
}
func TestBaseControllerRender(t *testing.T) {
app := &App{
funcs: make(map[string]any),
viewsFS: testViews,
controllers: make(map[string]Controller),
}
app.base = app.parseBaseTemplates(testViews)
c := &BaseController{App: app}
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c.Render(rec, req, "pages/home.html", map[string]any{"title": "Test"})
if rec.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, "Welcome Home") {
t.Errorf("expected body to contain 'Welcome Home', got: %s", body)
}
contentType := rec.Header().Get("Content-Type")
if contentType != "text/html; charset=utf-8" {
t.Errorf("expected Content-Type 'text/html; charset=utf-8', got: %s", contentType)
}
}
func TestBaseControllerRenderNotFound(t *testing.T) {
app := &App{
funcs: make(map[string]any),
viewsFS: testViews,
controllers: make(map[string]Controller),
}
app.base = app.parseBaseTemplates(testViews)
c := &BaseController{App: app}
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c.Render(rec, req, "nonexistent.html", nil)
if rec.Code != http.StatusInternalServerError {
t.Errorf("expected status 500, got %d", rec.Code)
}
}
func TestRenderError(t *testing.T) {
c := &BaseController{}
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c.RenderError(rec, req, errors.New("something went wrong"))
// Should return 200 OK for HTMX compatibility
if rec.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, "something went wrong") {
t.Errorf("expected error message in body, got: %s", body)
}
if !strings.Contains(body, "alert-error") {
t.Errorf("expected alert-error class in body, got: %s", body)
}
contentType := rec.Header().Get("Content-Type")
if contentType != "text/html; charset=utf-8" {
t.Errorf("expected Content-Type 'text/html; charset=utf-8', got: %s", contentType)
}
}
func TestRenderErrorEscapesHTML(t *testing.T) {
c := &BaseController{}
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
c.RenderError(rec, req, errors.New("<script>alert('xss')</script>"))
body := rec.Body.String()
if strings.Contains(body, "<script>") {
t.Error("expected HTML to be escaped")
}
if !strings.Contains(body, "<script>") {
t.Errorf("expected escaped HTML in body, got: %s", body)
}
}