// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 package payloads import ( "encoding/json" "strings" "testing" "time" "sourcedock.dev/petrbalvin/interpres/v2" "sourcedock.dev/petrbalvin/volumen/internal/config" "sourcedock.dev/petrbalvin/volumen/internal/frontmatter" "sourcedock.dev/petrbalvin/volumen/internal/post" ) const dtoSample = `+++ title = "Ahoj světe" slug = "ahoj-svete" lang = "cs" author = "Petr" tags = ["go", "blog"] date = 2026-08-18 series = "Série" series_order = 2 cover = "/media/cover.webp" cover_alt = "alt" cover_caption = "caption" fediverse_creator = "@petr@mastodon.social" [translations] en = "hello-world" +++ # Hlavička Tělo. ` func TestBuildSummary(t *testing.T) { s := BuildSummary(parsePost(t, dtoSample)) if s.Slug != "ahoj-svete" { t.Fatalf("slug = %q", s.Slug) } if s.URL != "/api/volumen/posts/ahoj-svete" { t.Fatalf("url = %q", s.URL) } if s.ReadingTime != 1 { t.Fatalf("reading_time = %d", s.ReadingTime) } if s.SeriesOrder == nil || *s.SeriesOrder != 2 { t.Fatalf("series_order = %v", s.SeriesOrder) } if len(s.Tags) != 2 || s.Tags[0] != "go" { t.Fatalf("tags = %v", s.Tags) } if s.Translations["en"] != "hello-world" { t.Fatalf("translations = %v", s.Translations) } if s.CoverAlt != "alt" { t.Fatalf("cover_alt = %q", s.CoverAlt) } } func TestBuildSummaryEmptyPostOmitsOptionals(t *testing.T) { s := BuildSummary(post.New(nil, "body")) if s.Slug != "" || s.Title != "" || s.Date != "" || s.Series != "" { t.Fatalf("optional fields should be empty: %+v", s) } if len(s.Tags) != 0 || len(s.Translations) != 0 { t.Fatalf("collections should be empty: %+v", s) } if s.SeriesOrder != nil { t.Fatalf("series_order = %v, want nil", s.SeriesOrder) } // Marshal and confirm the empty optionals are omitted entirely. raw, err := json.Marshal(s) if err != nil { t.Fatalf("marshal: %v", err) } for _, key := range []string{`"slug"`, `"title"`, `"date"`, `"tags"`, `"translations"`, `"series_order"`, `"cover"`} { if strings.Contains(string(raw), key) { t.Fatalf("%s should be omitted: %s", key, raw) } } if !strings.Contains(string(raw), `"excerpt":"body"`) { t.Fatalf("excerpt missing: %s", raw) } } func TestBuildDetail(t *testing.T) { d, err := BuildDetail(parsePost(t, dtoSample), "https://example.com/") if err != nil { t.Fatalf("BuildDetail: %v", err) } if d.HTML == "" || d.TOC == "" || d.Body == "" { t.Fatalf("detail = %+v", d) } if d.Meta.URL != "https://example.com/ahoj-svete" { t.Fatalf("meta url = %q", d.Meta.URL) } for _, want := range []string{ `"@context":"https://schema.org"`, `"@type":"Article"`, `"headline":"Ahoj světe"`, `"inLanguage":"cs"`, `"datePublished":"2026-08-18"`, } { if !strings.Contains(d.Meta.JSONLD, want) { t.Fatalf("json_ld missing %s: %s", want, d.Meta.JSONLD) } } if d.Meta.OG["og:type"] != "article" || d.Meta.OG["og:image"] != "/media/cover.webp" { t.Fatalf("og = %v", d.Meta.OG) } if d.Meta.Twitter["twitter:card"] != "summary_large_image" { t.Fatalf("twitter = %v", d.Meta.Twitter) } } func TestBuildMetaDescriptionFallback(t *testing.T) { p := post.New(nil, "Body without an excerpt and with enough words to fill the description.") meta, err := BuildMeta(p, "") if err != nil { t.Fatalf("BuildMeta: %v", err) } if meta.OG["og:description"] == "" || meta.OG["og:description"] == nil { t.Fatal("og:description empty") } if meta.URL != "/api/volumen/posts/" { t.Fatalf("url = %q", meta.URL) } } func TestBuildSiteShape(t *testing.T) { cfg, err := config.Load(t.TempDir()+"/none.toml", config.Overrides{Port: -1}) if err != nil { t.Fatalf("config: %v", err) } site := BuildSite(cfg) if site.Title != config.DefaultSiteTitle || site.Language != "en" { t.Fatalf("site = %+v", site) } if site.FediverseCreator != "" { t.Fatalf("fediverse_creator = %q, want empty", site.FediverseCreator) } } func TestPlainTextTruncation(t *testing.T) { long := "

" + strings.Repeat("word ", 100) + "

" plain := post.PlainText(long, 20) // Trailing spaces are trimmed before the ellipsis, so the result is // at most limit+1 runes long. if len([]rune(plain)) > 21 || !strings.HasSuffix(plain, "…") { t.Fatalf("plain = %q (%d runes)", plain, len([]rune(plain))) } if plain := post.PlainText("short", 200); plain != "short" { t.Fatalf("plain = %q", plain) } } func TestSeriesOrderPointer(t *testing.T) { meta := frontmatter.NewMeta() meta.Set("series_order", int64(3)) s := BuildSummary(post.New(meta, "")) if s.SeriesOrder == nil || *s.SeriesOrder != 3 { t.Fatalf("series_order = %v", s.SeriesOrder) } meta2 := frontmatter.NewMeta() meta2.Set("date", interpres.LocalDate{Time: time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC)}) if got := BuildSummary(post.New(meta2, "")).Date; got != "2026-01-02" { t.Fatalf("date = %q", got) } }