aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/web_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/web_test.go')
-rw-r--r--internal/web/web_test.go192
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)
+ }
}