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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

blog_test.gno

7.68 Kb · 231 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/nym-moultest123/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}
191
192// A markdown link in the first line of a post reached the index as its own
193// source, backslash-escaped, on 2026-09-23. The excerpt is plain text, so the
194// link has to be flattened before ui.Excerpt escapes what is left.
195func TestFlattenLinks(t *testing.T) {
196	for _, tc := range []struct{ in, want string }{
197		{"plain text", "plain text"},
198		{"I built [mygnoscan](https://x.example) in April", "I built mygnoscan in April"},
199		{"[a](1) and [b](2)", "a and b"},
200		{"", ""},
201		// Unbalanced: left alone rather than half-parsed.
202		{"an [unclosed link", "an [unclosed link"},
203		{"brackets [only] here", "brackets [only] here"},
204		{"[text](no-close", "[text](no-close"},
205	} {
206		uassert.Equal(t, tc.want, flattenLinks(tc.in), "flattenLinks("+tc.in+")")
207	}
208}
209
210// The index excerpt must read as a sentence, not as markdown source.
211func TestExcerptIsProseNotSource(t *testing.T) {
212	reset()
213	seed("a", "A", "2026-09-23", "", "I built [mygnoscan](https://mygnoscan.example) in April.")
214
215	out := Render("")
216	uassert.False(t, strings.Contains(out, "\\["), "no escaped bracket survives into the index")
217	uassert.True(t, strings.Contains(out, "I built mygnoscan in April"), "the excerpt reads as prose")
218}
219
220// The back link is the realm root, which carries no ':'. linkPrefix with an
221// empty path would produce "/r/nym-moultest123/blog:", which shipped on 2026-09-23.
222func TestBackLinkHasNoTrailingColon(t *testing.T) {
223	reset()
224	seed("a", "A", "2026-09-23", "tag", "Body.")
225
226	for _, view := range []string{"a", "t/tag", "nosuchpost"} {
227		out := Render(view)
228		uassert.True(t, strings.Contains(out, "]("+indexLink+")"), view+" links back to the index")
229		uassert.False(t, strings.Contains(out, "]("+indexLink+":)"), view+" must not emit a trailing colon")
230	}
231}