Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

blog_test.gno

5.96 Kb · 190 lines
  1package blog
  2
  3import (
  4	"strings"
  5	"testing"
  6
  7	"gno.land/p/nt/avl/v0"
  8	"gno.land/p/nt/uassert/v0"
  9)
 10
 11// reset empties every package-level variable. ExampleRender runs after every
 12// Test in this package and would otherwise render whatever they left behind,
 13// so both the tests and the example start from here.
 14func reset() {
 15	posts = avl.NewTree()
 16	order = avl.NewTree()
 17	intro = ""
 18	rev = 0
 19}
 20
 21// seed writes a post without going through Set, so a caller needs no
 22// `cur realm` and the example stays a plain function.
 23func seed(slug, title, date, tags, body string) {
 24	rev++
 25	p := &post{
 26		slug:  slug,
 27		title: title,
 28		date:  date,
 29		tags:  joinTags(splitTags(tags)),
 30		body:  body,
 31		rev:   rev,
 32	}
 33	posts.Set(slug, p)
 34	order.Set(orderKey(date, slug), p)
 35}
 36
 37func TestValidSlug(t *testing.T) {
 38	for _, tc := range []struct {
 39		slug string
 40		want bool
 41	}{
 42		{"mygnoscan", true},
 43		{"a", true},
 44		{"hello-world.2", true},
 45		{"under_score", true},
 46		{"", false},
 47		{"Caps", false},
 48		{"has space", false},
 49		{"colon:here", false},
 50		{"slash/here", false},
 51		{"!intro", false},
 52		{strings.Repeat("a", maxSlugLen), true},
 53		{strings.Repeat("a", maxSlugLen+1), false},
 54	} {
 55		uassert.Equal(t, tc.want, validSlug(tc.slug), "validSlug("+tc.slug+")")
 56	}
 57}
 58
 59func TestValidDate(t *testing.T) {
 60	for _, tc := range []struct {
 61		date string
 62		want bool
 63	}{
 64		{"2026-09-23", true},
 65		{"0000-00-00", true}, // shape only: ordering is all this field is for
 66		{"2026-9-23", false},
 67		{"2026/09/23", false},
 68		{"2026-09-233", false},
 69		{"", false},
 70	} {
 71		uassert.Equal(t, tc.want, validDate(tc.date), "validDate("+tc.date+")")
 72	}
 73}
 74
 75func TestSplitTagsNormalizes(t *testing.T) {
 76	for _, tc := range []struct{ in, want string }{
 77		{"", ""},
 78		{"gno", "gno"},
 79		{" Gno , TOOLING ", "gno,tooling"},
 80		{"gno,gno,gno", "gno"},
 81		{"gno,,tooling", "gno,tooling"},
 82	} {
 83		uassert.Equal(t, tc.want, joinTags(splitTags(tc.in)), "splitTags("+tc.in+")")
 84	}
 85}
 86
 87// A post published earlier must render below one published later, which is
 88// the only thing the second tree exists to guarantee.
 89func TestIndexIsNewestFirst(t *testing.T) {
 90	reset()
 91	seed("older", "Older", "2026-01-01", "", "First.")
 92	seed("newer", "Newer", "2026-09-23", "", "Second.")
 93
 94	out := Render("")
 95	iNew := strings.Index(out, "Newer")
 96	iOld := strings.Index(out, "Older")
 97	uassert.True(t, iNew >= 0 && iOld >= 0, "both posts render")
 98	uassert.True(t, iNew < iOld, "the newer post renders first")
 99}
