oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package sheets
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "net/url"
7 "regexp"
8 "strings"
9 "testing"
10)
11
12func get(t *testing.T, path string) *httptest.ResponseRecorder {
13 t.Helper()
14 rec := httptest.NewRecorder()
15 New(Config{}).Router().ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
16 return rec
17}
18
19func TestIndexServesHTML(t *testing.T) {
20 rec := get(t, "/")
21 if rec.Code != http.StatusOK {
22 t.Fatalf("GET / = %d, want 200", rec.Code)
23 }
24 if ct := rec.Header().Get("Content-Type"); ct != "text/html; charset=utf-8" {
25 t.Errorf("Content-Type = %q", ct)
26 }
27 // Affordances, not copy. This test used to assert on four
28 // sentences of prose, and every one of them had to be edited the
29 // day the page moved onto a template — which taught nothing.
30 //
31 // It then asserted `data-theme="day"`, which is worse than
32 // teaching nothing: "day" is a rastrillo THEME NAME and the
33 // attribute takes a light/dark SCHEME, so the assertion passed on
34 // markup whose switcher did nothing at all. A test can only pin a
35 // mistake it shares. The scheme is pinned in theme_test.go now,
36 // against the toggle selectors in theme.css itself.
37 //
38 // "Oficina Sheets" went in round 2 (shared chrome spec §10: the app
39 // is "Sheets"), and the way in is the bar's own Sign in, carrying
40 // the page to come back to, rather than a prose link in the page.
41 for _, want := range []string{
42 "<h1>Sheets</h1>",
43 `<a rst-btn href="/signin?return_to=%2F" estilo-bar-signin>Sign in</a>`, // an anonymous visitor must be able to get in
44 } {
45 if !strings.Contains(rec.Body.String(), want) {
46 t.Errorf("body missing %q", want)
47 }
48 }
49}
50
51// TestIndexDoesNotClaimNothingElseExists: the page said "there is
52// nothing else to reach at this address" while /signin was live, which
53// it told the first person to click a magic link. A page that describes
54// the app has to be corrected when the app changes.
55func TestIndexDoesNotClaimNothingElseExists(t *testing.T) {
56 if body := get(t, "/").Body.String(); strings.Contains(body, "nothing else to reach") {
57 t.Error("root page still claims nothing else is reachable")
58 }
59}
60
61// TestIndexIsNotACatchAll: only "/" renders the index. Every other
62// unknown path is a 404 — the error page for a browser (shared chrome
63// spec §3.3; this used to pin chi's plain "404 page not found"), and
64// plain text for anything that did not ask for HTML.
65func TestIndexIsNotACatchAll(t *testing.T) {
66 for _, path := range []string{"/nope", "/api/sheets", "/healthz/x"} {
67 rec := get(t, path)
68 if rec.Code != http.StatusNotFound {
69 t.Errorf("GET %s = %d, want 404", path, rec.Code)
70 }
71 if strings.Contains(rec.Body.String(), "<h1>Sheets</h1>") {
72 t.Errorf("GET %s rendered the index", path)
73 }
74 if strings.HasPrefix(rec.Header().Get("Content-Type"), "text/html") {
75 t.Errorf("GET %s without Accept: text/html got a page", path)
76 }
77 req := httptest.NewRequest(http.MethodGet, path, nil)
78 req.Header.Set("Accept", "text/html,*/*;q=0.8")
79 page := httptest.NewRecorder()
80 New(Config{}).Router().ServeHTTP(page, req)
81 if page.Code != http.StatusNotFound || !strings.Contains(page.Body.String(), "<title>Page not found · Sheets</title>") {
82 t.Errorf("GET %s as a browser = %d, want the 404 page", path, page.Code)
83 }
84 }
85}
86
87// TestIndexLeaksNothing: the one rule reaches even a static page.
88func TestIndexLeaksNothing(t *testing.T) {
89 // Fingerprinted asset URLs are dropped first: a content hash is hex,
90 // and "chrome.db41….css" is not a database file.
91 body := regexp.MustCompile(`(href|src)="/static/[^"]*"`).ReplaceAllString(get(t, "/").Body.String(), "")
92 for _, forbidden := range []string{"/home/", "oncarlos.com", "sqlite", ".db"} {
93 if strings.Contains(strings.ToLower(body), strings.ToLower(forbidden)) {
94 t.Errorf("page contains %q", forbidden)
95 }
96 }
97}
98
99// TestSignInEndToEnd drives the whole flow the way a person does:
100// request a link, take the link out of the email that was actually sent,
101// open it, press Sign in, and check the root page then says who you are.
102//
103// It exists because every earlier test here proved a piece — the gate,
104// the routes, the page — and the first real click still landed on a page
105// that gave no sign anything had happened. The pieces were all correct.
106func TestSignInEndToEnd(t *testing.T) {
107 app, mail := appWithRecorder(t)
108
109 // 1. Anonymous: the page offers a way in and names nobody.
110 before := httptest.NewRecorder()
111 app.Router().ServeHTTP(before, httptest.NewRequest(http.MethodGet, "/", nil))
112 if strings.Contains(before.Body.String(), "Signed in as") {
113 t.Fatal("anonymous request already looks signed in")
114 }
115
116 // 2. Ask for a link as a member, and take the one that was emailed.
117 postSignin(t, app, "paul@example.com")
118 link := mail.lastLink(t)
119
120 u, err := url.Parse(link)
121 if err != nil {
122 t.Fatalf("parse emailed link %q: %v", link, err)
123 }
124 if u.Query().Get("token") == "" {
125 t.Fatalf("emailed link carries no token: %s", link)
126 }
127
128 // 3. Open it — the confirm door, which spends nothing — and press
129 // its Sign in button, which does.
130 verify := followLink(t, app.Router(), link)
131 cookies := verify.Result().Cookies()
132 if len(cookies) == 0 {
133 t.Fatalf("verify set no cookie (status %d, location %q)",
134 verify.Code, verify.Header().Get("Location"))
135 }
136
137 // 4. The root page sends you on to your sheets rather than
138 // rendering a signpost, and that page says who you are.
139 //
140 // This assertion used to read the root page itself, because the
141 // root page was the whole proof that a click had worked. It is
142 // the same proof either way: the question is whether the person
143 // who clicked can tell they are signed in, and the page they
144 // actually land on is the one that has to answer it.
145 after := httptest.NewRecorder()
146 req := httptest.NewRequest(http.MethodGet, "/", nil)
147 for _, c := range cookies {
148 req.AddCookie(c)
149 }
150 app.Router().ServeHTTP(after, req)
151 if after.Code != http.StatusSeeOther {
152 t.Fatalf("GET / signed in = %d, want 303", after.Code)
153 }
154 if loc := after.Header().Get("Location"); loc != "/sheets" {
155 t.Fatalf("GET / signed in redirects to %q, want /sheets", loc)
156 }
157
158 landing := httptest.NewRecorder()
159 onward := httptest.NewRequest(http.MethodGet, "/sheets", nil)
160 for _, c := range cookies {
161 onward.AddCookie(c)
162 }
163 app.Router().ServeHTTP(landing, onward)
164
165 body := landing.Body.String()
166 if !strings.Contains(body, "paul@example.com") {
167 t.Errorf("page after sign-in does not name the user:\n%s", body)
168 }
169 if !strings.Contains(body, `action="/signout"`) {
170 t.Error("no way to sign out")
171 }
172 if strings.Contains(body, `<a href="/signin">`) {
173 t.Error("still offering sign-in to a signed-in user")
174 }
175}
176
177// TestSignedInRootIsNotASignpost: the root page had two states, and the
178// signed-in one was a button reading "Open your sheets" — a page whose
179// entire content for a signed-in person was a link to the next page.
180// That existed because /sheets did not when the root page was written,
181// and auth lands a fresh session on SignedInPath, which defaults to "/".
182// Both halves are fixed: auth lands on /sheets, and anyone who types the
183// bare address is sent there too.
184func TestSignedInRootIsNotASignpost(t *testing.T) {
185 app, cookies := signIn(t)
186
187 rec := httptest.NewRecorder()
188 req := httptest.NewRequest(http.MethodGet, "/", nil)
189 for _, c := range cookies {
190 req.AddCookie(c)
191 }
192 app.Router().ServeHTTP(rec, req)
193
194 if rec.Code != http.StatusSeeOther {
195 t.Fatalf("GET / signed in = %d, want 303", rec.Code)
196 }
197 if body := rec.Body.String(); strings.Contains(body, "Open your sheets") {
198 t.Error("root page still renders the signpost")
199 }
200}
201
202// TestAnonymousRootStillOffersAWayIn: the redirect above must not cost
203// the anonymous visitor the only page that offers sign-in.
204func TestAnonymousRootStillOffersAWayIn(t *testing.T) {
205 rec := get(t, "/")
206 if rec.Code != http.StatusOK {
207 t.Fatalf("GET / anonymous = %d, want 200", rec.Code)
208 }
209 if !strings.Contains(rec.Body.String(), `estilo-bar-signin>Sign in</a>`) {
210 t.Error("anonymous root page offers no way in")
211 }
212}
213
214// TestSignInIsSingleUse: the same link must not work twice.
215func TestSignInIsSingleUse(t *testing.T) {
216 app, mail := appWithRecorder(t)
217 postSignin(t, app, "paul@example.com")
218 h := app.Router()
219 action, token := openLink(t, h, mail.lastLink(t))
220
221 first := postLink(t, h, action, token)
222 if len(first.Result().Cookies()) == 0 {
223 t.Fatal("first use did not sign in")
224 }
225
226 second := postLink(t, h, action, token)
227 if len(second.Result().Cookies()) != 0 {
228 t.Error("second use of the same link minted a session")
229 }
230 if loc := second.Header().Get("Location"); !strings.Contains(loc, "err=expired") {
231 t.Errorf("second use redirected to %q, want err=expired", loc)
232 }
233}
234