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

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}