oficina / slides Public

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

Plain git — no account needed to clone.

Download

Download this file

1package slidestest
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "os"
7 "path/filepath"
8 "regexp"
9 "strings"
10 "testing"
11
12 slides "amadan.net/oficina/slides/internal/slides"
13)
14
15// hashedStylesheet is the fingerprinted URL shape a page links:
16// /static/tokens.<16 hex>.css.
17var hashedStylesheet = regexp.MustCompile(`/static/tokens\.[0-9a-f]{16}\.css`)
18
19// The scaffold shipped two tests about the placeholder index page and
20// said in AGENTS.md that sign-in would break both, because it does:
21// "/" is behind the session gate now, so a signed-out GET is a 303 with
22// no <link> in its body. They are repointed at /signin, the one screen
23// that is still public — the subject was never the placeholder's
24// wording, it was the framework's asset delivery, and that still earns
25// its place. The placeholder's own "does this render" assertion is
26// covered by TestSigninPageIsPublic (signin_test.go), against a page
27// this app actually has.
28func TestSigninLinksFingerprintedStylesheet(t *testing.T) {
29 h := newApp(t)
30 href := hashedStylesheet.FindString(get(t, h, "/signin").Body.String())
31 if href == "" {
32 t.Fatal("the sign-in page links no fingerprinted stylesheet")
33 }
34 rec := get(t, h, href)
35 if rec.Code != http.StatusOK {
36 t.Fatalf("GET %s: status %d, want 200", href, rec.Code)
37 }
38 if got := rec.Header().Get("Cache-Control"); got != "public, max-age=31536000, immutable" {
39 t.Errorf("GET %s: Cache-Control = %q, want immutable year", href, got)
40 }
41}
42
43// The bare name keeps working, just never long-cached — so a changed
44// file always shows on an ordinary reload.
45func TestBareAssetNameStaysFresh(t *testing.T) {
46 rec := get(t, newApp(t), "/static/tokens.css")
47 if rec.Code != http.StatusOK {
48 t.Fatalf("GET /static/tokens.css: status %d, want 200", rec.Code)
49 }
50 if got := rec.Header().Get("Cache-Control"); got != "no-cache" {
51 t.Errorf("Cache-Control = %q, want no-cache", got)
52 }
53}
54
55// Static assets are mounted outside the session gate on purpose: the
56// sign-in page has to be able to load its own stylesheet before anybody
57// is signed in, or the front door renders unstyled.
58func TestStaticAssetsAreReachableSignedOut(t *testing.T) {
59 rec := get(t, newApp(t), "/static/theme.css")
60 if rec.Code != http.StatusOK {
61 t.Fatalf("GET /static/theme.css signed out: status %d, want 200", rec.Code)
62 }
63}
64
65// ErrorPage is what main.go points Options.ErrorPage at (and the seam
66// to wire into Ctx.ErrorPage as handlers grow); called directly, it
67// pins that a panic's page is the framework's error-page partial, not
68// a stray net/http default.
69func TestErrorPageRendersFrameworkPartial(t *testing.T) {
70 rec := httptest.NewRecorder()
71 req := httptest.NewRequest(http.MethodGet, "/", nil)
72 req.Header.Set("Accept", "text/html")
73 localised(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
74 slides.ErrorPage(w, r, http.StatusInternalServerError, "abc123")
75 })).ServeHTTP(rec, req)
76 if rec.Code != http.StatusInternalServerError {
77 t.Fatalf("ErrorPage: status %d, want %d", rec.Code, http.StatusInternalServerError)
78 }
79 body := rec.Body.String()
80 if !strings.Contains(body, "Something went wrong") {
81 t.Errorf("ErrorPage body missing the 500 title:\n%s", body)
82 }
83 if !strings.Contains(body, "abc123") {
84 t.Errorf("ErrorPage body missing the ref:\n%s", body)
85 }
86}
87
88// Every page failure a browser can navigate to is rendered inside Slides'
89// layout and bar. A fetch that did not ask for HTML remains plain text; the
90// split is by Accept rather than by an accident of which route failed.
91func TestNavigableErrorsUseTheSharedErrorPage(t *testing.T) {
92 a := newApp(t)
93 signedIn := signedIn(t, a, allowedAddress)
94 // The user row is created on the first guarded request; optional chrome
95 // deliberately will not create it while merely resolving an error page.
96 mustGet(t, a, "/", http.StatusOK, signedIn)
97 type errorCase struct {
98 name, path, title string
99 cookies []*http.Cookie
100 }
101 for _, tc := range []errorCase{
102 {"unknown route, signed out", "/no/such/page", "Page not found · Slides", nil},
103 {"unknown route, signed in", "/no/such/page", "Page not found · Slides", signedIn},
104 {"post-only route, signed out", "/signout", "Page not found · Slides", nil},
105 {"post-only route, signed in", "/signout", "Page not found · Slides", signedIn},
106 {"missing deck", "/d/23456789ab", "Deck not available · Slides", signedIn},
107 {"missing share link", "/s/doesnotexist", "Deck not available · Slides", nil},
108 } {
109 t.Run(tc.name, func(t *testing.T) {
110 req := httptest.NewRequest(http.MethodGet, tc.path, nil)
111 req.Header.Set("Accept", "text/html,application/xhtml+xml")
112 for _, c := range tc.cookies {
113 req.AddCookie(c)
114 }
115 rec := httptest.NewRecorder()
116 a.ServeHTTP(rec, req)
117 body := rec.Body.String()
118 if rec.Code != http.StatusNotFound || !strings.HasPrefix(rec.Header().Get("Content-Type"), "text/html") {
119 t.Fatalf("status %d, Content-Type %q; want HTML 404", rec.Code, rec.Header().Get("Content-Type"))
120 }
121 if title := regexp.MustCompile(`<title>([^<]*)</title>`).FindStringSubmatch(body); title == nil || title[1] != tc.title {
122 t.Fatalf("title %v, want %q", title, tc.title)
123 }
124 if !strings.Contains(body, "<header estilo-bar") || !strings.Contains(body, "<div rst-error>") {
125 t.Fatal("error is outside the shared layout and bar")
126 }
127 if account := strings.Contains(body, "<details estilo-account"); account != (tc.cookies != nil) {
128 t.Fatalf("account menu present = %v, want %v", account, tc.cookies != nil)
129 }
130 })
131 }
132 plain := get(t, a, "/no/such/page", signedIn...)
133 if plain.Code != http.StatusNotFound || strings.HasPrefix(plain.Header().Get("Content-Type"), "text/html") || strings.Contains(plain.Body.String(), "<html") {
134 t.Fatalf("non-HTML failure = %d %q, want plain 404", plain.Code, plain.Header().Get("Content-Type"))
135 }
136}
137
138// The only callout vocabulary is estilo's real tones. A valued rst-callout
139// or the old rst-notice spelling looks plausible but receives no callout
140// treatment, so scan every authored template rather than waiting for a path
141// to happen to render one.
142func TestTemplatesUseOnlyCalloutTones(t *testing.T) {
143 files, err := filepath.Glob(filepath.Join("..", "slides", "templates", "*.html"))
144 if err != nil || len(files) == 0 {
145 t.Fatal("find templates", err)
146 }
147 stripComments := regexp.MustCompile(`(?s)\{\{/\*.*?\*/\}\}`)
148 for _, file := range files {
149 body, err := os.ReadFile(file)
150 if err != nil {
151 t.Fatal(err)
152 }
153 source := stripComments.ReplaceAllString(string(body), "")
154 for _, bad := range []string{"rst-notice", `rst-callout="`, `rst-tone="warn"`} {
155 if strings.Contains(source, bad) {
156 t.Errorf("%s uses %q", file, bad)
157 }
158 }
159 }
160}
161