oficina / calendar Public

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

Plain git — no account needed to clone.

Download

Download this file

1package calendartest
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "regexp"
7 "strings"
8 "testing"
9
10 calendar "amadan.net/oficina/calendar/internal/calendar"
11)
12
13// hashedStylesheet is the fingerprinted URL shape a page links:
14// /static/tokens.<16 hex>.css.
15var hashedStylesheet = regexp.MustCompile(`/static/tokens\.[0-9a-f]{16}\.css`)
16
17func TestIndexRendersForASignedInPerson(t *testing.T) {
18 app, session := signedIn(t, "someone@example.com")
19 rec := follow(t, app.Handler(), session, "/")
20 if rec.Code != http.StatusOK {
21 t.Fatalf("GET /: status %d, want 200", rec.Code)
22 }
23 // The day page opens with its own sentence, which is the one thing
24 // every day has whether or not anything is on it.
25 if !strings.Contains(rec.Body.String(), "cal-sentence") {
26 t.Errorf("GET /: the day page is missing its summary sentence:\n%s", rec.Body.String())
27 }
28}
29
30func TestIndexLinksFingerprintedStylesheet(t *testing.T) {
31 // The sign-in page, not the index: this pins the asset pipeline,
32 // which every page shares, and a signed-out page is the one every
33 // visitor reaches.
34 h := newApp(t).Handler()
35 href := hashedStylesheet.FindString(get(t, h, "/signin").Body.String())
36 if href == "" {
37 t.Fatal("sign-in page links no fingerprinted stylesheet")
38 }
39 rec := get(t, h, href)
40 if rec.Code != http.StatusOK {
41 t.Fatalf("GET %s: status %d, want 200", href, rec.Code)
42 }
43 if got := rec.Header().Get("Cache-Control"); got != "public, max-age=31536000, immutable" {
44 t.Errorf("GET %s: Cache-Control = %q, want immutable year", href, got)
45 }
46}
47
48// The bare name keeps working, just never long-cached — so a changed
49// file always shows on an ordinary reload.
50func TestBareAssetNameStaysFresh(t *testing.T) {
51 rec := get(t, newApp(t).Handler(), "/static/tokens.css")
52 if rec.Code != http.StatusOK {
53 t.Fatalf("GET /static/tokens.css: status %d, want 200", rec.Code)
54 }
55 if got := rec.Header().Get("Cache-Control"); got != "no-cache" {
56 t.Errorf("Cache-Control = %q, want no-cache", got)
57 }
58}
59
60// ErrorPage is what main.go points Options.ErrorPage at; called
61// directly, it pins that a panic's page is the framework's error-page
62// partial rather than a stray net/http default.
63func TestErrorPageRendersFrameworkPartial(t *testing.T) {
64 rec := httptest.NewRecorder()
65 calendar.ErrorPage(rec, httptest.NewRequest(http.MethodGet, "/", nil), http.StatusInternalServerError, "abc123")
66 if rec.Code != http.StatusInternalServerError {
67 t.Fatalf("ErrorPage: status %d, want %d", rec.Code, http.StatusInternalServerError)
68 }
69 body := rec.Body.String()
70 if !strings.Contains(body, "If it keeps happening, quote the reference below.") {
71 t.Errorf("ErrorPage body missing the 500 body (spec §3.2):\n%s", body)
72 }
73 if !strings.Contains(body, "abc123") {
74 t.Errorf("ErrorPage body missing the ref:\n%s", body)
75 }
76}
77
78// The error pages say what the shared-chrome spec's §3.2 table says (estilo
79// docs/superpowers/specs/2026-09-23-shared-chrome.md): the tab title, the
80// h1, and a way back that names the app. A 500 or 503 on a GET offers the
81// same URL again; anything else goes to Calendar.
82func TestErrorPagesUseTheSuiteCopy(t *testing.T) {
83 for _, c := range []struct {
84 status int
85 method string
86 title, h1, cta string
87 ctaHref string
88 }{
89 {http.StatusNotFound, http.MethodGet, "Page not found · Calendar", "We can’t find that page", "Go to Calendar", "/"},
90 {http.StatusForbidden, http.MethodGet, "No access · Calendar", "You don’t have access to this page", "Go to Calendar", "/"},
91 {http.StatusInternalServerError, http.MethodGet, "Something went wrong · Calendar", "Something went wrong", "Try again", "/week/2026-09-24?x=1"},
92 {http.StatusInternalServerError, http.MethodPost, "Something went wrong · Calendar", "Something went wrong", "Go to Calendar", "/"},
93 {http.StatusServiceUnavailable, http.MethodGet, "Calendar is unavailable · Calendar", "Calendar is unavailable right now", "Try again", "/week/2026-09-24?x=1"},
94 } {
95 rec := httptest.NewRecorder()
96 calendar.ErrorPage(rec, httptest.NewRequest(c.method, "/week/2026-09-24?x=1", nil), c.status, "")
97 body := rec.Body.String()
98 if rec.Code != c.status {
99 t.Errorf("%d %s: status %d", c.status, c.method, rec.Code)
100 }
101 if !strings.Contains(body, "<title>"+c.title+"</title>") {
102 t.Errorf("%d %s: no <title>%s</title>", c.status, c.method, c.title)
103 }
104 if !regexp.MustCompile(`<h1 rst-error-title>\s*` + regexp.QuoteMeta(c.h1) + `\s*</h1>`).MatchString(body) {
105 t.Errorf("%d %s: no h1 %q", c.status, c.method, c.h1)
106 }
107 cta := regexp.MustCompile(`<a rst-btn="primary" href="([^"]*)">([^<]*)</a>`).FindStringSubmatch(body)
108 if cta == nil || cta[2] != c.cta || strings.ReplaceAll(cta[1], "&amp;", "&") != c.ctaHref {
109 t.Errorf("%d %s: primary way back = %q, want %q → %s", c.status, c.method, cta, c.cta, c.ctaHref)
110 }
111 }
112}
113