lastwords_test.gno
8.65 Kb · 251 lines
1package lastwords
2
3import (
4 "strings"
5 "testing"
6
7 "chain"
8 "chain/banker"
9 "chain/runtime"
10
11 "gno.land/p/nt/testutils/v0"
12 "gno.land/p/nt/uassert/v0"
13)
14
15var realmAddr = chain.PackageAddress("gno.land/r/moul/x/games/lastwords/v0")
16
17var (
18 alice = testutils.TestAddress("alice")
19 bob = testutils.TestAddress("bob")
20 carol = testutils.TestAddress("carol")
21)
22
23// reset reopens the slot at the current height. Heights are relative in tests
24// (SkipHeights moves forward from wherever the last test left off and there is
25// no absolute setter), so nothing here asserts an absolute height.
26func reset() {
27 open(runtime.ChainHeight())
28}
29
30// write sends amount ugnot alongside the call, the way a real transaction
31// would: the runtime deposits it at the realm's address, then OriginSend
32// reports it to the crossing function.
33func write(cur realm, who address, amount int64, msg string) {
34 testing.IssueCoins(realmAddr, chain.Coins{{denom, amount}})
35 testing.SetOriginSend(chain.Coins{{denom, amount}})
36 testing.SetRealm(testing.NewUserRealm(who))
37 Write(cross(cur), msg)
38}
39
40func TestWriteTakesTheSlot(cur realm, t *testing.T) {
41 reset()
42 uassert.Equal(t, "", Message())
43
44 write(cur, alice, Price("hello"), "hello")
45 uassert.Equal(t, "hello", Message())
46 uassert.Equal(t, alice.String(), Author().String())
47 uassert.Equal(t, int64(0), ledger.TotalOwed(), "nobody was displaced yet")
48 uassert.Equal(t, MinWrite+5*PerByte, Pot(), "the whole first payment is the pot")
49}
50
51// TestDisplacedWriterIsPaid is rule 3: overwriting somebody pays them, so a
52// second player has a reason to exist.
53func TestDisplacedWriterIsPaid(cur realm, t *testing.T) {
54 reset()
55 write(cur, alice, Price("alice was here"), "alice was here")
56 potAfterFirst := Pot()
57
58 paidByBob := Price("no, bob was")
59 write(cur, bob, paidByBob, "no, bob was")
60
61 dividend := paidByBob * DividendPct / 100
62 uassert.Equal(t, dividend, Owed(alice), "half of bob's payment, credited")
63 uassert.Equal(t, potAfterFirst+paidByBob-dividend, Pot())
64 uassert.Equal(t, int64(0), Owed(bob), "the holder is owed nothing yet")
65}
66
67func TestPriceRisesWithTheHolderAndTheLength(cur realm, t *testing.T) {
68 reset()
69 uassert.Equal(t, MinWrite, Price(""), "the base price with an empty slot")
70 uassert.Equal(t, MinWrite+10*PerByte, Price("0123456789"), "ten bytes cost ten byte-prices")
71
72 first := Price("x")
73 write(cur, alice, first, "x")
74 uassert.Equal(t, first+1, Price(""), "outbidding the holder is the floor")
75}
76
77func TestWriteRejectsBadInput(cur realm, t *testing.T) {
78 reset()
79
80 testing.SetOriginSend(chain.Coins{{denom, MinWrite}})
81 testing.SetRealm(testing.NewUserRealm(alice))
82 uassert.AbortsContains(t, cur, "must not be empty", func() { Write(cross(cur), "") })
83
84 long := strings.Repeat("x", MaxLen+1)
85 testing.SetOriginSend(chain.Coins{{denom, Price(long)}})
86 testing.SetRealm(testing.NewUserRealm(alice))
87 uassert.AbortsContains(t, cur, "at most", func() { Write(cross(cur), long) })
88
89 testing.SetOriginSend(chain.Coins{{denom, Price("a\nb")}})
90 testing.SetRealm(testing.NewUserRealm(alice))
91 uassert.AbortsContains(t, cur, "single line", func() { Write(cross(cur), "a\nb") })
92
93 testing.SetOriginSend(chain.Coins{{denom, MinWrite - 1}})
94 testing.SetRealm(testing.NewUserRealm(alice))
95 uassert.AbortsContains(t, cur, "price is", func() { Write(cross(cur), "cheapskate") })
96
97 uassert.Equal(t, "", Message(), "none of that took the slot")
98}
99
100// TestEveryWriteExtendsButNeverPastTheHardEnd is rule 1, the terminator. A
101// constant extension per write is how a pot stays open forever.
102func TestEveryWriteExtendsButNeverPastTheHardEnd(cur realm, t *testing.T) {
103 reset()
104 start := runtime.ChainHeight()
105 hardEnd := start + Life
106 uassert.Equal(t, start+Window, Deadline(), "it opens with the window")
107
108 prev := Deadline()
109 for i := 0; i < 30; i++ {
110 testing.SkipHeights(100)
111 if runtime.ChainHeight() >= Deadline() {
112 break
113 }
114 write(cur, alice, Price("again"), "again")
115 uassert.True(t, Deadline() >= prev, "a write never shortens the clock")
116 uassert.True(t, Deadline() <= hardEnd, "and never passes the hard end")
117 prev = Deadline()
118 }
119 uassert.True(t, Deadline() <= hardEnd)
120 uassert.True(t, Deadline() > start+Window, "the writes did buy time")
121}
122
123func TestClosedSlotRefusesAWrite(cur realm, t *testing.T) {
124 reset()
125 write(cur, alice, Price("famous last words"), "famous last words")
126 testing.SkipHeights(Life + 1)
127
128 testing.SetOriginSend(chain.Coins{{denom, Price("too late")}})
129 testing.SetRealm(testing.NewUserRealm(bob))
130 uassert.AbortsContains(t, cur, "slot is closed", func() { Write(cross(cur), "too late") })
131 uassert.Equal(t, "famous last words", Message(), "the last words stay last")
132}
133
134// TestSettleSplitsAmongEveryoneButTheHolder is rule 2, and it is the whole
135// reason this is not a winner-takes-all pot.
136func TestSettleSplitsAmongEveryoneButTheHolder(cur realm, t *testing.T) {
137 reset()
138 write(cur, alice, Price("a"), "a")
139 testing.SkipHeights(10)
140 write(cur, bob, Price("b"), "b")
141 testing.SkipHeights(10)
142 write(cur, carol, Price("c"), "c")
143
144 beforeAlice, beforeBob := Owed(alice), Owed(bob)
145 pot := Pot()
146 uassert.True(t, pot > 0)
147
148 testing.SkipHeights(Life + 1)
149 testing.SetRealm(testing.NewUserRealm(bob)) // anyone may settle
150 Settle(cross(cur))
151
152 uassert.Equal(t, int64(0), Pot(), "the pot is fully distributed")
153 uassert.Equal(t, int64(0), Owed(carol), "the holder takes the slot, not the pot")
154 uassert.True(t, Owed(alice) > beforeAlice, "alice got a share")
155 uassert.True(t, Owed(bob) > beforeBob, "bob got a share")
156 split := (Owed(alice) - beforeAlice) + (Owed(bob) - beforeBob)
157 uassert.Equal(t, pot, split, "and the shares add up to exactly the pot")
158}
159
160func TestSettleWithOneWriterReturnsThePot(cur realm, t *testing.T) {
161 reset()
162 write(cur, alice, Price("alone"), "alone")
163 pot := Pot()
164 testing.SkipHeights(Life + 1)
165
166 testing.SetRealm(testing.NewUserRealm(alice))
167 Settle(cross(cur))
168 uassert.Equal(t, pot, Owed(alice), "nobody else to share with")
169}
170
171func TestSettleRefusesEarlyAndTwice(cur realm, t *testing.T) {
172 reset()
173 write(cur, alice, Price("tick"), "tick")
174
175 testing.SetRealm(testing.NewUserRealm(bob))
176 uassert.AbortsContains(t, cur, "still open", func() { Settle(cross(cur)) })
177
178 testing.SkipHeights(Life + 1)
179 testing.SetRealm(testing.NewUserRealm(bob))
180 Settle(cross(cur))
181 testing.SetRealm(testing.NewUserRealm(bob))
182 uassert.AbortsContains(t, cur, "already settled", func() { Settle(cross(cur)) })
183}
184
185// TestWithdrawPaysRealCoins checks the half nothing else can: the ledger and
186// the realm's balance agree.
187func TestWithdrawPaysRealCoins(cur realm, t *testing.T) {
188 reset()
189 write(cur, alice, Price("first"), "first")
190 testing.SkipHeights(10)
191 write(cur, bob, Price("second"), "second")
192
193 owed := Owed(alice)
194 uassert.True(t, owed > 0)
195
196 ro := banker.NewReadonlyBanker()
197 before := ro.GetCoins(alice).AmountOf(denom)
198
199 testing.SetRealm(testing.NewUserRealm(alice))
200 got := Withdraw(cross(cur))
201 uassert.Equal(t, owed, got)
202 uassert.Equal(t, int64(0), Owed(alice), "the credit is zeroed before the coins move")
203
204 after := banker.NewReadonlyBanker().GetCoins(alice).AmountOf(denom)
205 uassert.Equal(t, before+owed, after, "the coins actually arrived")
206
207 testing.SetRealm(testing.NewUserRealm(carol))
208 uassert.AbortsContains(t, cur, "nothing to withdraw", func() { Withdraw(cross(cur)) })
209}
210
211// TestRenderEscapesTheMessage is the one bug a live deploy makes permanent: the
212// realm's entire product is a string somebody else typed.
213func TestRenderEscapesTheMessage(cur realm, t *testing.T) {
214 reset()
215 write(cur, alice, Price("[click](https://evil.example)"), "[click](https://evil.example)")
216
217 out := Render("")
218 uassert.False(t, strings.Contains(out, "](https://evil.example)"), "no live link")
219 uassert.False(t, strings.Contains(out, "[click]"), "the link syntax is dead")
220 uassert.True(t, strings.Contains(out, "click"), "the text is still readable")
221 uassert.True(t, strings.Contains(out, "Last Words"))
222}
223
224func TestRenderCoversEveryPhase(cur realm, t *testing.T) {
225 reset()
226 uassert.True(t, strings.Contains(Render(""), "The slot is empty"))
227
228 write(cur, alice, Price("open for business"), "open for business")
229 out := Render("")
230 uassert.True(t, strings.Contains(out, "open for business"))
231 uassert.True(t, strings.Contains(out, "Open for another"))
232
233 testing.SkipHeights(Life + 1)
234 out = Render("")
235 uassert.True(t, strings.Contains(out, "**Closed** at height"))
236 uassert.True(t, strings.Contains(out, "may call `Settle`"))
237
238 testing.SetRealm(testing.NewUserRealm(bob))
239 Settle(cross(cur))
240 out = Render("")
241 uassert.True(t, strings.Contains(out, "Settled"))
242 uassert.True(t, strings.Contains(out, "Owed"))
243}
244
245func TestGnotFormatting(t *testing.T) {
246 uassert.Equal(t, "0", gnot(0))
247 uassert.Equal(t, "1", gnot(1000000))
248 uassert.Equal(t, "1.5", gnot(1500000))
249 uassert.Equal(t, "0.000001", gnot(1))
250 uassert.Equal(t, "12.345678", gnot(12345678))
251}