diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-28 12:52:13 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-28 12:52:13 +0200 |
| commit | 44472bbe31475a3c672ae003d928d7caffb4b50a (patch) | |
| tree | e74bae03c61b62479ef4f68b046e3345618933c8 /internal/web/server_test.go | |
| parent | 626f2275438a37e5cd2efb5964281b9bef5ce8cc (diff) | |
| parent | 7b220084cf3951c8cde0582efdfcf628afc64336 (diff) | |
| download | lectio-44472bbe31475a3c672ae003d928d7caffb4b50a.tar.gz lectio-44472bbe31475a3c672ae003d928d7caffb4b50a.zip | |
Merge branch 'of-offline-migration': offline daily view, remove scrapers
Diffstat (limited to 'internal/web/server_test.go')
| -rw-r--r-- | internal/web/server_test.go | 171 |
1 files changed, 69 insertions, 102 deletions
diff --git a/internal/web/server_test.go b/internal/web/server_test.go index 12542e5..57286d5 100644 --- a/internal/web/server_test.go +++ b/internal/web/server_test.go @@ -15,22 +15,9 @@ import ( "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. +// TestServer exercises NewServer's handler tree end to end via httptest. Every +// reading is computed offline (no network, no cache, no fixture server). 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) { @@ -41,20 +28,24 @@ func TestServer(t *testing.T) { } 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. + // label is localised to "Gospel" (render.LocalizeHeading). if !strings.Contains(body, "Gospel") { t.Errorf("body missing reading heading: %q", body) } + // ?v=wuj -> the gospel is rendered from the Wujek corpus ("grobu" is + // distinctly Polish Wujek verse text, not Latin/English). + if !strings.Contains(body, "grobu") { + t.Errorf("body missing Wujek verse text: %q", body) + } if !strings.Contains(body, "htmx") { t.Errorf("body missing htmx reference") } if !strings.Contains(body, `id="theme"`) { t.Errorf("body missing theme <link>") } - // 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") { + // The offline engine localises the celebration name to the UI language + // (en): 2026-07-22 is Saint Mary Magdalene. + if !strings.Contains(body, `class="dayinfo"`) || !strings.Contains(body, "Saint Mary Magdalene") { t.Errorf("body missing day-info header: %q", body) } }) @@ -187,18 +178,7 @@ func TestServer(t *testing.T) { // Regression coverage for the ?date= path-traversal finding: resolveQuery // must reject anything that isn't YYYY-MM-DD and fall back to today(), // the same "normalize, don't trust" pattern requestDisplay already uses. - t.Run("date path traversal does not read a planted cache file", func(t *testing.T) { - // cacheDir() == filepath.Join(cacheHome, "lectio"), so - // filepath.Join(cacheDir(), "../evil"+".json") resolves to - // cacheHome/evil.json -- one level *above* the real cache dir, and - // only reachable via an unvalidated "../" date. If the marker below - // ever appears in a response, liturgy.Load read this planted file. - evilPath := filepath.Join(cacheHome, "evil.json") - evilJSON := `[{"Heading":"LEAKED-VIA-TRAVERSAL","PartID":"ewangelia","Paragraphs":[["s"]]}]` - if err := os.WriteFile(evilPath, []byte(evilJSON), 0o644); err != nil { - t.Fatal(err) - } - + t.Run("date path traversal 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() if baseline.Code != http.StatusOK { @@ -210,12 +190,8 @@ func TestServer(t *testing.T) { 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()) + if rec.Body.String() != baseline.Body.String() { + t.Errorf("traversal date did not fall back to today() identically to omitting date\n got: %q\nwant: %q", rec.Body.String(), baseline.Body.String()) } }) @@ -234,23 +210,21 @@ func TestServer(t *testing.T) { }) } -// 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) { +// TestBTCheckboxRemoved checks index.html no longer renders a "bt" version +// checkbox (the niedziela.pl corpus was retired; bibleVersions is now +// wuj,vul,grb,drb) for either lectionary, while the real version boxes remain. +func TestBTCheckboxRemoved(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") + for _, lect := range []string{"traditional", "new"} { + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, httptest.NewRequest("GET", "/?lectionary="+lect, nil)) + b := rec.Body.String() + if strings.Contains(b, `id="ver-bt"`) { + t.Errorf("%s: bt checkbox should no longer be rendered", lect) + } + if !strings.Contains(b, `id="ver-wuj"`) { + t.Errorf("%s: wuj checkbox missing", lect) + } } } @@ -325,17 +299,6 @@ func TestRenderOrErrorNoSectionsLang(t *testing.T) { // TestIndexHTMLLangAttribute checks index.html's <html lang="..."> follows // cfg.UILanguage (finding §6) instead of being hardcoded "pl". func TestIndexHTMLLangAttribute(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") - t.Setenv("XDG_CACHE_HOME", t.TempDir()) - cfg := config.Default() cfg.UILanguage = "en" rec := httptest.NewRecorder() @@ -422,11 +385,11 @@ func TestVsetEmptyShowsNothing(t *testing.T) { t.Errorf("readings with vset and no v should be empty, got %q", b) } - // Fresh visit (no vset): config default (bt for modern) box is checked. + // Fresh visit (no vset): the config default (vul) box is checked. fresh := httptest.NewRecorder() srv.ServeHTTP(fresh, httptest.NewRequest("GET", "/", nil)) - if !strings.Contains(fresh.Body.String(), `value="bt" checked`) { - t.Errorf("fresh visit should check the config default (bt) version box") + if !strings.Contains(fresh.Body.String(), `value="vul" checked`) { + t.Errorf("fresh visit should check the config default (vul) version box") } // Full page with vset and no v: no VERSION box checked (mono may be). @@ -439,32 +402,31 @@ func TestVsetEmptyShowsNothing(t *testing.T) { } } -// TestTraditionalDropsPhantomBT guards the traditional case of "no version -> -// nothing": a phantom checked-but-hidden bt (carried over from modern) must not -// substitute to wuj on an explicit form submit, but a fresh visit keeps the -// bt->wuj default. -func TestTraditionalDropsPhantomBT(t *testing.T) { +// TestBTSubstitutesToWuj: the legacy "bt" version has no corpus, so an explicit +// ?v=bt now renders the Wujek column instead (render.EffectiveVersions maps +// bt->wuj unconditionally, offline) for both lectionaries -- there is no longer +// a traditional-only "drop bt" special case. A fresh visit checks the config +// default (vul), never bt. +func TestBTSubstitutesToWuj(t *testing.T) { srv := NewServer(config.Default()) - // Explicit submit, only the phantom bt "checked": empty pane. - phantom := httptest.NewRecorder() - srv.ServeHTTP(phantom, httptest.NewRequest("GET", "/readings?vset=1&lectionary=traditional&v=bt", nil)) - if b := strings.TrimSpace(phantom.Body.String()); b != "" { - t.Errorf("traditional vset+v=bt should be empty, got %d bytes", len(b)) - } - - // Explicit submit, bt phantom + a real corpus version: still renders it. - withWuj := httptest.NewRecorder() - srv.ServeHTTP(withWuj, httptest.NewRequest("GET", "/readings?vset=1&lectionary=traditional&v=bt&v=wuj", nil)) - if !strings.Contains(withWuj.Body.String(), "block") { - t.Errorf("traditional vset+v=bt+v=wuj should still render wuj") + for _, lect := range []string{"new", "traditional"} { + rec := httptest.NewRecorder() + srv.ServeHTTP(rec, httptest.NewRequest("GET", "/readings?vset=1&date=2026-07-22&lectionary="+lect+"&v=bt", nil)) + b := rec.Body.String() + if strings.TrimSpace(b) == "" { + t.Errorf("%s: vset+v=bt should substitute wuj and render, got empty", lect) + } + if !strings.Contains(b, "Wujek") { + t.Errorf("%s: vset+v=bt should render the Wujek column: %q", lect, b) + } } - // Fresh visit (no vset): the bt->wuj default is kept and wuj is checked. + // Fresh traditional visit (no vset): the config default (vul) is checked. fresh := httptest.NewRecorder() srv.ServeHTTP(fresh, httptest.NewRequest("GET", "/?lectionary=traditional", nil)) - if !strings.Contains(fresh.Body.String(), `value="wuj" checked`) { - t.Errorf("fresh traditional visit should default to wuj (checked)") + if !strings.Contains(fresh.Body.String(), `value="vul" checked`) { + t.Errorf("fresh traditional visit should default to vul (checked)") } } @@ -521,13 +483,12 @@ func TestSettingsPostAppliesLive(t *testing.T) { form := url.Values{} form.Set("lectionary", "new") - form.Set("traditional_lang", "pl") form.Set("ui_language", "pl") // change it form.Set("sigla_style", "auto") form.Set("web_display", "vertical") form.Set("web_theme", "transfiguration") - form.Set("default_version", "bt") - form["versions"] = []string{"bt", "wuj", "vul", "grb", "drb"} + form.Set("default_version", "vul") + form["versions"] = []string{"wuj", "vul", "grb", "drb"} form.Set("books", string(bibleDefaultBooks())) post := httptest.NewRequest("POST", "/settings", strings.NewReader(form.Encode())) @@ -554,8 +515,8 @@ func TestSettingsPostInvalidBooks(t *testing.T) { form.Set("ui_language", "en") form.Set("web_display", "vertical") form.Set("web_theme", "transfiguration") - form.Set("default_version", "bt") - form["versions"] = []string{"bt"} + form.Set("default_version", "vul") + form["versions"] = []string{"wuj"} form.Set("books", "this is not [valid toml") post := httptest.NewRequest("POST", "/settings", strings.NewReader(form.Encode())) post.Header.Set("Content-Type", "application/x-www-form-urlencoded") @@ -584,8 +545,8 @@ func TestSettingsPostEmptyBooksPreservesFile(t *testing.T) { form.Set("ui_language", "en") form.Set("web_display", "vertical") form.Set("web_theme", "transfiguration") - form.Set("default_version", "bt") - form["versions"] = []string{"bt"} // no "books" field + form.Set("default_version", "vul") + form["versions"] = []string{"wuj"} // no "books" field post := httptest.NewRequest("POST", "/settings", strings.NewReader(form.Encode())) post.Header.Set("Content-Type", "application/x-www-form-urlencoded") rec := httptest.NewRecorder() @@ -634,15 +595,21 @@ func TestBookmarksFlow(t *testing.T) { } } -func TestExportNoReadings(t *testing.T) { - t.Setenv("XDG_CACHE_HOME", t.TempDir()) - cfg := config.Default() - cfg.Offline = true // no network; uncached date -> no readings - srv := NewServer(cfg) +// TestExportSucceeds: the offline engine computes readings for any valid date, +// so /export always has content to render (there is no "no readings" 404/500 +// path for a normal date anymore). Even a far-future date exports successfully. +func TestExportSucceeds(t *testing.T) { + srv := NewServer(config.Default()) rec := httptest.NewRecorder() srv.ServeHTTP(rec, httptest.NewRequest("GET", "/export?fmt=md&date=2099-01-01", nil)) - if rec.Code != http.StatusNotFound && rec.Code != http.StatusInternalServerError { - t.Errorf("export with no readings status=%d (want 404/500)", rec.Code) + if rec.Code != http.StatusOK { + t.Fatalf("export status=%d (want 200), body=%q", rec.Code, rec.Body.String()) + } + if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "markdown") { + t.Errorf("Content-Type = %q, want markdown", ct) + } + if !strings.Contains(rec.Body.String(), "## Gospel") { + t.Errorf("export body missing a gospel section:\n%s", rec.Body.String()) } } |
