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}