application_test.go

135 lines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
package application

import (
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"testing/fstest"
)

// msgCtrl is a minimal controller with one template-callable method.
type msgCtrl struct{ BaseController }

func (m *msgCtrl) Message() string { return "hello" }

// probe counts how often it is spawned (via a shared counter reached through a
// pointer that survives the shallow copy) and records the count as a per-instance
// id, so tests can distinguish "one spawn per request" from "one spawn per
// reference".
type probe struct {
	BaseController
	count *int
	id    int
}

func (p *probe) SetRequest(r *http.Request) {
	p.BaseController.SetRequest(r)
	*p.count++
	p.id = *p.count
}

func (p *probe) ID() int { return p.id }

// pageCtrl reaches a sibling controller through Use.
type pageCtrl struct{ BaseController }

func (p *pageCtrl) Greeting() string {
	if h, ok := p.Use("home").(*msgCtrl); ok {
		return h.Message()
	}
	return "none"
}

func render(app *App, view string) string {
	r := withCache(httptest.NewRequest("GET", "/", nil))
	w := httptest.NewRecorder()
	app.render(w, r, view, nil)
	return w.Body.String()
}

func TestRenderControllerAccessor(t *testing.T) {
	views := fstest.MapFS{"views/page.html": {Data: []byte(`<h1>{{home.Message}}</h1>`)}}
	app := newApp(views, WithController("home", &msgCtrl{}))
	if got := render(app, "page.html"); !strings.Contains(got, "hello") {
		t.Fatalf("render: got %q, want it to contain %q", got, "hello")
	}
}

func TestPerRequestMemoization(t *testing.T) {
	n := 0
	views := fstest.MapFS{"views/page.html": {Data: []byte(`{{probe.ID}}|{{probe.ID}}`)}}
	app := newApp(views, WithController("probe", &probe{count: &n}))

	// Two references in one render share one memoized instance → same id.
	if got := render(app, "page.html"); got != "1|1" {
		t.Fatalf("request 1: got %q, want %q", got, "1|1")
	}
	// A second request gets a fresh, isolated instance → next id.
	if got := render(app, "page.html"); got != "2|2" {
		t.Fatalf("request 2: got %q, want %q", got, "2|2")
	}
	if n != 2 {
		t.Fatalf("spawns: got %d, want 2 (one per request, not one per reference)", n)
	}
}

func TestUseBindsSibling(t *testing.T) {
	views := fstest.MapFS{"views/page.html": {Data: []byte(`{{page.Greeting}}`)}}
	app := newApp(views,
		WithController("home", &msgCtrl{}),
		WithController("page", &pageCtrl{}),
	)
	if got := render(app, "page.html"); got != "hello" {
		t.Fatalf("Use: got %q, want %q", got, "hello")
	}
}

func TestValidateCatchesUnknownMethod(t *testing.T) {
	views := fstest.MapFS{"views/bad.html": {Data: []byte(`{{home.Nope}}`)}}
	app := newApp(views, WithController("home", &msgCtrl{}))
	issues := app.Validate()
	if len(issues) == 0 {
		t.Fatal("expected a validation issue for {{home.Nope}}")
	}
	if !strings.Contains(issues[0].Error(), "Nope") {
		t.Fatalf("issue should name the missing method: %v", issues[0])
	}
}

func TestValidatePassesValidAndSkipsData(t *testing.T) {
	// {{home.Message}} resolves; {{.Anything}} is raw data access the validator
	// must not flag.
	views := fstest.MapFS{"views/ok.html": {Data: []byte(`{{home.Message}} {{.Anything}}`)}}
	app := newApp(views, WithController("home", &msgCtrl{}))
	if issues := app.Validate(); len(issues) != 0 {
		t.Fatalf("expected no issues, got: %v", issues)
	}
}

func TestServeHTTPInstallsCache(t *testing.T) {
	n := 0
	views := fstest.MapFS{"views/page.html": {Data: []byte(`{{probe.ID}}-{{probe.ID}}`)}}
	app := newApp(views, WithController("probe", &probe{count: &n}))
	app.Handle("GET /", app.Serve("page.html", nil))

	srv := httptest.NewServer(app)
	defer srv.Close()

	get := func() string {
		resp, err := http.Get(srv.URL + "/")
		if err != nil {
			t.Fatal(err)
		}
		defer resp.Body.Close()
		body, _ := io.ReadAll(resp.Body)
		return string(body)
	}
	if got := get(); got != "1-1" {
		t.Fatalf("first request: got %q, want %q (memoized through ServeHTTP)", got, "1-1")
	}
	if got := get(); got != "2-2" {
		t.Fatalf("second request: got %q, want %q", got, "2-2")
	}
}