The template's files at a77fd30, without its history or LICENSE, after script/rename simplexcalc. Model: opus-5-5
198 lines
4.9 KiB
Go
198 lines
4.9 KiB
Go
package render_test
|
|
|
|
import (
|
|
"bytes"
|
|
"html/template"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/simplexcalc/internal/database"
|
|
"sneak.berlin/go/simplexcalc/internal/globals"
|
|
"sneak.berlin/go/simplexcalc/internal/render"
|
|
"sneak.berlin/go/simplexcalc/templates"
|
|
)
|
|
|
|
func newRenderer(t *testing.T) *render.Renderer {
|
|
t.Helper()
|
|
|
|
r, err := render.New(nil, render.Params{
|
|
Globals: &globals.Globals{
|
|
Appname: "simplexcalc", Version: "test", Buildarch: "amd64",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("compiling templates: %v", err)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// TestEveryEmbeddedPageCompiles is the reason New parses everything at
|
|
// startup: a template that does not compile must be a process that does
|
|
// not start, and this is what proves the set is complete rather than
|
|
// just non-empty.
|
|
func TestEveryEmbeddedPageCompiles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newRenderer(t)
|
|
|
|
embedded, err := templates.FS.ReadDir(".")
|
|
if err != nil {
|
|
t.Fatalf("reading embedded templates: %v", err)
|
|
}
|
|
|
|
want := 0
|
|
|
|
for _, e := range embedded {
|
|
if !e.IsDir() && strings.HasSuffix(e.Name(), ".html") && e.Name() != "base.html" {
|
|
want++
|
|
}
|
|
}
|
|
|
|
if want == 0 {
|
|
t.Fatal("no page templates were embedded, so this test proves nothing")
|
|
}
|
|
|
|
if got := len(r.Names()); got != want {
|
|
t.Errorf("compiled %d pages (%v), embedded %d", got, r.Names(), want)
|
|
}
|
|
}
|
|
|
|
// TestIndexRendersEmbeddedContent renders the page the service serves
|
|
// at /, with real data, and checks that the base document, both
|
|
// partials and the page body all made it into one response.
|
|
func TestIndexRendersEmbeddedContent(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newRenderer(t)
|
|
|
|
data := render.IndexPage{
|
|
Page: r.NewPage(template.HTML(`<input type="hidden" name="csrf" />`)),
|
|
WidgetCount: 1,
|
|
Widgets: []database.Widget{
|
|
{
|
|
ID: "abc", Name: "a widget", SizeBytes: 4096,
|
|
CreatedAt: time.Now().Add(-time.Hour),
|
|
},
|
|
},
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
|
|
err := r.Execute(&buf, "index.html", data)
|
|
if err != nil {
|
|
t.Fatalf("rendering index: %v", err)
|
|
}
|
|
|
|
out := buf.String()
|
|
|
|
for _, want := range []string{
|
|
"<!doctype html>", // base document
|
|
`href="/static/css/style.css"`, // base document links the embedded asset
|
|
"<nav>", // navbar partial
|
|
"<footer>", // footer partial
|
|
"a widget", // page data
|
|
"4.0 KiB", // the bytes template func
|
|
"ago", // the since template func
|
|
`name="csrf"`, // the CSRF field was placed
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("rendered page is missing %q\n---\n%s", want, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestHTMLEscapesUserData: the name comes from a form, and html/template
|
|
// is only a defence if the data actually goes through it.
|
|
func TestHTMLEscapesUserData(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newRenderer(t)
|
|
|
|
data := render.IndexPage{
|
|
Page: r.NewPage(""),
|
|
WidgetCount: 1,
|
|
Widgets: []database.Widget{
|
|
{ID: "x", Name: `<script>alert(1)</script>`, CreatedAt: time.Now()},
|
|
},
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
|
|
err := r.Execute(&buf, "index.html", data)
|
|
if err != nil {
|
|
t.Fatalf("rendering: %v", err)
|
|
}
|
|
|
|
if strings.Contains(buf.String(), "<script>alert(1)</script>") {
|
|
t.Error("a widget name was rendered as live markup")
|
|
}
|
|
}
|
|
|
|
// TestPagesDoNotShareBlocks: index.html and error.html both define
|
|
// "title" and "content". Parsed into one set, the last one parsed would
|
|
// define both for everybody, and every page would render as whichever
|
|
// that was.
|
|
func TestPagesDoNotShareBlocks(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newRenderer(t)
|
|
|
|
var buf bytes.Buffer
|
|
|
|
err := r.Execute(&buf, "error.html", render.ErrorPage{
|
|
Page: r.NewPage(""), Status: 404, Message: "gone fishing",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("rendering error page: %v", err)
|
|
}
|
|
|
|
out := buf.String()
|
|
|
|
if !strings.Contains(out, "gone fishing") {
|
|
t.Errorf("error page did not render its own content:\n%s", out)
|
|
}
|
|
|
|
if strings.Contains(out, "widgets (") {
|
|
t.Error("the error page rendered the index's content block")
|
|
}
|
|
}
|
|
|
|
// TestUnknownTemplateIsAnError, rather than an empty 200.
|
|
func TestUnknownTemplateIsAnError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newRenderer(t)
|
|
|
|
err := r.Execute(&bytes.Buffer{}, "nope.html", nil)
|
|
if err == nil {
|
|
t.Fatal("rendering a template that does not exist must fail")
|
|
}
|
|
}
|
|
|
|
// TestHTMLWritesStatusAndType checks the response contract, since the
|
|
// handlers rely on it for every page they serve.
|
|
func TestHTMLWritesStatusAndType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := newRenderer(t)
|
|
w := httptest.NewRecorder()
|
|
|
|
err := r.HTML(w, 404, "error.html", render.ErrorPage{
|
|
Page: r.NewPage(""), Status: 404, Message: "no",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("rendering: %v", err)
|
|
}
|
|
|
|
if w.Code != 404 {
|
|
t.Errorf("status = %d, want 404", w.Code)
|
|
}
|
|
|
|
if ct := w.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/html") {
|
|
t.Errorf("Content-Type = %q", ct)
|
|
}
|
|
}
|