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}