diff options
Diffstat (limited to 'internal/web/web_test.go')
| -rw-r--r-- | internal/web/web_test.go | 192 |
1 files changed, 159 insertions, 33 deletions
diff --git a/internal/web/web_test.go b/internal/web/web_test.go index 9afa11a..b8f761a 100644 --- a/internal/web/web_test.go +++ b/internal/web/web_test.go @@ -51,7 +51,7 @@ func TestHostRoutingWithoutDB(t *testing.T) { func TestRootRoutePrecedence(t *testing.T) { cfg := &config.Config{BaseDomain: "example.com", JWTSecret: []byte("x"), MaxUploadBytes: 1 << 20} s := NewServer(cfg, nil) - for path, want := range map[string]int{"/webadmin": 200, "/healthz": 200, "/dashboard": 303, "/admin/": 303, "/b/alice/posts": 303, "/b/alice/announcements": 303, "/account/password": 303} { + for path, want := range map[string]int{"/webadmin": 200, "/healthz": 200, "/dashboard": 303, "/admin/": 303, "/b/alice/posts": 303, "/b/alice/announcements": 303, "/b/alice/layout": 303, "/account/password": 303} { rec := httptest.NewRecorder() req := httptest.NewRequest("GET", path, nil) req.Host = "example.com" @@ -68,19 +68,25 @@ func TestRootRoutePrecedence(t *testing.T) { } func TestThemeNormalizeAndCSS(t *testing.T) { - th := ParseTheme([]byte(`{"bg_color":"red;}body{display:none","font":"comic","bg_image":"../etc","nav_position":"left-sidebar","link_hover":"blue","heading_font":"wingdings","favicon":"x"}`)) - if th.BgColor != DefaultTheme().BgColor || th.Font != "sans" || th.BgImage != "" || th.NavPosition != "left-sidebar" || th.LinkHover != "" || th.HeadingFont != "same" || th.Favicon != "" { + th := ParseTheme([]byte(`{"bg_color":"red;}body{display:none","font":"comic","bg_image":"../etc","logo":"../etc","link_hover":"blue","heading_font":"wingdings","favicon":"x","logo_size":"huge","left_width":5,"right_width":90}`)) + if th.BgColor != DefaultTheme().BgColor || th.Font != "sans" || th.BgImage != "" || th.Logo != "" || th.LinkHover != "" || th.HeadingFont != "same" || th.Favicon != "" || th.LogoSize != "medium" { t.Errorf("normalize: %+v", th) } - // Themes saved before the new options existed keep today's look. - if !th.ShowDates || !th.FooterShowRSS || th.LinkUnderline != "always" || th.ContentStyle != "card" || th.DateFormat != "long" { + if th.LeftWidth != 15 || th.RightWidth != 40 { + t.Errorf("width clamp: %+v", th) + } + // Themes saved before the new options existed keep today's look: every area on, columns a quarter each. + if !th.ShowDates || th.LinkUnderline != "always" || th.ContentStyle != "card" || th.DateFormat != "long" { t.Errorf("old theme defaults: %+v", th) } - f := url.Values{"bg_color": {"#ABCDEF"}, "content_width": {"wide"}, "header_show_title": {"on"}, "bg_image": {"not-a-uuid"}, + if !th.HeaderOn || !th.LeftOn || !th.RightOn || !th.FooterOn || th.LeftWidth != 15 { + t.Errorf("old theme layout defaults: %+v", th) + } + f := url.Values{"bg_color": {"#ABCDEF"}, "content_width": {"wide"}, "bg_image": {"not-a-uuid"}, "logo_size": {"large"}, "link_hover": {"#ff0000"}, "nav_hover": {"#00ff00"}, "nav_hover_custom": {"on"}, "nav_style": {"uppercase"}, "nav_align": {"center"}, "link_underline": {"hover"}, "heading_font": {"serif"}, "content_style": {"flat"}, "content_padding": {"roomy"}, "date_format": {"iso"}} th = ThemeFromForm(DefaultTheme(), f) - if th.BgColor != "#abcdef" || th.ContentWidth != "wide" || !th.HeaderShowTitle || th.BgImage != "" { + if th.BgColor != "#abcdef" || th.ContentWidth != "wide" || th.BgImage != "" || th.LogoSize != "large" { t.Errorf("from form: %+v", th) } if th.LinkHover != "" || th.NavHover != "#00ff00" || th.ShowDates || th.DateFormat != "iso" { @@ -88,7 +94,7 @@ func TestThemeNormalizeAndCSS(t *testing.T) { } css := th.CSS() for _, want := range []string{"background-color:#abcdef", "max-width:1100px", "text-transform:uppercase", ".site-nav a:hover { color:#00ff00", "a:hover { text-decoration:underline", - "h1, h2, h3, h4, h5, h6 { font-family:Georgia", "text-align:center", "border-radius:0", "padding:2.2em 3em", "@media (max-width: 700px)"} { + "h1, h2, h3, h4, h5, h6 { font-family:Georgia", "text-align:center", "border-radius:0", "padding:2.2em 3em", "@media (max-width: 700px)", ".logo { max-height:140px"} { if !strings.Contains(css, want) { t.Errorf("css missing %q:\n%s", want, css) } @@ -102,20 +108,139 @@ func TestThemeNormalizeAndCSS(t *testing.T) { } } -func TestSidebarWidthCSS(t *testing.T) { - th := ThemeFromForm(DefaultTheme(), url.Values{"nav_position": {"left-sidebar"}, "nav_sidebar_width": {"wide"}}) +func TestLayoutTheme(t *testing.T) { + th := LayoutFromForm(DefaultTheme(), url.Values{"left_width": {"40"}, "right_width": {"40"}, "keep_columns": {"on"}}) + if th.LeftWidth != 40 || th.RightWidth != 30 || th.MainWidth() != 30 || !th.KeepColumns { + t.Errorf("the right column should give way: %+v", th) + } + th = LayoutFromForm(th, url.Values{"left_width": {"junk"}, "right_width": {"20"}}) + if th.LeftWidth != 15 || th.RightWidth != 20 || th.KeepColumns { + t.Errorf("junk clamps to the minimum: %+v", th) + } css := th.CSS() - if !strings.Contains(css, "width:240px") || !strings.Contains(css, "margin-left:260px") || !strings.Contains(css, "margin-left:0") { - t.Errorf("sidebar css: %s", css) + for _, want := range []string{"@media (min-width: 701px)", ".col-left { width:15%", ".col-right { width:20%", "body.has-left.has-right .col-main { width:65%", + "body.has-left:not(.has-right) .col-main { width:85%", "body.has-right:not(.has-left) .col-main { width:80%"} { + if !strings.Contains(css, want) { + t.Errorf("css missing %q:\n%s", want, css) + } + } + th.SetAreaOn("left", false) + th.SetAreaOn("bogus", false) + if th.AreaOn("left") || !th.AreaOn("right") || !th.AreaOn("above") { + t.Errorf("area switches: %+v", th) + } +} + +func TestBuildLayout(t *testing.T) { + mods := []store.Module{{ID: 1, Area: "header", Kind: "title"}, {ID: 2, Area: "left", Kind: "recent", Count: 3}, {ID: 3, Area: "right", Kind: "recent", Count: 8}, + {ID: 4, Area: "right", Kind: "archive"}, {ID: 5, Area: "footer", Kind: "rss"}, {ID: 6, Area: "above", Kind: "html"}} + th := DefaultTheme() + l := buildLayout(th, mods) + if !l.HasLeft || !l.HasRight || len(l.Header) != 1 || len(l.Footer) != 1 || len(l.Above) != 1 || l.MaxRecent() != 8 || !l.NeedsArchive() { + t.Errorf("layout: %+v", l) + } + th.RightOn, th.FooterOn = false, false + l = buildLayout(th, mods) + if l.HasRight || len(l.Footer) != 0 || l.MaxRecent() != 3 || l.NeedsArchive() { + t.Errorf("switched-off areas should drop their modules: %+v", l) + } + if buildLayout(th, nil).HasLeft { + t.Error("an empty column is not shown") + } + th.KeepColumns = true + if k := buildLayout(th, nil); !k.HasLeft || !k.HasRight || len(k.Right) != 0 { + t.Errorf("keep_columns reserves empty and hidden columns: %+v", k) + } + th.KeepColumns = false + // announcements go to their column, or to the main one when that column is not laid out + secs := []store.Section{{ID: 1, Placement: "main-top"}, {ID: 2, Placement: "right-bottom"}, {ID: 3, Placement: "left-top"}, {ID: 4, Placement: "left-bottom"}} + n := placeNotices(secs, l) // l: left laid out, right not + if len(n["main-top"]) != 1 || len(n["main-bottom"]) != 1 || n["main-bottom"][0].ID != 2 || len(n["left-top"]) != 1 || len(n["left-bottom"]) != 1 || len(n["right-bottom"]) != 0 { + t.Errorf("left column only: %v", n) + } + n = placeNotices(secs, Layout{}) + if len(n["main-top"]) != 2 || n["main-top"][1].ID != 3 || len(n["main-bottom"]) != 2 || len(n) != 2 { // no columns: everything in main, order kept + t.Errorf("no columns: %v", n) + } +} + +func TestModuleKinds(t *testing.T) { + for area, kinds := range moduleKinds { + if _, ok := areaNames[area]; !ok { + t.Errorf("area %q has no name", area) + } + for _, k := range kinds { + if moduleNames[k] == "" { + t.Errorf("kind %q has no name", k) + } + } + } + if !allowedKind("header", "menu") || allowedKind("header", "archive") || allowedKind("above", "menu") || allowedKind("attic", "html") { + t.Error("allowedKind") + } + if !moduleHasSettings("html") || moduleHasSettings("rss") { + t.Error("moduleHasSettings") + } + if got := moduleSummary(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>" { + t.Errorf("html summary: %q", got) } - // The width only matters for the sidebar layout, and "normal" is blog.css's own value. - th.NavPosition = "top-bar" - if strings.Contains(th.CSS(), "width:240px") { - t.Error("sidebar width leaked into a horizontal menu") + if got := moduleSummary(store.Module{Kind: "html", Body: strings.Repeat("x", 100)}); len([]rune(got)) != 58 || !strings.HasSuffix(got, "…") { + t.Errorf("long html summary: %q", got) } - th = ThemeFromForm(DefaultTheme(), url.Values{"nav_position": {"left-sidebar"}}) - if strings.Contains(th.CSS(), ".nav-left-sidebar") { - t.Error("normal width should emit nothing") + if got := moduleSummary(store.Module{Kind: "menu"}); got != "" { + t.Errorf("menu summary: %q", got) + } +} + +func TestValidLinkURL(t *testing.T) { + for _, ok := range []string{"https://example.org", "http://example.org/a?b=c", "mailto:me@example.org", "/about", "/news/hello"} { + if !validLinkURL(ok) { + t.Errorf("%q should be accepted", ok) + } + } + for _, bad := range []string{"", "javascript:alert(1)", "//evil.org", "ftp://x", "https://", "example.org", "https://a b", "\"><script>", strings.Repeat("/", 501)} { + if validLinkURL(bad) { + t.Errorf("%q should be rejected", bad) + } + } +} + +func TestGroupArchive(t *testing.T) { + d := func(y int, m time.Month, day int) time.Time { return time.Date(y, m, day, 12, 0, 0, 0, time.UTC) } + posts := []store.PostRef{{Title: "d", CreatedAt: d(2026, 9, 3)}, {Title: "c", CreatedAt: d(2026, 9, 1)}, {Title: "b", CreatedAt: d(2026, 2, 1)}, {Title: "a", CreatedAt: d(2025, 12, 31)}} + years := groupArchive(posts) + 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" { + t.Errorf("months: %+v", years[0].Months) + } + if years[0].Months[0].Posts[0].Title != "d" || years[1].Months[0].Posts[0].Title != "a" { + t.Errorf("post order: %+v", years) + } + if len(groupArchive(nil)) != 0 { + t.Error("empty index") + } +} + +func TestMenuItemLinks(t *testing.T) { + id := int64(7) + page := store.MenuItem{PageID: &id, PageTitle: "About", PageSlug: "about"} + home := store.MenuItem{PageID: &id, PageTitle: "Home", PageSlug: "home", IsHome: true} + link := store.MenuItem{Label: "Photos", URL: "https://example.org/p"} + if page.Href() != "/about" || page.Text() != "About" || home.Href() != "/" || link.Href() != "https://example.org/p" || link.Text() != "Photos" { + t.Errorf("%q %q %q", page.Href(), home.Href(), link.Href()) + } +} + +func TestRedirectOKKeepsAnchor(t *testing.T) { + rec := httptest.NewRecorder() + redirectOK(rec, httptest.NewRequest("GET", "/", nil), "/b/a/layout#menu", "hi there") + if got := rec.Header().Get("Location"); got != "/b/a/layout?ok=hi+there#menu" { + t.Errorf("location: %q", got) } } @@ -142,7 +267,7 @@ func TestPresets(t *testing.T) { t.Fatal("expected several presets") } cur := DefaultTheme() - cur.BgImage, cur.NavPosition, cur.Font, cur.LinkHover = "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "top-bar", "mono", "#123456" + cur.BgImage, cur.LeftWidth, cur.Font, cur.LinkHover = "6ba7b810-9dad-11d1-80b4-00c04fd430c8", 30, "mono", "#123456" for _, p := range Presets() { for _, c := range []string{p.Bg, p.ContentBg, p.Text, p.Link, p.HeaderBg, p.HeaderText, p.NavBg, p.NavText, p.FooterBg, p.FooterText} { if !hexColor.MatchString(c) { @@ -153,7 +278,7 @@ func TestPresets(t *testing.T) { if th.BgColor != p.Bg || th.FooterColor != p.FooterText || th.LinkHover != "" { t.Errorf("preset %s not applied: %+v", p.Key, th) } - if th.BgImage != cur.BgImage || th.NavPosition != "top-bar" || th.Font != "mono" { + if th.BgImage != cur.BgImage || th.LeftWidth != 30 || th.Font != "mono" { t.Errorf("preset %s touched non-colour fields: %+v", p.Key, th) } } @@ -176,7 +301,7 @@ func TestAllTemplatesParse(t *testing.T) { tpl := newTemplates(false, funcs) 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", "admin/index.html", "admin/new_user.html", "admin/delete_user.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) @@ -211,19 +336,20 @@ func TestAppendImageMD(t *testing.T) { } } -func TestSplitSections(t *testing.T) { - secs := []store.Section{{ID: 1, Placement: "above"}, {ID: 2, Placement: "below"}, {ID: 3, Placement: "sidebar"}} - above, below, sidebar := splitSections(secs, true) - if len(above) != 1 || len(below) != 1 || len(sidebar) != 1 || sidebar[0].ID != 3 { - t.Errorf("with sidebar: %v %v %v", above, below, sidebar) - } - above, _, sidebar = splitSections(secs, false) - if len(above) != 2 || len(sidebar) != 0 || above[1].ID != 3 { // sidebar falls back to above, keeping order - t.Errorf("without sidebar: %v %v", above, sidebar) - } +func TestNormalizeSection(t *testing.T) { sec := &store.Section{Placement: "footer", Style: "<script>"} normalizeSection(sec) - if sec.Placement != "above" || sec.Style != "note" { + if sec.Placement != "main-top" || sec.Style != "note" { t.Errorf("normalize: %+v", sec) } + sec = &store.Section{Placement: "right-bottom"} + normalizeSection(sec) + if sec.Placement != "right-bottom" || sec.Column() != "right" || sec.Position() != "bottom" { + t.Errorf("valid placement kept: %+v", sec) + } + sec = &store.Section{Placement: "left-sideways"} + normalizeSection(sec) + if sec.Placement != "left-top" { + t.Errorf("half-valid placement: %+v", sec) + } } |
