readysite / pkg / application / views_test.go
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, "&lt;script&gt;") {
		t.Errorf("expected escaped HTML in body, got: %s", body)
	}
}
← Back