application_test.go
135 lines1
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")
}
}