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())
}
})
}
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