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.26 Kb · 386 lines
  1package patron
  2
  3import (
  4	"strings"
  5	"testing"
  6
  7	"gno.land/p/moul/kit/store/v0"
  8	"gno.land/p/nt/testutils/v0"
  9	"gno.land/p/nt/uassert/v0"
 10	"gno.land/p/nt/urequire/v0"
 11)
 12
 13var (
 14	alice = testutils.TestAddress("alice")
 15	bob   = testutils.TestAddress("bob")
 16	carol = testutils.TestAddress("carol")
 17)
 18
 19// plan opens one plan with the given price and period and returns its id,
 20// failing the test rather than returning an error nobody reads.
 21func plan(t *testing.T, r *Registry, price, period int64) store.ID {
 22	t.Helper()
 23	id, err := r.Open(alice, "a plan", "every period, forever", price, period)
 24	urequire.NoError(t, err)
 25	return id
 26}
 27
 28// planOf reads a plan back, failing the test when it is not there.
 29func planOf(t *testing.T, r *Registry, id store.ID) *Plan {
 30	t.Helper()
 31	p, ok := r.Get(id)
 32	urequire.True(t, ok, "plan #"+id.String()+" exists")
 33	return p
 34}
 35
 36func TestValidTitle(t *testing.T) {
 37	tests := []struct {
 38		name  string
 39		title string
 40		want  bool
 41	}{
 42		{"plain", "monthly support", true},
 43		{"markdown is allowed and escaped later", "[x](y) | z", true},
 44		{"at the limit", strings.Repeat("x", MaxTitleLen), true},
 45		{"empty", "", false},
 46		{"blank", "   ", false},
 47		{"too long", strings.Repeat("x", MaxTitleLen+1), false},
 48		{"a title is one line", "two\nlines", false},
 49		{"tab is a control character here", "a\tb", false},
 50		{"delete character", "a\x7fb", false},
 51	}
 52	for _, tt := range tests {
 53		uassert.Equal(t, tt.want, ValidTitle(tt.title), tt.name)
 54	}
 55}
 56
 57func TestValidDescription(t *testing.T) {
 58	tests := []struct {
 59		name string
 60		desc string
 61		want bool
 62	}{
 63		{"empty is fine, the title may say it all", "", true},
 64		{"newlines and tabs are content", "a\nb\tc", true},
 65		{"at the limit", strings.Repeat("x", MaxDescriptionLen), true},
 66		{"too long", strings.Repeat("x", MaxDescriptionLen+1), false},
 67		{"control character", "a\x01b", false},
 68		{"delete character", "a\x7fb", false},
 69	}
 70	for _, tt := range tests {
 71		uassert.Equal(t, tt.want, ValidDescription(tt.desc), tt.name)
 72	}
 73}
 74
 75func TestOpenRejectsWhatItCannotStore(t *testing.T) {
 76	tests := []struct {
 77		name   string
 78		title  string
 79		desc   string
 80		price  int64
 81		period int64
 82		want   error
 83	}{
 84		{"no title", "", "d", 100, 100, ErrBadTitle},
 85		{"description too long", "t", strings.Repeat("x", MaxDescriptionLen+1), 100, 100, ErrBadDescription},
 86		{"a free plan is a tip jar", "t", "d", 0, 100, ErrBadPrice},
 87		{"a negative price", "t", "d", -1, 100, ErrBadPrice},
 88		{"no period at all", "t", "d", 100, 0, ErrBadPeriod},
 89		{"under the floor", "t", "d", 100, MinPeriodBlocks - 1, ErrBadPeriod},
 90		{"over the ceiling", "t", "d", 100, MaxPeriodBlocks + 1, ErrBadPeriod},
 91	}
 92	r := NewRegistry()
 93	for _, tt := range tests {
 94		_, err := r.Open(alice, tt.title, tt.desc, tt.price, tt.period)
 95		uassert.ErrorIs(t, err, tt.want, tt.name)
 96	}
 97	uassert.Equal(t, 0, r.Count(), "nothing refused was stored")
 98
 99	// The edges themselves are accepted.
100	_, err := r.Open(alice, "t", "", MinPricePerPeriod, MinPeriodBlocks)
101	uassert.NoError(t, err)
102	_, err = r.Open(alice, "t", "", MinPricePerPeriod, MaxPeriodBlocks)
103	uassert.NoError(t, err)
104	uassert.Equal(t, 2, r.Count())
105}
106
107func TestOnePeriodBuysExactlyOnePeriod(t *testing.T) {
108	r := NewRegistry()
109	id := plan(t, r, 1000, 100)
110
111	pay, err := r.Subscribe(id, bob, 1000, 500)
112	urequire.NoError(t, err)
113
114	uassert.Equal(t, int64(1), pay.Periods)
115	uassert.Equal(t, int64(1000), pay.Spent)
116	uassert.Equal(t, int64(0), pay.Change)
117	uassert.Equal(t, int64(600), pay.PaidThrough, "500 + one period of 100 blocks")
118	uassert.True(t, pay.NewSupporter)
119
120	uassert.Equal(t, int64(1000), r.CreditOf(alice), "the creator is credited at payment time")
121	uassert.Equal(t, int64(1000), r.EarnedBy(alice))
122	uassert.Equal(t, int64(0), r.CreditOf(bob), "an exact payment leaves no change")
123	uassert.Equal(t, int64(1000), r.TotalOwed())
124}
125
126func TestThreePeriodsAtOnce(t *testing.T) {
127	r := NewRegistry()
128	id := plan(t, r, 1000, 100)
129
130	pay, err := r.Subscribe(id, bob, 3000, 500)
131	urequire.NoError(t, err)
132
133	uassert.Equal(t, int64(3), pay.Periods)
134	uassert.Equal(t, int64(3000), pay.Spent)
135	uassert.Equal(t, int64(0), pay.Change)
136	uassert.Equal(t, int64(800), pay.PaidThrough, "three periods of 100 blocks from 500")
137}
138
139// Renewing before the current period ends adds a whole period on top of what
140// is left, rather than restarting the clock and discarding it.
141func TestRenewingEarlyExtendsFromPaidThrough(t *testing.T) {
142	r := NewRegistry()
143	id := plan(t, r, 1000, 100)
144
145	first, err := r.Subscribe(id, bob, 1000, 500)
146	urequire.NoError(t, err)
147	urequire.Equal(t, int64(600), first.PaidThrough)
148
149	// Two blocks before the lapse, with 98 blocks still paid for.
150	second, err := r.Subscribe(id, bob, 1000, 502)
151	urequire.NoError(t, err)
152	uassert.Equal(t, int64(700), second.PaidThrough, "600 + 100, not 502 + 100")
153	uassert.False(t, second.NewSupporter, "bob was already a supporter")
154	uassert.Equal(t, 1, planOf(t, r, id).SupporterCount(), "renewing is not a second supporter")
155}
156
157// Renewing after a lapse starts from now: the gap was never paid for and
158// nothing backdates it.
159func TestRenewingAfterLapseStartsFromNow(t *testing.T) {
160	r := NewRegistry()
161	id := plan(t, r, 1000, 100)
162
163	_, err := r.Subscribe(id, bob, 1000, 500)
164	urequire.NoError(t, err)
165
166	// 900 is long past the 600 the first period ran to.
167	second, err := r.Subscribe(id, bob, 1000, 900)
168	urequire.NoError(t, err)
169	uassert.Equal(t, int64(1000), second.PaidThrough, "900 + 100, the gap is not backdated")
170}
171
172// Paying exactly at the paid-through height is the boundary between the two
173// rules above, and it resolves the same way either way: 600 is not later than
174// 600, so the extension runs from 600.
175func TestRenewingAtTheBoundary(t *testing.T) {
176	r := NewRegistry()
177	id := plan(t, r, 1000, 100)
178
179	_, err := r.Subscribe(id, bob, 1000, 500)
180	urequire.NoError(t, err)
181	second, err := r.Subscribe(id, bob, 1000, 600)
182	urequire.NoError(t, err)
183	uassert.Equal(t, int64(700), second.PaidThrough)
184}
185
186func TestAPaymentShortOfOnePeriodIsRefused(t *testing.T) {
187	r := NewRegistry()
188	id := plan(t, r, 1000, 100)
189
190	for _, sent := range []int64{0, 1, 999} {
191		_, err := r.Subscribe(id, bob, sent, 500)
192		uassert.ErrorIs(t, err, ErrShortOfOnePeriod)
193	}
194	uassert.Equal(t, int64(0), r.CreditOf(alice), "a refused payment credits nobody")
195	uassert.Equal(t, int64(0), planOf(t, r, id).PaidThrough(bob))
196	uassert.Equal(t, 0, planOf(t, r, id).SupporterCount())
197}
198
199// The remainder under one period is the supporter's, not the realm's: keeping
200// it would be a fee nobody agreed to.
201func TestChangeIsCreditedBackAndIsWithdrawable(t *testing.T) {
202	r := NewRegistry()
203	id := plan(t, r, 1000, 100)
204
205	pay, err := r.Subscribe(id, bob, 2500, 500)
206	urequire.NoError(t, err)
207
208	uassert.Equal(t, int64(2), pay.Periods)
209	uassert.Equal(t, int64(2000), pay.Spent)
210	uassert.Equal(t, int64(500), pay.Change)
211	uassert.Equal(t, int64(700), pay.PaidThrough)
212
213	uassert.Equal(t, int64(500), r.CreditOf(bob), "the change is the supporter's")
214	uassert.Equal(t, int64(2000), r.CreditOf(alice))
215	uassert.Equal(t, int64(2500), r.TotalOwed(), "every ugnot that came in is owed to somebody")
216	uassert.Equal(t, int64(0), r.EarnedBy(bob), "change is not earnings")
217
218	got, err := r.Withdraw(bob)
219	urequire.NoError(t, err)
220	uassert.Equal(t, int64(500), got)
221	uassert.Equal(t, int64(0), r.CreditOf(bob))
222	uassert.Equal(t, int64(2000), r.TotalOwed())
223}
224
225// Withdraw zeroes the credit before it reports it, so the second call of a
226// reentrant pair finds nothing.
227func TestWithdrawZeroesBeforeItPays(t *testing.T) {
228	r := NewRegistry()
229	id := plan(t, r, 1000, 100)
230	_, err := r.Subscribe(id, bob, 1000, 500)
231	urequire.NoError(t, err)
232
233	got, err := r.Withdraw(alice)
234	urequire.NoError(t, err)
235	uassert.Equal(t, int64(1000), got)
236
237	_, err = r.Withdraw(alice)
238	uassert.ErrorIs(t, err, ErrNothingToWithdraw)
239
240	_, err = r.Withdraw(carol)
241	uassert.ErrorIs(t, err, ErrNothingToWithdraw, "an address owed nothing withdraws nothing")
242
243	uassert.Equal(t, int64(1000), r.EarnedBy(alice), "earnings outlive the withdrawal")
244	uassert.Equal(t, int64(0), r.CreditOf(alice))
245}
246
247// IsActive is strict on both sides: active at paidThrough-1, not at
248// paidThrough.
249func TestIsActiveFlipsAtTheRightHeight(t *testing.T) {
250	r := NewRegistry()
251	id := plan(t, r, 1000, 100)
252	_, err := r.Subscribe(id, bob, 1000, 500)
253	urequire.NoError(t, err)
254	p := planOf(t, r, id)
255
256	tests := []struct {
257		name string
258		now  int64
259		want bool
260	}{
261		{"the block it was bought in", 500, true},
262		{"one before the end", 599, true},
263		{"the end itself", 600, false},
264		{"after", 601, false},
265	}
266	for _, tt := range tests {
267		uassert.Equal(t, tt.want, p.IsActive(bob, tt.now), tt.name)
268	}
269
270	uassert.False(t, p.IsActive(carol, 500), "an address that never paid is never active")
271	uassert.Equal(t, int64(600), p.PaidThrough(bob))
272	uassert.Equal(t, int64(0), p.PaidThrough(carol))
273	uassert.Equal(t, 1, p.ActiveCount(599))
274	uassert.Equal(t, 0, p.ActiveCount(600))
275}
276
277func TestCloseStopsNewMoneyAndNotPaidTime(t *testing.T) {
278	r := NewRegistry()
279	id := plan(t, r, 1000, 100)
280	_, err := r.Subscribe(id, bob, 1000, 500)
281	urequire.NoError(t, err)
282
283	uassert.ErrorIs(t, r.Close(id, bob), ErrNotCreator)
284	urequire.NoError(t, r.Close(id, alice))
285	uassert.ErrorIs(t, r.Close(id, alice), ErrAlreadyClosed)
286
287	_, err = r.Subscribe(id, carol, 1000, 501)
288	uassert.ErrorIs(t, err, ErrPlanClosed)
289
290	p := planOf(t, r, id)
291	uassert.False(t, p.Open)
292	uassert.True(t, p.IsActive(bob, 599), "a closed plan does not take back a paid period")
293
294	uassert.ErrorIs(t, r.Reopen(id, bob), ErrNotCreator)
295	urequire.NoError(t, r.Reopen(id, alice))
296	uassert.ErrorIs(t, r.Reopen(id, alice), ErrAlreadyOpen)
297
298	_, err = r.Subscribe(id, carol, 1000, 501)
299	uassert.NoError(t, err)
300}
301
302func TestNothingWorksOnAPlanThatIsNotThere(t *testing.T) {
303	r := NewRegistry()
304	_, err := r.Subscribe(4242, bob, 1000, 500)
305	uassert.ErrorIs(t, err, ErrNoPlan)
306	uassert.ErrorIs(t, r.Close(4242, alice), ErrNoPlan)
307	uassert.ErrorIs(t, r.Reopen(4242, alice), ErrNoPlan)
308
309	_, ok := r.Get(4242)
310	uassert.False(t, ok)
311}
312
313func TestOnePaymentCannotBuyUnboundedTime(t *testing.T) {
314	r := NewRegistry()
315	id := plan(t, r, 1, MaxPeriodBlocks)
316
317	_, err := r.Subscribe(id, bob, MaxPeriodsPerPayment+1, 500)
318	uassert.ErrorIs(t, err, ErrTooManyPeriods)
319	uassert.Equal(t, int64(0), planOf(t, r, id).PaidThrough(bob))
320
321	// The ceiling itself is accepted, and the product is the one place the
322	// naive arithmetic would have wrapped.
323	pay, err := r.Subscribe(id, bob, MaxPeriodsPerPayment, 500)
324	urequire.NoError(t, err)
325	uassert.Equal(t, 500+MaxPeriodsPerPayment*MaxPeriodBlocks, pay.PaidThrough)
326}
327
328func TestSupportersAreDistinctAndInFirstPaymentOrder(t *testing.T) {
329	r := NewRegistry()
330	id := plan(t, r, 1000, 100)
331
332	_, err := r.Subscribe(id, carol, 1000, 500)
333	urequire.NoError(t, err)
334	_, err = r.Subscribe(id, bob, 1000, 501)
335	urequire.NoError(t, err)
336	_, err = r.Subscribe(id, carol, 1000, 502)
337	urequire.NoError(t, err)
338
339	p := planOf(t, r, id)
340	got := p.Supporters()
341	urequire.Equal(t, 2, len(got))
342	uassert.Equal(t, carol.String(), got[0].String(), "first payment wins the first slot")
343	uassert.Equal(t, bob.String(), got[1].String())
344	uassert.Equal(t, 2, p.SupporterCount())
345
346	// The slice handed out is a copy: writing to it cannot reach the plan.
347	got[0] = bob
348	uassert.Equal(t, carol.String(), p.Supporters()[0].String())
349}
350
351func TestEarningsAccumulateAcrossPlansAndSupporters(t *testing.T) {
352	r := NewRegistry()
353	first := plan(t, r, 1000, 100)
354	second := plan(t, r, 2000, 200)
355
356	_, err := r.Subscribe(first, bob, 1000, 500)
357	urequire.NoError(t, err)
358	_, err = r.Subscribe(second, carol, 4000, 500)
359	urequire.NoError(t, err)
360
361	uassert.Equal(t, int64(5000), r.EarnedBy(alice))
362	uassert.Equal(t, int64(5000), r.CreditOf(alice))
363	uassert.Equal(t, int64(1000), planOf(t, r, first).Received)
364	uassert.Equal(t, int64(4000), planOf(t, r, second).Received)
365	uassert.Equal(t, 2, r.Count())
366}
367
368func TestListIsNewestFirst(t *testing.T) {
369	r := NewRegistry()
370	first := plan(t, r, 1000, 100)
371	second := plan(t, r, 1000, 100)
372	third := plan(t, r, 1000, 100)
373
374	got := r.List(1, 10)
375	urequire.Equal(t, 3, len(got))
376	uassert.Equal(t, uint64(third), uint64(got[0].ID))
377	uassert.Equal(t, uint64(second), uint64(got[1].ID))
378	uassert.Equal(t, uint64(first), uint64(got[2].ID))
379	uassert.Equal(t, 2, r.Pages(2))
380	uassert.Equal(t, 0, len(r.List(9, 10)))
381}
382
383func TestURLsStripTheChainDomain(t *testing.T) {
384	uassert.Equal(t, "/r/moul/x/social/patron/v0", RealmURL("gno.land/r/moul/x/social/patron/v0"))
385	uassert.Equal(t, "/r/a/b:plan/7", PlanURL("gno.land/r/a/b", 7))
386}