package calendartest import ( "net/http" "net/http/httptest" "regexp" "strings" "testing" calendar "amadan.net/oficina/calendar/internal/calendar" ) // hashedStylesheet is the fingerprinted URL shape a page links: // /static/tokens.<16 hex>.css. var hashedStylesheet = regexp.MustCompile(`/static/tokens\.[0-9a-f]{16}\.css`) func TestIndexRendersForASignedInPerson(t *testing.T) { app, session := signedIn(t, "someone@example.com") rec := follow(t, app.Handler(), session, "/") if rec.Code != http.StatusOK { t.Fatalf("GET /: status %d, want 200", rec.Code) } // The day page opens with its own sentence, which is the one thing // every day has whether or not anything is on it. if !strings.Contains(rec.Body.String(), "cal-sentence") { t.Errorf("GET /: the day page is missing its summary sentence:\n%s", rec.Body.String()) } } func TestIndexLinksFingerprintedStylesheet(t *testing.T) { // The sign-in page, not the index: this pins the asset pipeline, // which every page shares, and a signed-out page is the one every // visitor reaches. h := newApp(t).Handler() href := hashedStylesheet.FindString(get(t, h, "/signin").Body.String()) if href == "" { t.Fatal("sign-in page links no fingerprinted stylesheet") } rec := get(t, h, href) if rec.Code != http.StatusOK { t.Fatalf("GET %s: status %d, want 200", href, rec.Code) } if got := rec.Header().Get("Cache-Control"); got != "public, max-age=31536000, immutable" { t.Errorf("GET %s: Cache-Control = %q, want immutable year", href, got) } } // The bare name keeps working, just never long-cached — so a changed // file always shows on an ordinary reload. func TestBareAssetNameStaysFresh(t *testing.T) { rec := get(t, newApp(t).Handler(), "/static/tokens.css") if rec.Code != http.StatusOK { t.Fatalf("GET /static/tokens.css: status %d, want 200", rec.Code) } if got := rec.Header().Get("Cache-Control"); got != "no-cache" { t.Errorf("Cache-Control = %q, want no-cache", got) } } // ErrorPage is what main.go points Options.ErrorPage at; called // directly, it pins that a panic's page is the framework's error-page // partial rather than a stray net/http default. func TestErrorPageRendersFrameworkPartial(t *testing.T) { rec := httptest.NewRecorder() calendar.ErrorPage(rec, httptest.NewRequest(http.MethodGet, "/", nil), http.StatusInternalServerError, "abc123") if rec.Code != http.StatusInternalServerError { t.Fatalf("ErrorPage: status %d, want %d", rec.Code, http.StatusInternalServerError) } body := rec.Body.String() if !strings.Contains(body, "If it keeps happening, quote the reference below.") { t.Errorf("ErrorPage body missing the 500 body (spec §3.2):\n%s", body) } if !strings.Contains(body, "abc123") { t.Errorf("ErrorPage body missing the ref:\n%s", body) } } // The error pages say what the shared-chrome spec's §3.2 table says (estilo // docs/superpowers/specs/2026-09-23-shared-chrome.md): the tab title, the // h1, and a way back that names the app. A 500 or 503 on a GET offers the // same URL again; anything else goes to Calendar. func TestErrorPagesUseTheSuiteCopy(t *testing.T) { for _, c := range []struct { status int method string title, h1, cta string ctaHref string }{ {http.StatusNotFound, http.MethodGet, "Page not found · Calendar", "We can’t find that page", "Go to Calendar", "/"}, {http.StatusForbidden, http.MethodGet, "No access · Calendar", "You don’t have access to this page", "Go to Calendar", "/"}, {http.StatusInternalServerError, http.MethodGet, "Something went wrong · Calendar", "Something went wrong", "Try again", "/week/2026-09-24?x=1"}, {http.StatusInternalServerError, http.MethodPost, "Something went wrong · Calendar", "Something went wrong", "Go to Calendar", "/"}, {http.StatusServiceUnavailable, http.MethodGet, "Calendar is unavailable · Calendar", "Calendar is unavailable right now", "Try again", "/week/2026-09-24?x=1"}, } { rec := httptest.NewRecorder() calendar.ErrorPage(rec, httptest.NewRequest(c.method, "/week/2026-09-24?x=1", nil), c.status, "") body := rec.Body.String() if rec.Code != c.status { t.Errorf("%d %s: status %d", c.status, c.method, rec.Code) } if !strings.Contains(body, ""+c.title+"") { t.Errorf("%d %s: no %s", c.status, c.method, c.title) } if !regexp.MustCompile(`

\s*` + regexp.QuoteMeta(c.h1) + `\s*

`).MatchString(body) { t.Errorf("%d %s: no h1 %q", c.status, c.method, c.h1) } cta := regexp.MustCompile(`([^<]*)`).FindStringSubmatch(body) if cta == nil || cta[2] != c.cta || strings.ReplaceAll(cta[1], "&", "&") != c.ctaHref { t.Errorf("%d %s: primary way back = %q, want %q → %s", c.status, c.method, cta, c.cta, c.ctaHref) } } }