kitindexdemo_test.gno
8.01 Kb · 257 lines
1package kitindexdemo
2
3import (
4 "strconv"
5 "strings"
6 "testing"
7
8 "gno.land/p/moul/kit/index/v0"
9 "gno.land/p/moul/kit/store/v0"
10 "gno.land/p/nt/testutils/v0"
11 "gno.land/p/nt/uassert/v0"
12 "gno.land/p/nt/urequire/v0"
13)
14
15var (
16 alice = testutils.TestAddress("alice")
17 bob = testutils.TestAddress("bob")
18)
19
20// reset puts the realm back to its init() state.
21//
22// Realm globals persist for the whole test binary and examples run after every
23// Test, so without this an ExampleRender's pinned output depends silently on
24// which tests ran before it. Assigning the globals directly is allowed here:
25// same package, and it crosses nothing.
26func reset() {
27 notes = store.Named("note")
28 byTag = index.Index{}
29 byAuthor = index.Index{}
30}
31
32func TestPostIndexesInBothDirections(cur realm, t *testing.T) {
33 reset()
34 testing.SetRealm(testing.NewUserRealm(alice))
35 id := Post(cross(cur), "gno", "first")
36
37 uassert.Equal(t, 1, notes.Len())
38 uassert.Equal(t, 1, byTag.Count("gno"))
39 uassert.Equal(t, 1, byAuthor.Count(alice.String()))
40
41 got := byTag.Lookup("gno")
42 urequire.Equal(t, 1, len(got))
43 uassert.Equal(t, store.ID(id).String(), got[0].String())
44}
45
46func TestTagIsNormalisedAtWriteTime(cur realm, t *testing.T) {
47 reset()
48 testing.SetRealm(testing.NewUserRealm(alice))
49 Post(cross(cur), " GNO ", "mixed case and spaces")
50
51 uassert.Equal(t, 1, byTag.Count("gno"))
52 uassert.Equal(t, 1, byTag.Keys())
53}
54
55// The tag is an index key AND a path segment, so it is validated rather than
56// escaped: a tag carrying a pipe or a slash breaks the table and the URL, not
57// only the sentence.
58func TestTagRefusals(cur realm, t *testing.T) {
59 reset()
60 testing.SetRealm(testing.NewUserRealm(alice))
61
62 for _, tc := range []struct {
63 name string
64 tag string
65 want string
66 }{
67 {"empty", "", "a tag is 1 to 24 characters"},
68 {"blank", " ", "a tag is 1 to 24 characters"},
69 {"too long", "aaaaaaaaaaaaaaaaaaaaaaaaa", "a tag is 1 to 24 characters"},
70 {"a pipe", "a|b", "a tag is [a-z0-9-]"},
71 {"a slash", "a/b", "a tag is [a-z0-9-]"},
72 {"markdown", "a]b", "a tag is [a-z0-9-]"},
73 {"a space inside", "a b", "a tag is [a-z0-9-]"},
74 } {
75 t.Run(tc.name, func(t *testing.T) {
76 uassert.AbortsContains(t, cur, tc.want, func() {
77 Post(cross(cur), tc.tag, "body")
78 })
79 })
80 }
81 uassert.Equal(t, 0, notes.Len())
82}
83
84func TestBodyRefusals(cur realm, t *testing.T) {
85 reset()
86 testing.SetRealm(testing.NewUserRealm(alice))
87
88 uassert.AbortsContains(t, cur, "the body is empty", func() {
89 Post(cross(cur), "gno", " ")
90 })
91
92 long := ""
93 for i := 0; i <= MaxBody; i++ {
94 long += "x"
95 }
96 uassert.AbortsContains(t, cur, "the limit is 280", func() {
97 Post(cross(cur), "gno", long)
98 })
99 uassert.Equal(t, 0, notes.Len())
100}
101
102func TestOnlyTheAuthorMayRetract(cur realm, t *testing.T) {
103 reset()
104 testing.SetRealm(testing.NewUserRealm(alice))
105 id := Post(cross(cur), "gno", "alice's note")
106
107 testing.SetRealm(testing.NewUserRealm(bob))
108 uassert.AbortsContains(t, cur, "is not yours", func() {
109 Retract(cross(cur), id)
110 })
111 uassert.Equal(t, 1, notes.Len())
112
113 testing.SetRealm(testing.NewUserRealm(alice))
114 Retract(cross(cur), id)
115 uassert.Equal(t, 0, notes.Len())
116}
117
118// The store and both indexes have to come apart together, or a later Render
119// walks ids the store no longer holds.
120func TestRetractUnwindsEveryContainer(cur realm, t *testing.T) {
121 reset()
122 testing.SetRealm(testing.NewUserRealm(alice))
123 id := Post(cross(cur), "gno", "one")
124 Post(cross(cur), "gno", "two")
125
126 Retract(cross(cur), id)
127
128 uassert.Equal(t, 1, notes.Len())
129 uassert.Equal(t, 1, byTag.Count("gno"))
130 uassert.Equal(t, 1, byAuthor.Count(alice.String()))
131}
132
133func TestRetractRefusals(cur realm, t *testing.T) {
134 reset()
135 testing.SetRealm(testing.NewUserRealm(alice))
136
137 uassert.AbortsContains(t, cur, "not found", func() { Retract(cross(cur), 99) })
138 uassert.AbortsContains(t, cur, "not an id", func() { Retract(cross(cur), 0) })
139 uassert.AbortsContains(t, cur, "not an id", func() { Retract(cross(cur), -1) })
140}
141
142// A body is a caller's markdown and must not be able to write the page around
143// it. ui.Cell is what stops a pipe from opening a column.
144func TestARenderedBodyCannotBreakTheTable(cur realm, t *testing.T) {
145 reset()
146 testing.SetRealm(testing.NewUserRealm(alice))
147 Post(cross(cur), "gno", "a | b [x](http://evil)")
148
149 got := Render("tag/gno")
150 uassert.False(t, strings.Contains(got, "| a | b ["),
151 "the raw body must not reach the table")
152 uassert.True(t, strings.Contains(got, "\\|"),
153 "the pipe must be escaped")
154}
155
156func TestRenderOfAnUnknownPage(t *testing.T) {
157 reset()
158 uassert.True(t, strings.Contains(Render("nope"), "Not found"))
159 uassert.True(t, strings.Contains(Render("tag"), "No tag given."))
160 uassert.True(t, strings.Contains(Render("author"), "No author given."))
161}
162
163// MaxBody bounds one write. These bound the bill, which is the thing the realm
164// actually pays.
165func TestPerAuthorCap(cur realm, t *testing.T) {
166 reset()
167 testing.SetRealm(testing.NewUserRealm(alice))
168 for i := 0; i < MaxPerAuthor; i++ {
169 Post(cross(cur), "gno", "note "+strconv.Itoa(i))
170 }
171 uassert.AbortsContains(t, cur, "the limit is 20", func() {
172 Post(cross(cur), "gno", "one too many")
173 })
174
175 // Another address is unaffected, which is the whole reason the cap is
176 // per author and not only global.
177 testing.SetRealm(testing.NewUserRealm(bob))
178 Post(cross(cur), "gno", "bob still fits")
179 uassert.Equal(t, MaxPerAuthor+1, notes.Len())
180}
181
182// Retracting frees a slot, so the cap is not a one-way door.
183func TestRetractingFreesASlot(cur realm, t *testing.T) {
184 reset()
185 testing.SetRealm(testing.NewUserRealm(alice))
186 first := Post(cross(cur), "gno", "note 0")
187 for i := 1; i < MaxPerAuthor; i++ {
188 Post(cross(cur), "gno", "note "+strconv.Itoa(i))
189 }
190 uassert.AbortsContains(t, cur, "the limit is 20", func() {
191 Post(cross(cur), "gno", "blocked")
192 })
193
194 Retract(cross(cur), first)
195 Post(cross(cur), "gno", "now it fits")
196 uassert.Equal(t, MaxPerAuthor, notes.Len())
197}
198
199// The page number is user input on the Render path. One outside 1..Pages is
200// clamped into it, so it never renders an empty board under a footer that
201// says "page -1 of 2".
202func TestRenderClampsThePageNumber(cur realm, t *testing.T) {
203 reset()
204 testing.SetRealm(testing.NewUserRealm(alice))
205 for i := 0; i < PageSize; i++ {
206 Post(cross(cur), "t"+strconv.Itoa(i), "x")
207 }
208 testing.SetRealm(testing.NewUserRealm(bob))
209 Post(cross(cur), "t-last", "x")
210 urequire.Equal(t, 2, byTag.Pages(PageSize))
211
212 first, last := Render("?page=1"), Render("?page=2")
213 uassert.Equal(t, first, Render("?page=-1"))
214 uassert.Equal(t, first, Render("?page=0"))
215 uassert.Equal(t, last, Render("?page=999"))
216 uassert.True(t, strings.Contains(last, "page 2 of 2"))
217}
218
219// Links are absolute. gnoweb serves this realm at .../kitindexdemo/v0, so a
220// "./kitindexdemo:tag/x" link resolves to .../kitindexdemo/kitindexdemo:tag/x,
221// a package that does not exist.
222func TestRenderLinksAreAbsolute(cur realm, t *testing.T) {
223 reset()
224 testing.SetRealm(testing.NewUserRealm(alice))
225 for i := 0; i <= PageSize; i++ {
226 if i == MaxPerAuthor {
227 testing.SetRealm(testing.NewUserRealm(bob))
228 }
229 Post(cross(cur), "t"+strconv.Itoa(i), "x")
230 }
231 out := Render("")
232 uassert.True(t, strings.Contains(out, "](/r/moul/x/kitindexdemo/v0:tag/t0)"), out)
233 uassert.True(t, strings.Contains(out, "](/r/moul/x/kitindexdemo/v0:?page=2)"), out)
234 uassert.False(t, strings.Contains(out, "](./"), out)
235}
236
237// realmURL must follow the module line, or a version bump ships dead links.
238func TestRealmURLMatchesTheModule(cur realm, t *testing.T) {
239 uassert.Equal(t, "gno.land"+realmURL, cur.PkgPath())
240}
241
242// The global cap is what bounds the realm's whole storage bill, so its
243// refusal is pinned, and pinned as refusing BEFORE any write: the store and
244// both indexes are exactly as full as they were.
245func TestTheBoardIsFull(cur realm, t *testing.T) {
246 reset()
247 for i := 0; i < MaxNotes; i++ {
248 notes.Add(¬e{Author: bob, Tag: "gno", Body: "x"})
249 }
250 testing.SetRealm(testing.NewUserRealm(alice))
251 uassert.AbortsContains(t, cur, "the board is full", func() {
252 Post(cross(cur), "gno", "one too many")
253 })
254 uassert.Equal(t, MaxNotes, notes.Len())
255 uassert.Equal(t, 0, byTag.Len())
256 uassert.Equal(t, 0, byAuthor.Count(alice.String()))
257}