100
101// Re-dating a post has to move it, not leave a ghost entry behind at the old
102// key. Set is the only path that can do this, so it is checked through Set.
103func TestRedatingMovesTheIndexEntry(t *testing.T) {
104	reset()
105	seed("a", "A", "2026-01-01", "", "Body A.")
106	uassert.Equal(t, 1, order.Size(), "one order entry")
107
108	// Emulate Set's re-key without needing a realm: remove then re-add.
109	p := posts.Get("a").(*post)
110	order.Remove(orderKey(p.date, "a"))
111	p.date = "2026-12-31"
112	order.Set(orderKey(p.date, "a"), p)
113
114	uassert.Equal(t, 1, order.Size(), "still one order entry, not two")
115	uassert.True(t, strings.Contains(Render(""), "2026-12-31"), "the new date renders")
116}
117
118func TestRenderPostAndNotFound(t *testing.T) {
119	reset()
120	seed("mygnoscan", "Sharing mygnoscan", "2026-09-23", "mygnoscan,tooling", "## Why\n\nBecause it is useful.")
121
122	out := Render("mygnoscan")
123	uassert.True(t, strings.Contains(out, "# Sharing mygnoscan"), "title renders as the H1")
124	uassert.True(t, strings.Contains(out, "Because it is useful."), "body renders verbatim")
125	uassert.True(t, strings.Contains(out, "## Why"), "markdown in the body is not escaped")
126
127	missing := Render("nope")
128	uassert.True(t, strings.Contains(missing, "Not found"), "an unknown slug is a 404, not the index")
129}
130
131func TestRenderTagFiltersAndReportsEmpty(t *testing.T) {
132	reset()
133	seed("a", "A", "2026-01-01", "tooling", "Body A.")
134	seed("b", "B", "2026-01-02", "chain", "Body B.")
135
136	out := Render("t/tooling")
137	uassert.True(t, strings.Contains(out, "A"), "the tagged post is listed")
138	uassert.False(t, strings.Contains(out, "](/r/moul/blog:b)"), "the untagged post is not")
139
140	empty := Render("t/nothing")
141	uassert.True(t, strings.Contains(empty, "No post carries this tag."), "an unused tag says so")
142}
143
144// The manifest is the whole diff protocol. A change to any of the four fields
145// the record covers has to change the hash, or gnoblog reports a stale post as
146// up to date and the fix never gets pushed.
147func TestManifestHashCoversEveryField(t *testing.T) {
148	base := record("T", "2026-01-01", "a", "body")
149	for _, tc := range []struct {
150		name string
151		got  string
152	}{
153		{"title", record("T2", "2026-01-01", "a", "body")},
154		{"date", record("T", "2026-01-02", "a", "body")},
155		{"tags", record("T", "2026-01-01", "b", "body")},
156		{"body", record("T", "2026-01-01", "a", "body2")},
157	} {
158		uassert.NotEqual(t, hashOf(base), hashOf(tc.got), "changing the "+tc.name+" changes the hash")
159	}
160}
161
162func TestManifestShape(t *testing.T) {
163	reset()
164	seed("a", "A", "2026-01-01", "tooling", "Body A.")
165
166	lines := strings.Split(strings.TrimRight(Manifest(), "\n"), "\n")
167	uassert.Equal(t, 2, len(lines), "one intro row plus one post row")
168	uassert.True(t, strings.HasPrefix(lines[0], introKey+"\t"), "the intro row comes first")
169
170	f := strings.Split(lines[1], "\t")
171	uassert.Equal(t, 5, len(f), "five tab-separated fields")
172	uassert.Equal(t, "a", f[0], "slug")
173	uassert.Equal(t, "2026-01-01", f[2], "date")
174	uassert.Equal(t, "7", f[3], "body length")
175	uassert.Equal(t, hashOf(record("A", "2026-01-01", "tooling", "Body A.")), f[4], "record hash")
176}
177
178func TestFirstLineSkipsHeadings(t *testing.T) {
179	uassert.Equal(t, "The prose.", firstLine("# Title\n\n## Sub\n\nThe prose.\n"))
180	uassert.Equal(t, "", firstLine("# Only a heading\n"))
181}
182
183func TestIntroDefaultsAndOverrides(t *testing.T) {
184	reset()
185	uassert.True(t, strings.Contains(Render(""), "moul's blog"), "the built-in header shows before SetIntro")
186
187	intro = "# Something else"
188	uassert.True(t, strings.Contains(Render(""), "Something else"), "a set intro replaces it")
189	uassert.False(t, strings.Contains(Render(""), "moul's blog"), "and the default is gone")
190}