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

patron_test.gno

12.84 Kb · 387 lines
  1package patron
  2
  3import (
  4	"strconv"
  5	"strings"
  6	"testing"
  7
  8	"chain"
  9	"chain/banker"
 10	"chain/runtime"
 11
 12	pt "gno.land/p/moul/x/social/patron/v0"
 13	"gno.land/p/nt/testutils/v0"
 14	"gno.land/p/nt/uassert/v0"
 15	"gno.land/p/nt/urequire/v0"
 16)
 17
 18const (
 19	price  = int64(1000000) // 1 GNOT per period
 20	period = int64(100)     // blocks
 21)
 22
 23var (
 24	alice = testutils.TestAddress("alice") // the creator, throughout
 25	bob   = testutils.TestAddress("bob")
 26	carol = testutils.TestAddress("carol")
 27
 28	realmAddr = chain.PackageAddress(realmPath)
 29)
 30
 31// reset clears realm state. Realm globals live for the whole test binary, so
 32// everything a later test or the Render tests read starts here.
 33func reset() { plans = pt.NewRegistry() }
 34
 35// pay stages a payment the way the chain does: the coins land at the realm's
 36// address before a line of its code runs, and OriginSend is what the crossing
 37// function then reads.
 38func pay(amount int64) {
 39	testing.IssueCoins(realmAddr, chain.Coins{{denom, amount}})
 40	testing.SetOriginSend(chain.Coins{{denom, amount}})
 41}
 42
 43// open creates a plan as alice. SetRealm is call-frame scoped, so the account
 44// is switched here and then crossed from here, never from a helper that does
 45// not itself cross.
 46func open(cur realm, t *testing.T) int64 {
 47	t.Helper()
 48	testing.SetRealm(testing.NewUserRealm(alice))
 49	return Open(cross(cur), "monthly support", "a plan with a | pipe in it", price, period)
 50}
 51
 52func TestOpenStoresWhatItWasGiven(cur realm, t *testing.T) {
 53	reset()
 54	id := open(cur, t)
 55
 56	creator, title, desc, p, blocks, isOpen := Get(id)
 57	uassert.Equal(t, alice.String(), creator.String())
 58	uassert.Equal(t, "monthly support", title)
 59	uassert.Equal(t, "a plan with a | pipe in it", desc)
 60	uassert.Equal(t, price, p)
 61	uassert.Equal(t, period, blocks)
 62	uassert.True(t, isOpen)
 63	uassert.Equal(t, 1, Count())
 64	uassert.Equal(t, 0, SupporterCount(id))
 65}
 66
 67func TestOpenRefusesAFreePlanAndASillyPeriod(cur realm, t *testing.T) {
 68	reset()
 69
 70	testing.SetRealm(testing.NewUserRealm(alice))
 71	uassert.AbortsContains(t, cur, "at least one ugnot per period", func() {
 72		Open(cross(cur), "free", "", 0, period)
 73	})
 74	testing.SetRealm(testing.NewUserRealm(alice))
 75	uassert.AbortsContains(t, cur, "period out of range", func() {
 76		Open(cross(cur), "instant", "", price, 0)
 77	})
 78	testing.SetRealm(testing.NewUserRealm(alice))
 79	uassert.AbortsContains(t, cur, "period out of range", func() {
 80		Open(cross(cur), "forever", "", price, pt.MaxPeriodBlocks+1)
 81	})
 82	testing.SetRealm(testing.NewUserRealm(alice))
 83	uassert.AbortsContains(t, cur, "title is empty", func() {
 84		Open(cross(cur), "   ", "", price, period)
 85	})
 86	uassert.Equal(t, 0, Count())
 87}
 88
 89// One period, paid exactly, credited to the creator at payment time and
 90// withdrawable as real coins in this same test, because the bank does not
 91// carry over between test functions.
 92func TestOnePeriodIsBoughtAndTheCreatorIsPaid(cur realm, t *testing.T) {
 93	reset()
 94	id := open(cur, t)
 95	start := runtime.ChainHeight()
 96
 97	pay(price)
 98	testing.SetRealm(testing.NewUserRealm(bob))
 99	through := Subscribe(cross(cur), id)
100
101	uassert.Equal(t, start+period, through)
102	uassert.Equal(t, start+period, PaidThrough(id, bob))
103	uassert.True(t, IsActive(id, bob))
104	uassert.Equal(t, 1, SupporterCount(id))
105	uassert.Equal(t, price, EarnedBy(alice))
106	uassert.Equal(t, price, CreditOf(alice))
107	uassert.Equal(t, int64(0), CreditOf(bob), "an exact payment leaves no change")
108	uassert.Equal(t, price, TotalOwed())
109
110	ro := banker.NewReadonlyBanker()
111	before := ro.GetCoin(alice, denom)
112
113	testing.SetRealm(testing.NewUserRealm(alice))
114	got := Withdraw(cross(cur))
115
116	uassert.Equal(t, price, got)
117	uassert.Equal(t, price, ro.GetCoin(alice, denom)-before, "the creator was really paid")
118	uassert.Equal(t, int64(0), CreditOf(alice))
119	uassert.Equal(t, price, EarnedBy(alice), "earnings outlive the withdrawal")
120	uassert.Equal(t, int64(0), TotalOwed())
121}
122
123func TestThreePeriodsAtOnce(cur realm, t *testing.T) {
124	reset()
125	id := open(cur, t)
126	start := runtime.ChainHeight()
127
128	pay(3 * price)
129	testing.SetRealm(testing.NewUserRealm(bob))
130	through := Subscribe(cross(cur), id)
131
132	uassert.Equal(t, start+3*period, through)
133	uassert.Equal(t, 3*price, CreditOf(alice))
134	uassert.Equal(t, int64(0), CreditOf(bob))
135}
136
137// Renewing before the lapse extends from paidThrough, so the time already
138// paid for is still there afterwards.
139func TestRenewingEarlyExtendsRatherThanRestarts(cur realm, t *testing.T) {
140	reset()
141	id := open(cur, t)
142
143	pay(price)
144	testing.SetRealm(testing.NewUserRealm(bob))
145	first := Subscribe(cross(cur), id)
146
147	// Ten blocks in, with ninety still paid for.
148	testing.SkipHeights(10)
149	pay(price)
150	testing.SetRealm(testing.NewUserRealm(bob))
151	second := Subscribe(cross(cur), id)
152
153	uassert.Equal(t, first+period, second, "the second period starts where the first ended")
154	uassert.True(t, second > runtime.ChainHeight()+period,
155		"more than one period is left, which is the point")
156	uassert.Equal(t, 1, SupporterCount(id), "renewing is not a second supporter")
157}
158
159// Renewing after a lapse starts from now: the gap was never paid for.
160func TestRenewingAfterALapseStartsFromNow(cur realm, t *testing.T) {
161	reset()
162	id := open(cur, t)
163
164	pay(price)
165	testing.SetRealm(testing.NewUserRealm(bob))
166	Subscribe(cross(cur), id)
167
168	testing.SkipHeights(500)
169	uassert.False(t, IsActive(id, bob), "the first period lapsed long ago")
170
171	pay(price)
172	testing.SetRealm(testing.NewUserRealm(bob))
173	second := Subscribe(cross(cur), id)
174
175	uassert.Equal(t, runtime.ChainHeight()+period, second)
176	uassert.True(t, IsActive(id, bob))
177}
178
179// IsActive flips at the block the period ends on, and not a block either side.
180func TestIsActiveFlipsAtTheRightHeight(cur realm, t *testing.T) {
181	reset()
182	id := open(cur, t)
183	start := runtime.ChainHeight()
184
185	pay(price)
186	testing.SetRealm(testing.NewUserRealm(bob))
187	Subscribe(cross(cur), id)
188
189	testing.SetHeight(start + period - 1)
190	uassert.True(t, IsActive(id, bob), "the last block of the period")
191
192	testing.SetHeight(start + period)
193	uassert.False(t, IsActive(id, bob), "the first block after it")
194
195	uassert.False(t, IsActive(id, carol), "an address that never paid is never active")
196	uassert.Equal(t, int64(0), PaidThrough(id, carol))
197}
198
199func TestAPaymentShortOfOnePeriodIsRefused(cur realm, t *testing.T) {
200	reset()
201	id := open(cur, t)
202
203	pay(price - 1)
204	testing.SetRealm(testing.NewUserRealm(bob))
205	uassert.AbortsContains(t, cur, "this call costs at least", func() {
206		Subscribe(cross(cur), id)
207	})
208
209	uassert.Equal(t, int64(0), PaidThrough(id, bob))
210	uassert.Equal(t, int64(0), EarnedBy(alice))
211	uassert.Equal(t, 0, SupporterCount(id))
212}
213
214// The remainder under one period is the supporter's, not the realm's, and it
215// comes back out as real coins.
216func TestChangeIsCreditedBackAndWithdrawable(cur realm, t *testing.T) {
217	reset()
218	id := open(cur, t)
219
220	pay(2*price + 250000)
221	testing.SetRealm(testing.NewUserRealm(bob))
222	Subscribe(cross(cur), id)
223
224	uassert.Equal(t, int64(250000), CreditOf(bob), "the change is the supporter's")
225	uassert.Equal(t, 2*price, CreditOf(alice))
226	uassert.Equal(t, 2*price+250000, TotalOwed(), "every ugnot in is owed to somebody")
227
228	ro := banker.NewReadonlyBanker()
229	before := ro.GetCoin(bob, denom)
230
231	testing.SetRealm(testing.NewUserRealm(bob))
232	got := Withdraw(cross(cur))
233
234	uassert.Equal(t, int64(250000), got)
235	uassert.Equal(t, int64(250000), ro.GetCoin(bob, denom)-before)
236	uassert.Equal(t, int64(0), CreditOf(bob))
237
238	testing.SetRealm(testing.NewUserRealm(bob))
239	uassert.AbortsContains(t, cur, "nothing to withdraw", func() { Withdraw(cross(cur)) })
240}
241
242func TestWithdrawRefusesSomebodyOwedNothing(cur realm, t *testing.T) {
243	reset()
244	testing.SetRealm(testing.NewUserRealm(carol))
245	uassert.AbortsContains(t, cur, "nothing to withdraw", func() { Withdraw(cross(cur)) })
246}
247
248// A realm that the user called holds the coins itself and could subscribe
249// over and over against one payment. The guard runs before the envelope is
250// read, so it cannot.
251func TestOnlyAUserCanSubscribe(cur realm, t *testing.T) {
252	reset()
253	id := open(cur, t)
254
255	pay(price)
256	testing.SetRealm(testing.NewCodeRealm("gno.land/r/g1relay/forwarder"))
257	uassert.AbortsContains(t, cur, "must be called directly by a user", func() {
258		Subscribe(cross(cur), id)
259	})
260	uassert.Equal(t, int64(0), EarnedBy(alice))
261}
262
263func TestCloseAndReopenAreTheCreatorsAlone(cur realm, t *testing.T) {
264	reset()
265	id := open(cur, t)
266
267	pay(price)
268	testing.SetRealm(testing.NewUserRealm(bob))
269	Subscribe(cross(cur), id)
270
271	testing.SetRealm(testing.NewUserRealm(bob))
272	uassert.AbortsContains(t, cur, "only the plan's creator", func() { Close(cross(cur), id) })
273
274	testing.SetRealm(testing.NewUserRealm(alice))
275	Close(cross(cur), id)
276	_, _, _, _, _, isOpen := Get(id)
277	uassert.False(t, isOpen)
278	uassert.True(t, IsActive(id, bob), "closing does not take back a paid period")
279
280	pay(price)
281	testing.SetRealm(testing.NewUserRealm(carol))
282	uassert.AbortsContains(t, cur, "closed to new subscriptions", func() {
283		Subscribe(cross(cur), id)
284	})
285
286	testing.SetRealm(testing.NewUserRealm(alice))
287	uassert.AbortsContains(t, cur, "already closed", func() { Close(cross(cur), id) })
288
289	testing.SetRealm(testing.NewUserRealm(bob))
290	uassert.AbortsContains(t, cur, "only the plan's creator", func() { Reopen(cross(cur), id) })
291
292	testing.SetRealm(testing.NewUserRealm(alice))
293	Reopen(cross(cur), id)
294	testing.SetRealm(testing.NewUserRealm(alice))
295	uassert.AbortsContains(t, cur, "already open", func() { Reopen(cross(cur), id) })
296
297	pay(price)
298	testing.SetRealm(testing.NewUserRealm(carol))
299	Subscribe(cross(cur), id)
300	uassert.Equal(t, 2, SupporterCount(id))
301}
302
303func TestAPlanThatIsNotThere(cur realm, t *testing.T) {
304	reset()
305	testing.SetRealm(testing.NewUserRealm(bob))
306	uassert.AbortsContains(t, cur, "plan #4242 not found", func() {
307		Subscribe(cross(cur), 4242)
308	})
309	uassert.PanicsContains(t, cur, "plan #4242 not found", func() { Count(); IsActive(4242, bob) })
310}
311
312func TestSupportersAreDistinctAndInFirstPaymentOrder(cur realm, t *testing.T) {
313	reset()
314	id := open(cur, t)
315
316	pay(price)
317	testing.SetRealm(testing.NewUserRealm(carol))
318	Subscribe(cross(cur), id)
319	pay(price)
320	testing.SetRealm(testing.NewUserRealm(bob))
321	Subscribe(cross(cur), id)
322	pay(price)
323	testing.SetRealm(testing.NewUserRealm(carol))
324	Subscribe(cross(cur), id)
325
326	got := Supporters(id)
327	urequire.Equal(t, 2, len(got))
328	uassert.Equal(t, carol.String(), got[0].String())
329	uassert.Equal(t, bob.String(), got[1].String())
330}
331
332// TestRender uses uassert rather than an Example because every view contains
333// consecutive blank lines, which gno collapses inside an // Output: block.
334func TestRender(cur realm, t *testing.T) {
335	reset()
336
337	empty := Render("")
338	uassert.True(t, strings.Contains(empty, "No plans yet."), empty)
339	uassert.True(t, strings.Contains(empty, "no cron on chain"), "the index says it: "+empty)
340
341	testing.SetRealm(testing.NewUserRealm(alice))
342	id := Open(cross(cur), "a | pipe and [a link](x)", "about **everything**", price, period)
343
344	pay(price)
345	testing.SetRealm(testing.NewUserRealm(bob))
346	Subscribe(cross(cur), id)
347
348	index := Render("")
349	uassert.False(t, strings.Contains(index, "| a | pipe"),
350		"a raw pipe would open a column: "+index)
351	uassert.True(t, strings.Contains(index, "a \\| pipe and \\[a link\\]\\(x\\)"),
352		"the title is escaped once in the listing, pipe included: "+index)
353	uassert.True(t, strings.Contains(index, "["+strconv.FormatInt(id, 10)+"](/r/moul/x/social/patron/v0:plan/"),
354		"the link text is the plan number, which is ours and not the caller's: "+index)
355	uassert.True(t, strings.Contains(index, "func=Open"), "the index offers the transaction")
356	uassert.True(t, strings.Contains(index, "1 of 1"), "one active of one supporter: "+index)
357
358	sid := strconv.FormatInt(id, 10)
359	page := Render("plan/" + sid)
360	uassert.True(t, strings.Contains(page, "# Plan #"+sid))
361	// A heading is prose, so ui.Inline escapes the link syntax and leaves the
362	// pipe alone: a pipe only means something inside a table.
363	uassert.True(t, strings.Contains(page, "# Plan #"+sid+": a | pipe and \\[a link\\]\\(x\\)"),
364		"the title is escaped in the heading too: "+page)
365	uassert.True(t, strings.Contains(page, "about \\*\\*everything\\*\\*"),
366		"the description is escaped: "+page)
367	uassert.True(t, strings.Contains(page, "func=Subscribe"))
368	uassert.True(t, strings.Contains(page, ".send="+strconv.FormatInt(price, 10)+"ugnot"),
369		"the support link carries the price: "+page)
370	uassert.True(t, strings.Contains(page, "func=Close"))
371
372	testing.SetRealm(testing.NewUserRealm(alice))
373	Close(cross(cur), id)
374	closed := Render("plan/" + sid)
375	uassert.True(t, strings.Contains(closed, "takes no new subscriptions"), closed)
376	uassert.False(t, strings.Contains(closed, "func=Subscribe"),
377		"a closed plan offers no payment: "+closed)
378
379	uassert.Equal(t, "# Patron\nNo plan #4242", Render("plan/4242"))
380	uassert.Equal(t, "# Patron\nNot a plan id: nope", Render("plan/nope"))
381	uassert.True(t, strings.HasPrefix(Render("elsewhere"), "# Not found"))
382
383	// A page past the end is an empty listing, not a panic.
384	uassert.True(t, strings.Contains(Render("?page=99"), "No plans yet."))
385	uassert.True(t, strings.Contains(Render("?page=nonsense"), "a \\| pipe"),
386		"an unreadable page number falls back to the first")
387}