package web import ( "net" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/lukaszkasprzak/lectio/internal/config" "github.com/lukaszkasprzak/lectio/internal/liturgy" ) // TestServer exercises NewServer's handler tree end to end via httptest, // against the same fixture HTML/hook internal/readings uses (see // readings_test.go TestLoadModernRoutes): no real network, no real browser. func TestServer(t *testing.T) { html, err := os.ReadFile("../liturgy/testdata/2026-07-22.html") if err != nil { t.Fatal(err) } fixtureServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(html) })) defer fixtureServer.Close() liturgy.SetBaseURL(fixtureServer.URL + "/liturgia/%s/Ewangelia") cacheHome := t.TempDir() t.Setenv("XDG_CACHE_HOME", cacheHome) srv := NewServer(config.Default()) t.Run("index page", func(t *testing.T) { rec := httptest.NewRecorder() srv.ServeHTTP(rec, httptest.NewRequest("GET", "/?date=2026-07-22&v=wuj", nil)) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } body := rec.Body.String() // config.Default() -> UILanguage "en", so the gospel heading's part // label is localised to "Gospel" (render.LocalizeHeading); the // citation stays exactly as scraped. if !strings.Contains(body, "Gospel") { t.Errorf("body missing reading heading: %q", body) } if !strings.Contains(body, "htmx") { t.Errorf("body missing htmx reference") } if !strings.Contains(body, `id="theme"`) { t.Errorf("body missing theme ") } // Name is source-language (Polish), never translated, even // though the surrounding chrome is English -- see RenderReadings. if !strings.Contains(body, `class="dayinfo"`) || !strings.Contains(body, "Święto św. Marii Magdaleny") { t.Errorf("body missing day-info header: %q", body) } }) t.Run("readings partial", func(t *testing.T) { rec := httptest.NewRecorder() srv.ServeHTTP(rec, httptest.NewRequest("GET", "/readings?date=2026-07-22&v=wuj&all=1", nil)) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } body := rec.Body.String() if strings.Contains(body, " today() if baseline.Code != http.StatusOK { t.Fatalf("baseline status = %d, want 200", baseline.Code) } rec := httptest.NewRecorder() srv.ServeHTTP(rec, httptest.NewRequest("GET", "/readings?date=../evil&v=wuj", nil)) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } body := rec.Body.String() if strings.Contains(body, "LEAKED-VIA-TRAVERSAL") { t.Fatalf("traversal date reached the planted cache file outside the cache dir: %q", body) } if body != baseline.Body.String() { t.Errorf("traversal date did not fall back to today() identically to omitting date\n got: %q\nwant: %q", body, baseline.Body.String()) } }) t.Run("date query with many ../ segments falls back to today, same as omitting date", func(t *testing.T) { baseline := httptest.NewRecorder() srv.ServeHTTP(baseline, httptest.NewRequest("GET", "/readings?v=wuj", nil)) // no date -> today() rec := httptest.NewRecorder() srv.ServeHTTP(rec, httptest.NewRequest("GET", "/readings?date=../../../../etc/hostname&v=wuj", nil)) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } if rec.Body.String() != baseline.Body.String() { t.Errorf("traversal-shaped date did not behave identically to omitting date\n got: %q\nwant: %q", rec.Body.String(), baseline.Body.String()) } }) } // TestBTHiddenForTraditional checks index.html's server-side initial-hidden // state for the "bt" version checkbox: hidden (inline style) when the // lectionary is traditional (bt is meaningless for missalemeum -- see // render.EffectiveVersions), present and visible otherwise. This is // presentation-only: it does not touch which versions actually load. func TestBTHiddenForTraditional(t *testing.T) { srv := NewServer(config.Default()) trad := httptest.NewRecorder() srv.ServeHTTP(trad, httptest.NewRequest("GET", "/?lectionary=traditional", nil)) if !strings.Contains(trad.Body.String(), `id="ver-bt" style="display:none"`) { t.Errorf("bt checkbox not hidden for traditional") } modern := httptest.NewRecorder() srv.ServeHTTP(modern, httptest.NewRequest("GET", "/?lectionary=new", nil)) b := modern.Body.String() if !strings.Contains(b, `id="ver-bt">`) || strings.Contains(b, `id="ver-bt" style="display:none"`) { t.Errorf("bt checkbox should be visible for modern") } } func TestReaderDefault(t *testing.T) { srv := NewServer(config.Default()) rec := httptest.NewRecorder() srv.ServeHTTP(rec, httptest.NewRequest("GET", "/reader", nil)) if rec.Code != 200 { t.Fatalf("status %d", rec.Code) } body := rec.Body.String() if !strings.Contains(body, `id="reader-root"`) || !strings.Contains(body, `id="pane"`) { t.Errorf("reader page missing root/pane") } if !strings.Contains(body, "Genesis") { // book