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.

kitindexdemo_test.gno

8.05 Kb · 257 lines
  1package kitindexdemo
  2
  3import (
  4	"strconv"
  5	"strings"
  6	"testing"
  7
  8	"gno.land/p/nym-moultest123/kit/index/v0"
  9	"gno.land/p/nym-moultest123/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/nym-moultest123/x/kitindexdemo/v0:tag/t0)"), out)
233	uassert.True(t, strings.Contains(out, "](/r/nym-moultest123/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}