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