Files
volumen/internal/post/post_test.go
T
petrbalvin f8ed33df83
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Initial commit
Assisted-by: GLM 5.3
2026-09-29 10:03:32 +02:00

387 lines
11 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
package post
import (
"strings"
"testing"
"time"
"sourcedock.dev/petrbalvin/interpres/v2"
"sourcedock.dev/petrbalvin/volumen/internal/fediverse"
"sourcedock.dev/petrbalvin/volumen/internal/frontmatter"
)
func frontmatterMeta(m map[string]any) *frontmatter.Meta {
meta := frontmatter.NewMeta()
for k, v := range m {
meta.Set(k, v)
}
return meta
}
func frontmatterPost(m map[string]any) *Post {
return New(frontmatterMeta(m), "")
}
func mustParse(t *testing.T, content string) *Post {
t.Helper()
p, err := Parse(content)
if err != nil {
t.Fatalf("post.Parse: %v", err)
}
return p
}
const sample = `+++
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"
aliases = ["old-slug"]
[translations]
en = "hello-world"
+++
# Hlavička
První odstavec s dostatkem slov pro shrnutí, který popisuje obsah
celého článku a nemá nadpis, takže se z něj dá odvodit excerpt.
## Sekce
Další text.
`
func TestParseBasics(t *testing.T) {
p := mustParse(t, sample)
if p.Slug() != "ahoj-svete" {
t.Fatalf("slug = %q", p.Slug())
}
if p.Title() != "Ahoj světe" || p.Lang() != "cs" || p.Author() != "Petr" {
t.Fatalf("title/lang/author wrong")
}
if tags := p.Tags(); len(tags) != 2 || tags[0] != "go" || tags[1] != "blog" {
t.Fatalf("tags = %v", tags)
}
if p.Draft() || p.AllLangs() {
t.Fatal("draft/all_langs should be false")
}
if p.Translations()["en"] != "hello-world" {
t.Fatalf("translations = %v", p.Translations())
}
if p.Aliases()[0] != "old-slug" {
t.Fatalf("aliases = %v", p.Aliases())
}
if p.Cover() != "/media/cover.webp" || p.CoverAlt() != "alt" || p.CoverCaption() != "caption" {
t.Fatal("cover fields wrong")
}
if !fediverse.Valid(p.FediverseCreator()) {
t.Fatal("fediverse creator should be valid")
}
if p.Series() != "Série" {
t.Fatalf("series = %q", p.Series())
}
if order, ok := p.SeriesOrder(); !ok || order != 2 {
t.Fatalf("series_order = %d, %v", order, ok)
}
if !strings.HasPrefix(p.Body, "# Hlavička") {
t.Fatalf("body = %q", p.Body)
}
}
func TestDateStringAndTimestamp(t *testing.T) {
p := mustParse(t, sample)
if p.DateString() != "2026-08-18" {
t.Fatalf("DateString = %q", p.DateString())
}
d, ok := p.PublishedDate()
if !ok || d.Format("2006-01-02") != "2026-08-18" {
t.Fatalf("PublishedDate = %v, %v", d, ok)
}
ts, ok := p.PublishedTimestamp()
if !ok {
t.Fatal("PublishedTimestamp missing")
}
want := time.Date(2026, 8, 18, 0, 0, 0, 0, time.UTC).UnixMilli()
if ts != want {
t.Fatalf("timestamp = %d, want %d", ts, want)
}
}
func TestPublishAtAndScheduled(t *testing.T) {
future := mustParse(t, "+++\npublish_at = 2999-01-01\n+++\nx\n")
if !future.Scheduled() {
t.Fatal("future publish_at should be scheduled")
}
past := mustParse(t, "+++\npublish_at = 2000-01-01\n+++\nx\n")
if past.Scheduled() {
t.Fatal("past publish_at should not be scheduled")
}
none := mustParse(t, "+++\ntitle = \"x\"\n+++\nx\n")
if none.Scheduled() {
t.Fatal("missing publish_at should not be scheduled")
}
if _, ok := none.DueAt(); ok {
t.Fatal("missing publish_at returned a date")
}
}
func TestPublishAtStringForms(t *testing.T) {
p := mustParse(t, "+++\npublish_at = \"2026-02-03\"\n+++\nx\n")
d, ok := p.DueAt()
if !ok || d.Format("2006-01-02") != "2026-02-03" {
t.Fatalf("string publish_at = %v, %v", d, ok)
}
}
func TestFlagsStrict(t *testing.T) {
p := mustParse(t, "+++\ndraft = \"true\"\n+++\nx\n")
if p.Draft() {
t.Fatal("string draft flag must not count as draft")
}
p2 := mustParse(t, "+++\ndraft = true\n+++\nx\n")
if !p2.Draft() {
t.Fatal("boolean draft flag should count")
}
}
func TestReadingTime(t *testing.T) {
short := New(nil, "one two three")
if got := short.ReadingTime(); got != 1 {
t.Fatalf("ReadingTime = %d, want 1", got)
}
body := strings.Repeat("word ", 450)
long := New(nil, body)
if got := long.ReadingTime(); got != 3 {
t.Fatalf("ReadingTime = %d, want 3", got)
}
empty := New(nil, "")
if got := empty.ReadingTime(); got != 1 {
t.Fatalf("empty ReadingTime = %d, want 1", got)
}
}
func TestExcerptPrefersMetadata(t *testing.T) {
p := mustParse(t, "+++\nexcerpt = \"Vlastní perex\"\n+++\nbody\n")
if p.Excerpt() != "Vlastní perex" {
t.Fatalf("Excerpt = %q", p.Excerpt())
}
}
func TestDerivedExcerpt(t *testing.T) {
p := New(nil, "# Head\n\nFirst paragraph with **markdown** and <b>html</b>.\n\nSecond.")
ex := p.Excerpt()
if strings.Contains(ex, "*") || strings.Contains(ex, "<") || strings.HasPrefix(ex, "#") {
t.Fatalf("derived excerpt not cleaned: %q", ex)
}
if !strings.Contains(ex, "First paragraph") {
t.Fatalf("derived excerpt wrong: %q", ex)
}
}
func TestDerivedExcerptTruncates(t *testing.T) {
p := New(nil, strings.Repeat("x", 500))
ex := p.Excerpt()
if !strings.HasSuffix(ex, "…") {
t.Fatalf("excerpt not truncated: %q", ex)
}
if len([]rune(ex)) != 201 {
t.Fatalf("excerpt length = %d, want 201", len([]rune(ex)))
}
}
func TestHTMLAndTOCCached(t *testing.T) {
p := mustParse(t, "+++\ntitle = \"x\"\n+++\n# H\n\nText\n")
h1, err := p.HTML()
if err != nil {
t.Fatalf("HTML: %v", err)
}
h2, err := p.HTML()
if err != nil || h1 != h2 {
t.Fatalf("HTML not cached: %q vs %q", h1, h2)
}
toc, err := p.TOC()
if err != nil {
t.Fatalf("TOC: %v", err)
}
if !strings.Contains(toc, `href="#h"`) {
t.Fatalf("toc = %q", toc)
}
}
func TestToFileRoundTrip(t *testing.T) {
p := mustParse(t, sample)
out, err := p.ToFile()
if err != nil {
t.Fatalf("ToFile: %v", err)
}
p2 := mustParse(t, out)
if p2.Title() != p.Title() || p2.Slug() != p.Slug() || p2.Body != p.Body {
t.Fatal("round-trip lost data")
}
}
func TestCivilDateVariants(t *testing.T) {
meta := frontmatterMeta(map[string]any{
"date": interpres.LocalDate{Time: time.Date(2026, 3, 4, 0, 0, 0, 0, time.UTC)},
})
if got := New(meta, "").DateString(); got != "2026-03-04" {
t.Fatalf("LocalDate DateString = %q", got)
}
meta = frontmatterMeta(map[string]any{"date": "2026-03-05"})
if got := New(meta, "").DateString(); got != "2026-03-05" {
t.Fatalf("string DateString = %q", got)
}
meta = frontmatterMeta(map[string]any{"date": 42})
if got := New(meta, "").DateString(); got != "42" {
t.Fatalf("int DateString = %q", got)
}
}
func TestFediverseCreatorValidation(t *testing.T) {
if !fediverse.Valid("@user@host.social") {
t.Fatal("valid handle rejected")
}
for _, bad := range []string{"user@host", "@user@", "@@host", "@us er@host", ""} {
if fediverse.Valid(bad) {
t.Fatalf("invalid handle accepted: %q", bad)
}
}
}
func TestDOIORCIDReservedAndReadable(t *testing.T) {
p := mustParse(t, "+++\nslug = \"paper\"\ndoi = \"10.1000/xyz\"\norcid = \"0000-0002-1825-0097\"\nmethod = \"gravimetric\"\n+++\nx\n")
if p.DOI() != "10.1000/xyz" {
t.Fatalf("DOI = %q", p.DOI())
}
if p.ORCID() != "0000-0002-1825-0097" {
t.Fatalf("ORCID = %q", p.ORCID())
}
// The identifiers are reserved: only the author's own key reaches
// the fields object.
fields := p.CustomFields()
if _, present := fields["doi"]; present {
t.Fatal("doi leaked into custom fields")
}
if _, present := fields["orcid"]; present {
t.Fatal("orcid leaked into custom fields")
}
if fields["method"] != "gravimetric" {
t.Fatalf("custom field lost: %v", fields)
}
}
func TestSeriesOrderCoercions(t *testing.T) {
p := frontmatterPost(map[string]any{"series_order": "7"})
if order, ok := p.SeriesOrder(); !ok || order != 7 {
t.Fatalf("string series_order = %d, %v", order, ok)
}
p = frontmatterPost(map[string]any{"series_order": 3.9})
if order, ok := p.SeriesOrder(); !ok || order != 3 {
t.Fatalf("float series_order = %d, %v", order, ok)
}
p = frontmatterPost(map[string]any{"series_order": "x"})
if _, ok := p.SeriesOrder(); ok {
t.Fatal("garbage series_order accepted")
}
}
func TestUnparseablePublishAtWithholdsThePost(t *testing.T) {
// A typo in publish_at must not publish the post early: it is
// withheld, and validate reports it.
p := mustParse(t, "+++\npublish_at = \"18/09/2026\"\n+++\nx\n")
if p.Published() {
t.Fatal("a post with an unreadable publish_at was published")
}
if !p.Scheduled() {
t.Fatal("a post with an unreadable publish_at was not withheld")
}
if _, ok := p.DueAt(); ok {
t.Fatal("an unreadable publish_at produced a date")
}
}
func TestEmptyPublishAtIsPublished(t *testing.T) {
p := mustParse(t, "+++\npublish_at = \"\"\n+++\nx\n")
if !p.Published() {
t.Fatal("an empty publish_at withheld the post")
}
}
func TestStatusPrecedence(t *testing.T) {
draft := mustParse(t, "+++\ndraft = true\npublish_at = 2999-01-01\n+++\nx\n")
if draft.Status() != StatusDraft {
t.Fatal("a draft outranks a scheduled post")
}
if draft.Published() {
t.Fatal("a draft was published")
}
}
func TestCloneIsIndependent(t *testing.T) {
original := mustParse(t, "+++\ntitle = \"A\"\ntags = [\"go\", \"blog\"]\n[extra]\nkey = \"value\"\n+++\nbody\n")
clone := original.Clone()
clone.Metadata.Set("title", "B")
clone.Metadata.Set("tags", []string{"other"})
if table, ok := clone.Metadata.Get("extra"); ok {
if m, isMap := table.(map[string]any); isMap {
m["key"] = "changed"
}
}
title, _ := original.Metadata.Get("title")
if title != "A" {
t.Fatalf("the clone wrote through to the original: title = %v", title)
}
if got := original.Tags(); len(got) != 2 || got[0] != "go" {
t.Fatalf("the clone shares the tag list: %v", got)
}
if table, ok := original.Metadata.Get("extra"); ok {
if m, isMap := table.(map[string]any); isMap && m["key"] != "value" {
t.Fatalf("the clone shares the nested table: %v", m)
}
}
if clone.Path != original.Path || clone.Body != original.Body {
t.Fatal("the clone lost the body or the path")
}
}
func TestStoredExcerptIsNotDerived(t *testing.T) {
withExcerpt := mustParse(t, "+++\nexcerpt = \"Hand written\"\n+++\nFirst paragraph.\n")
if withExcerpt.StoredExcerpt() != "Hand written" {
t.Fatalf("StoredExcerpt = %q", withExcerpt.StoredExcerpt())
}
derived := mustParse(t, "+++\ntitle = \"x\"\n+++\nFirst paragraph.\n")
if derived.StoredExcerpt() != "" {
t.Fatalf("StoredExcerpt = %q, want empty before the editor fills it", derived.StoredExcerpt())
}
if derived.Excerpt() == "" {
t.Fatal("Excerpt should still derive from the body")
}
}
func TestPlainTextAndTruncate(t *testing.T) {
if got := PlainText("<p>Hello <b>world</b></p>", 100); got != "Hello world" {
t.Fatalf("PlainText = %q", got)
}
if got := PlainText("<p>one two</p>", 100); got != "one two" {
t.Fatalf("PlainText did not collapse whitespace: %q", got)
}
if got := Truncate("abcdefghij", 4); got != "abcd…" {
t.Fatalf("Truncate = %q", got)
}
if got := Truncate("abc", 4); got != "abc" {
t.Fatalf("Truncate changed a short value: %q", got)
}
}