threads_test.gno
7.80 Kb · 217 lines
1package threads
2
3import (
4 "strconv"
5 "strings"
6 "testing"
7
8 sc "gno.land/p/moul/x/social/coin/v0"
9 th "gno.land/p/moul/x/social/threads/v0"
10 "gno.land/p/nt/testutils/v0"
11 "gno.land/p/nt/uassert/v0"
12 "gno.land/p/nt/urequire/v0"
13)
14
15const homePage = "gno.land/r/moul/home"
16
17var (
18 alice = testutils.TestAddress("alice")
19 bob = testutils.TestAddress("bob")
20 carol = testutils.TestAddress("carol")
21)
22
23// reset clears realm state. Realm globals live for the whole test binary, so
24// everything a later test reads, including the ledger, starts here.
25//
26// The ledger is rebuilt through a same-realm cross because grc20.NewToken
27// needs a code-realm frame, which a test's EOA-origin cur is not. It is NOT
28// re-registered with grc20reg: the registry is another realm and its entry
29// survives this one's state.
30func reset(cur realm) {
31 board = th.NewBoard()
32 func(cur realm) { // the name is forced: a realm argument must be called cur
33 points = sc.New("Thread Points", "THREAD", 0, 0, tokenPolicy, 0, cur)
34 }(cross(cur))
35}
36
37// earn gives who n THREAD the only way the realm allows: n distinct addresses
38// replying to a thread they wrote.
39func earn(cur realm, t *testing.T, who address, n int) int64 {
40 t.Helper()
41 testing.SetRealm(testing.NewUserRealm(who))
42 id := Post(cross(cur), homePage, "a thread by "+who.String())
43 for i := 0; i < n; i++ {
44 testing.SetRealm(testing.NewUserRealm(testutils.TestAddress("replier" + strconv.Itoa(i))))
45 Reply(cross(cur), id, "hello")
46 }
47 return id
48}
49
50func TestAnAuthorEarnsPeopleAndNotMessages(cur realm, t *testing.T) {
51 reset(cur)
52
53 testing.SetRealm(testing.NewUserRealm(alice))
54 id := Post(cross(cur), homePage, "first thread")
55 uassert.Equal(t, int64(0), BalanceOf(alice), "writing earns nothing on its own")
56
57 testing.SetRealm(testing.NewUserRealm(bob))
58 Reply(cross(cur), id, "nice")
59 uassert.Equal(t, int64(1), BalanceOf(alice))
60
61 testing.SetRealm(testing.NewUserRealm(bob))
62 Reply(cross(cur), id, "still me")
63 uassert.Equal(t, int64(1), BalanceOf(alice), "one person is one point, however much they say")
64
65 testing.SetRealm(testing.NewUserRealm(alice))
66 Reply(cross(cur), id, "thanks")
67 uassert.Equal(t, int64(1), BalanceOf(alice), "replying to yourself is not an audience")
68
69 testing.SetRealm(testing.NewUserRealm(carol))
70 Reply(cross(cur), id, "hi")
71 uassert.Equal(t, int64(2), BalanceOf(alice))
72
73 uassert.Equal(t, 2, Repliers(id))
74 uassert.Equal(t, int64(2), TotalSupply())
75 uassert.Equal(t, int64(0), BalanceOf(bob), "a replier earns nothing")
76}
77
78func TestBoostIsTheOnlyThingThatBurns(cur realm, t *testing.T) {
79 reset(cur)
80 id := earn(cur, t, alice, 10)
81 urequire.Equal(t, int64(10), BalanceOf(alice))
82
83 testing.SetRealm(testing.NewUserRealm(alice))
84 Boost(cross(cur), id)
85
86 uassert.Equal(t, int64(0), BalanceOf(alice))
87 uassert.Equal(t, int64(0), TotalSupply(), "the sink removes supply, it does not move it")
88 uassert.Equal(t, int64(123+PinBlocks), PinnedUntil(id), "tests start at height 123")
89}
90
91func TestBoostRefusesWhatTheCallerCannotAfford(cur realm, t *testing.T) {
92 reset(cur)
93 id := earn(cur, t, alice, 1)
94
95 testing.SetRealm(testing.NewUserRealm(alice))
96 uassert.AbortsContains(t, cur, "balance 1 is short of 10", func() {
97 Boost(cross(cur), id)
98 })
99 uassert.Equal(t, int64(0), PinnedUntil(id), "a refused boost pins nothing")
100 uassert.Equal(t, int64(1), BalanceOf(alice))
101}
102
103// A second boost extends from the current pin, not from now, so the thread
104// never loses time it was already paid for.
105func TestASecondBoostExtendsRatherThanRestarts(cur realm, t *testing.T) {
106 reset(cur)
107 id := earn(cur, t, alice, 20)
108
109 testing.SetRealm(testing.NewUserRealm(alice))
110 Boost(cross(cur), id)
111 first := PinnedUntil(id)
112
113 testing.SetRealm(testing.NewUserRealm(alice))
114 Boost(cross(cur), id)
115 uassert.Equal(t, first+PinBlocks, PinnedUntil(id))
116 uassert.Equal(t, int64(0), BalanceOf(alice))
117}
118
119// Anyone may boost anyone's thread, and the buyer has to get THREAD from
120// somebody who earned it. That is the whole market.
121func TestPlacementIsBoughtFromAnEarner(cur realm, t *testing.T) {
122 reset(cur)
123 id := earn(cur, t, alice, 10)
124
125 testing.SetRealm(testing.NewUserRealm(alice))
126 Transfer(cross(cur), bob, 10)
127 uassert.Equal(t, int64(0), BalanceOf(alice))
128 uassert.Equal(t, int64(10), BalanceOf(bob))
129
130 testing.SetRealm(testing.NewUserRealm(bob))
131 Boost(cross(cur), id)
132 uassert.True(t, PinnedUntil(id) > 0, "bob pinned alice's thread")
133 uassert.Equal(t, int64(0), TotalSupply())
134}
135
136func TestPostRefusals(cur realm, t *testing.T) {
137 reset(cur)
138 testing.SetRealm(testing.NewUserRealm(alice))
139 uassert.AbortsContains(t, cur, "not a page key", func() {
140 Post(cross(cur), "nopath", "body")
141 })
142 testing.SetRealm(testing.NewUserRealm(alice))
143 uassert.AbortsContains(t, cur, "body is empty", func() {
144 Post(cross(cur), homePage, " ")
145 })
146 testing.SetRealm(testing.NewUserRealm(alice))
147 uassert.AbortsContains(t, cur, "no such thread", func() {
148 Reply(cross(cur), 99, "into the void")
149 })
150 uassert.Equal(t, 0, Total())
151}
152
153func TestCountsAreScopedToTheirPage(cur realm, t *testing.T) {
154 reset(cur)
155 testing.SetRealm(testing.NewUserRealm(alice))
156 Post(cross(cur), homePage, "on home")
157 testing.SetRealm(testing.NewUserRealm(alice))
158 Post(cross(cur), "gno.land/r/moul/blog", "on the blog")
159
160 uassert.Equal(t, 1, Count(homePage))
161 uassert.Equal(t, 0, Count("gno.land/r/nobody/here"))
162 uassert.Equal(t, 2, Total())
163 uassert.Equal(t, 2, Pages())
164}
165
166// RenderBlockFor is the embed, and it escapes what it shows: a raw pipe in a
167// body would otherwise open a column in the host realm's page.
168func TestRenderBlockForEscapesTheBody(cur realm, t *testing.T) {
169 reset(cur)
170 testing.SetRealm(testing.NewUserRealm(alice))
171 Post(cross(cur), homePage, "a | pipe")
172
173 got := RenderBlockFor(homePage)
174 uassert.True(t, strings.Contains(got, "func=Post"), "the block offers the transaction: "+got)
175 uassert.True(t, strings.Contains(got, `| a \| pipe |`),
176 "the body cell escapes the pipe rather than opening a column: "+got)
177 uassert.False(t, strings.Contains(got, "| a | pipe"), "no raw pipe survives: "+got)
178 uassert.True(t, strings.Contains(RenderBlockFor("gno.land/r/nobody/here"), "No discussion yet."))
179}
180
181// TestRender uses uassert rather than an Example because every view contains
182// consecutive blank lines, which gno collapses inside an // Output: block.
183func TestRender(cur realm, t *testing.T) {
184 reset(cur)
185 testing.SetRealm(testing.NewUserRealm(alice))
186 id := Post(cross(cur), homePage, "a thread about [links](x)")
187 testing.SetRealm(testing.NewUserRealm(bob))
188 Reply(cross(cur), id, "a reply with a | pipe")
189
190 index := Render("")
191 uassert.True(t, strings.Contains(index, "| 1 | 1 | 1 |"), "threads, pages, supply: "+index)
192 uassert.True(t, strings.Contains(index, tokenPolicy.Sink), "the index declares the sink")
193 uassert.True(t, strings.Contains(index, `import "`+realmPath+`"`), "the index is the embed doc")
194
195 page := Render("page/" + homePage)
196 uassert.True(t, strings.Contains(page, "/r/moul/home"), "it links the page itself: "+page)
197 uassert.True(t, strings.Contains(page, "func=Post"))
198
199 sid := strconv.FormatInt(id, 10)
200 thread := Render("thread/" + sid)
201 uassert.True(t, strings.Contains(thread, "# Thread #"+sid))
202 uassert.True(t, strings.Contains(thread, "a thread about \\[links\\]\\(x\\)"),
203 "the body is escaped where it is shown: "+thread)
204 uassert.True(t, strings.Contains(thread, "a reply with a \\| pipe"),
205 "a reply cell cannot open a column: "+thread)
206 uassert.True(t, strings.Contains(thread, "func=Boost"))
207
208 uassert.Equal(t, "# Threads\nNo thread #4242", Render("thread/4242"))
209 uassert.Equal(t, "# Threads\nNot a thread id: nope", Render("thread/nope"))
210 uassert.Equal(t, "# Threads\nNot a page key: nope", Render("page/nope"))
211 uassert.True(t, strings.HasPrefix(Render("elsewhere"), "# Not found"))
212}
213
214func TestTokenIsDiscoverableWithoutImportingThisRealm(t *testing.T) {
215 uassert.True(t, strings.HasSuffix(TokenKey(), ".THREAD"), TokenKey())
216 uassert.True(t, strings.Contains(TokenKey(), realmPath), TokenKey())
217}