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

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(&note{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}