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

threads_test.gno

6.36 Kb · 208 lines
  1package threads
  2
  3import (
  4	"strings"
  5	"testing"
  6
  7	"gno.land/p/nt/testutils/v0"
  8	"gno.land/p/nt/uassert/v0"
  9	"gno.land/p/nt/urequire/v0"
 10)
 11
 12var (
 13	alice = testutils.TestAddress("alice")
 14	bob   = testutils.TestAddress("bob")
 15	carol = testutils.TestAddress("carol")
 16
 17	home = "gno.land/r/moul/home"
 18)
 19
 20func TestValidPage(t *testing.T) {
 21	tests := []struct {
 22		page string
 23		want bool
 24	}{
 25		{"gno.land/r/moul/home", true},
 26		{"gno.land/r/gov/dao:proposal/42", true},
 27		{"a/b", true},
 28		{"", false},
 29		{"nopath", false},
 30		{"/leading", false},
 31		{"trailing/", false},
 32		{"double//slash", false},
 33		{"gno.land/r/Moul/home", false},
 34		{"gno.land/r/moul/home?x=1", false},
 35		{"gno.land/r/moul/" + strings.Repeat("x", MaxPageLen), false},
 36	}
 37	for _, tt := range tests {
 38		uassert.Equal(t, tt.want, ValidPage(tt.page), tt.page)
 39	}
 40}
 41
 42func TestValidBody(t *testing.T) {
 43	tests := []struct {
 44		name string
 45		body string
 46		want bool
 47	}{
 48		{"plain", "hello", true},
 49		{"newlines and tabs are content", "a\nb\tc", true},
 50		{"markdown is allowed and escaped later", "[x](y) | z", true},
 51		{"empty", "", false},
 52		{"blank", "   \n ", false},
 53		{"too long", strings.Repeat("x", MaxBodyLen+1), false},
 54		{"control character", "a\x01b", false},
 55		{"delete character", "a\x7fb", false},
 56	}
 57	for _, tt := range tests {
 58		uassert.Equal(t, tt.want, ValidBody(tt.body), tt.name)
 59	}
 60}
 61
 62func TestPostRejectsWhatItCannotStore(t *testing.T) {
 63	b := NewBoard()
 64
 65	_, err := b.Post("nopath", alice, "hi", 100)
 66	uassert.ErrorIs(t, err, ErrBadPage)
 67
 68	_, err = b.Post(home, alice, "", 100)
 69	uassert.ErrorIs(t, err, ErrBadBody)
 70
 71	uassert.Equal(t, 0, b.Len())
 72	uassert.Equal(t, 0, b.Pages())
 73}
 74
 75func TestReplyCountsPeopleAndNotMessages(t *testing.T) {
 76	b := NewBoard()
 77	id, err := b.Post(home, alice, "root", 100)
 78	urequire.NoError(t, err)
 79
 80	// A new address is the mint signal, exactly once.
 81	isNew, err := b.Reply(id, bob, "first", 101)
 82	urequire.NoError(t, err)
 83	uassert.True(t, isNew, "bob is a new replier")
 84
 85	isNew, err = b.Reply(id, bob, "again", 102)
 86	urequire.NoError(t, err)
 87	uassert.False(t, isNew, "bob replying twice is not two people")
 88
 89	// The author replying to their own thread never counts.
 90	isNew, err = b.Reply(id, alice, "mine", 103)
 91	urequire.NoError(t, err)
 92	uassert.False(t, isNew, "the author is not an audience")
 93
 94	isNew, err = b.Reply(id, carol, "hello", 104)
 95	urequire.NoError(t, err)
 96	uassert.True(t, isNew, "carol is a new replier")
 97
 98	th, ok := b.Get(id)
 99	urequire.True(t, ok, "thread exists")
100	uassert.Equal(t, 4, len(th.Replies))
101	uassert.Equal(t, 2, th.Repliers())
102	uassert.True(t, th.HasReplied(bob))
103	uassert.False(t, th.HasReplied(alice), "the author is never in the replier set")
104}
105
106func TestReplyToNothing(t *testing.T) {
107	b := NewBoard()
108	_, err := b.Reply(42, bob, "hi", 100)
109	uassert.ErrorIs(t, err, ErrNoThread)
110}
111
112func TestPinOnlyEverMovesForward(t *testing.T) {
113	b := NewBoard()
114	id, _ := b.Post(home, alice, "root", 100)
115
116	urequire.NoError(t, b.Pin(id, 1000))
117	th, _ := b.Get(id)
118	uassert.True(t, th.Pinned(999))
119	uassert.False(t, th.Pinned(1000), "the pin is exclusive at its own height")
120
121	// A cheaper pin cannot cut an expensive one short.
122	uassert.ErrorIs(t, b.Pin(id, 500), ErrPinIsPast)
123	uassert.ErrorIs(t, b.Pin(id, 1000), ErrPinIsPast)
124	uassert.Equal(t, int64(1000), th.PinnedUntil)
125
126	urequire.NoError(t, b.Pin(id, 1200))
127	uassert.Equal(t, int64(1200), th.PinnedUntil)
128}
129
130func TestListIsPinnedThenNewest(t *testing.T) {
131	b := NewBoard()
132	first, _ := b.Post(home, alice, "first", 100)
133	second, _ := b.Post(home, bob, "second", 101)
134	third, _ := b.Post(home, carol, "third", 102)
135
136	// Newest first, nothing pinned.
137	got := b.List(home, 200, 0)
138	urequire.Equal(t, 3, len(got))
139	uassert.Equal(t, uint64(third), uint64(got[0].ID))
140	uassert.Equal(t, uint64(second), uint64(got[1].ID))
141	uassert.Equal(t, uint64(first), uint64(got[2].ID))
142
143	// Pinning the oldest moves it to the top and leaves the rest in order.
144	urequire.NoError(t, b.Pin(first, 500))
145	got = b.List(home, 200, 0)
146	uassert.Equal(t, uint64(first), uint64(got[0].ID))
147	uassert.Equal(t, uint64(third), uint64(got[1].ID))
148	uassert.Equal(t, uint64(second), uint64(got[2].ID))
149
150	// The pin expires on its own, with nothing to sweep.
151	got = b.List(home, 500, 0)
152	uassert.Equal(t, uint64(third), uint64(got[0].ID))
153
154	// limit truncates after the partition, so a pin is never cut off.
155	got = b.List(home, 200, 1)
156	urequire.Equal(t, 1, len(got))
157	uassert.Equal(t, uint64(first), uint64(got[0].ID))
158}
159
160func TestPagesAreIndependent(t *testing.T) {
161	b := NewBoard()
162	b.Post(home, alice, "on home", 100)
163	b.Post("gno.land/r/moul/blog", bob, "on the blog", 101)
164
165	uassert.Equal(t, 2, b.Len())
166	uassert.Equal(t, 2, b.Pages())
167	uassert.Equal(t, 1, b.PageLen(home))
168	uassert.Equal(t, 0, b.PageLen("gno.land/r/nobody/here"))
169	uassert.Equal(t, 0, len(b.List("gno.land/r/nobody/here", 100, 0)))
170}
171
172func TestRecentCrossesPages(t *testing.T) {
173	b := NewBoard()
174	b.Post(home, alice, "one", 100)
175	b.Post("gno.land/r/moul/blog", bob, "two", 101)
176	third, _ := b.Post(home, carol, "three", 102)
177
178	got := b.Recent(2)
179	urequire.Equal(t, 2, len(got))
180	uassert.Equal(t, uint64(third), uint64(got[0].ID), "newest first")
181}
182
183func TestBlockEscapesTheBodyItShows(t *testing.T) {
184	b := NewBoard()
185	id, _ := b.Post(home, alice, "a | pipe and [a link](x)", 100)
186
187	out := Block("gno.land/r/moul/x/social/threads/v0", home, b.List(home, 200, 10), 1, 200)
188
189	// The assertion has to be on what IS there, not on what is not: an
190	// escaped cell contains the backslash form, and a row that silently lost
191	// the body entirely would satisfy any "does not contain" check.
192	uassert.True(t, strings.Contains(out, `a \| pipe and \[a link\]\(x\)`),
193		"the body is cut and escaped once, pipe included: "+out)
194	uassert.False(t, strings.Contains(out, "| a | pipe"), "a raw pipe would open a column: "+out)
195	uassert.True(t, strings.Contains(out, ThreadURL("gno.land/r/moul/x/social/threads/v0", id)))
196	uassert.True(t, strings.Contains(out, "func=Post"), "the block offers the transaction")
197}
198
199func TestBlockOnAnEmptyPageSaysSo(t *testing.T) {
200	out := Block("gno.land/r/moul/x/social/threads/v0", home, nil, 0, 200)
201	uassert.True(t, strings.Contains(out, "No discussion yet."), out)
202}
203
204func TestURLsStripTheChainDomain(t *testing.T) {
205	uassert.Equal(t, "/r/moul/x/social/threads/v0", RealmURL("gno.land/r/moul/x/social/threads/v0"))
206	uassert.Equal(t, "/r/a/b:thread/7", ThreadURL("gno.land/r/a/b", 7))
207	uassert.Equal(t, "/r/a/b:page/gno.land/r/moul/home", PageURL("gno.land/r/a/b", home))
208}