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}