diff options
Diffstat (limited to 'internal/web/web_test.go')
| -rw-r--r-- | internal/web/web_test.go | 111 |
1 files changed, 93 insertions, 18 deletions
diff --git a/internal/web/web_test.go b/internal/web/web_test.go index 5078c44..ae26029 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -1,15 +1,20 @@ package web import ( + "io/fs" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" + "regexp" "strings" "testing" "time" "github.com/google/uuid" "github.com/gramanas/blogspace/internal/config" + "github.com/gramanas/blogspace/internal/i18n" "github.com/gramanas/blogspace/internal/store" ) @@ -188,16 +193,16 @@ func TestModuleKinds(t *testing.T) { if !moduleHasSettings("html") || moduleHasSettings("rss") { t.Error("moduleHasSettings") } - if got := moduleSummary(store.Module{Kind: "recent", Count: 5, Title: "Fresh"}); got != "5 posts, heading “Fresh”" { + if got := moduleSummary("en", store.Module{Kind: "recent", Count: 5, Title: "Fresh"}); got != "5 posts, heading “Fresh”" { t.Errorf("recent summary: %q", got) } - if got := moduleSummary(store.Module{Kind: "html", Body: " <p>hi\n there</p> "}); got != "<p>hi there</p>" { + if got := moduleSummary("en", store.Module{Kind: "html", Body: " <p>hi\n there</p> "}); got != "<p>hi there</p>" { t.Errorf("html summary: %q", got) } - if got := moduleSummary(store.Module{Kind: "html", Body: strings.Repeat("x", 100)}); len([]rune(got)) != 58 || !strings.HasSuffix(got, "…") { + if got := moduleSummary("en", store.Module{Kind: "html", Body: strings.Repeat("x", 100)}); len([]rune(got)) != 58 || !strings.HasSuffix(got, "…") { t.Errorf("long html summary: %q", got) } - if got := moduleSummary(store.Module{Kind: "menu"}); got != "" { + if got := moduleSummary("en", store.Module{Kind: "menu"}); got != "" { t.Errorf("menu summary: %q", got) } } @@ -222,7 +227,7 @@ func TestGroupArchive(t *testing.T) { if len(years) != 2 || years[0].Year != 2026 || years[0].Count != 3 || years[1].Year != 2025 || years[1].Count != 1 { t.Fatalf("years: %+v", years) } - if len(years[0].Months) != 2 || years[0].Months[0].Name != "September" || len(years[0].Months[0].Posts) != 2 || years[0].Months[1].Name != "February" { + if len(years[0].Months) != 2 || years[0].Months[0].Month != time.September || len(years[0].Months[0].Posts) != 2 || years[0].Months[1].Month != time.February { t.Errorf("months: %+v", years[0].Months) } if years[0].Months[0].Posts[0].Title != "d" || years[1].Months[0].Posts[0].Title != "a" { @@ -294,26 +299,96 @@ func TestPresets(t *testing.T) { } } -func TestFormatDate(t *testing.T) { - at := time.Date(2026, 9, 3, 10, 0, 0, 0, time.UTC) - for format, want := range map[string]string{"long": "3 September 2026", "short": "3 Sep 2026", "iso": "2026-09-03"} { - th := Theme{DateFormat: format} - if got := th.FormatDate(at); got != want { - t.Errorf("%s: got %q, want %q", format, got, want) - } - } -} - func TestAllTemplatesParse(t *testing.T) { - tpl := newTemplates(false, funcs) + tpl := newTemplates(false) for _, name := range []string{"auth/login.html", "dashboard/overview.html", "dashboard/pages.html", "dashboard/page_form.html", "dashboard/posts.html", "dashboard/post_form.html", "dashboard/design.html", "dashboard/images.html", "dashboard/settings.html", "dashboard/password.html", "dashboard/confirm.html", "dashboard/sections.html", "dashboard/section_form.html", "dashboard/layout.html", "dashboard/module_form.html", "admin/index.html", "admin/new_user.html", "admin/delete_user.html", "blog/page.html", "blog/post.html", "blog/404.html"} { - if _, err := tpl.get(name); err != nil { - t.Errorf("%s: %v", name, err) + for _, l := range i18n.Languages() { + if _, err := tpl.get(l.Code, name); err != nil { + t.Errorf("%s (%s): %v", name, l.Code, err) + } + } + } +} + +// Every string the templates and handlers translate must be in the Greek +// catalog, and the catalog must not keep entries nothing uses any more (a +// reworded English key would otherwise silently fall back to English). +func TestGreekCatalogComplete(t *testing.T) { + used := map[string]bool{} + add := func(re *regexp.Regexp, src string) { + for _, m := range re.FindAllStringSubmatch(src, -1) { + used[m[1]] = true + } + } + tplKey := regexp.MustCompile(`\b(?:t|tf) "((?:[^"\\]|\\.)*)"`) + goKey := regexp.MustCompile(`\b(?:s\.trf?\(r|i18n\.Tf?\([A-Za-z]+), "((?:[^"\\]|\\.)*)"`) + if err := fs.WalkDir(assets, "templates", func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + b, err := fs.ReadFile(assets, path) + add(tplKey, string(b)) + return err + }); err != nil { + t.Fatal(err) + } + goFiles, _ := filepath.Glob("*.go") + for _, f := range goFiles { + if strings.HasSuffix(f, "_test.go") { + continue + } + b, err := os.ReadFile(f) + if err != nil { + t.Fatal(err) + } + add(goKey, string(b)) + } + // Keys that reach t/tr through a variable rather than a literal. + for _, m := range []map[string]string{moduleNames, areaNames} { + for _, v := range m { + used[v] = true } } + for _, p := range Presets() { + used[p.Name] = true + } + for _, k := range []string{"top", "bottom", store.RoleSuperadmin, store.RoleBlogger} { + used[k] = true + } + have := map[string]bool{} + for _, k := range i18n.Keys("el") { + have[k] = true + if !used[k] { + t.Errorf("el catalog: %q is not used anywhere", k) + } + } + for k := range used { + if !have[k] { + t.Errorf("el catalog: missing %q", k) + } + } + if len(used) < 100 { + t.Errorf("only %d keys found; the scan is broken", len(used)) + } +} + +// Anonymous pages follow the browser's language; unknown ones fall back to English. +func TestLangFromAcceptLanguage(t *testing.T) { + s := NewServer(&config.Config{BaseDomain: "example.com", JWTSecret: []byte("x")}, nil) + req := httptest.NewRequest("GET", "/webadmin", nil) + if got := s.lang(req); got != "en" { + t.Errorf("no header: %q", got) + } + req.Header.Set("Accept-Language", "el-GR,el;q=0.9,en;q=0.8") + if got := s.lang(req); got != "el" { + t.Errorf("greek header: %q", got) + } + if got := s.lang(withLang(req, "en")); got != "en" { + t.Errorf("context wins: %q", got) + } } // /webadmin on a blog's own host bounces to the root login page and back to that blog's dashboard. |
