1package patron23import (4 "strings"5 "testing"67 "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)1213var (14 alice = testutils.TestAddress("alice")15 bob = testutils.TestAddress("bob")16 carol = testutils.TestAddress("carol")17)1819// 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 id26}2728// 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 p34}3536func TestValidTitle(t *testing.T) {37 tests := []struct {38 name string39 title string40 want bool41 }{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}5657func TestValidDescription(t *testing.T) {58 tests := []struct {59 name string60 desc string61 want bool62 }{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}7475func TestOpenRejectsWhatItCannotStore(t *testing.T) {76 tests := []struct {77 name string78 title string79 desc string80 price int6481 period int6482 want error83 }{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")9899 // 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}106107func TestOnePeriodBuysExactlyOnePeriod(t *testing.T) {108 r := NewRegistry()109 id := plan(t, r, 1000, 100)110111 pay, err := r.Subscribe(id, bob, 1000, 500)112 urequire.NoError(t, err)113114 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)119120 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}125126func TestThreePeriodsAtOnce(t *testing.T) {127 r := NewRegistry()128 id := plan(t, r, 1000, 100)129130 pay, err := r.Subscribe(id, bob, 3000, 500)131 urequire.NoError(t, err)132133 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}138139// Renewing before the current period ends adds a whole period on top of what140// 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)144145 first, err := r.Subscribe(id, bob, 1000, 500)146 urequire.NoError(t, err)147 urequire.Equal(t, int64(600), first.PaidThrough)148149 // 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}156157// Renewing after a lapse starts from now: the gap was never paid for and158// nothing backdates it.159func TestRenewingAfterLapseStartsFromNow(t *testing.T) {160 r := NewRegistry()161 id := plan(t, r, 1000, 100)162163 _, err := r.Subscribe(id, bob, 1000, 500)164 urequire.NoError(t, err)165166 // 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}171172// Paying exactly at the paid-through height is the boundary between the two173// rules above, and it resolves the same way either way: 600 is not later than174// 600, so the extension runs from 600.175func TestRenewingAtTheBoundary(t *testing.T) {176 r := NewRegistry()177 id := plan(t, r, 1000, 100)178179 _, 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}185186func TestAPaymentShortOfOnePeriodIsRefused(t *testing.T) {187 r := NewRegistry()188 id := plan(t, r, 1000, 100)189190 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}198199// The remainder under one period is the supporter's, not the realm's: keeping200// it would be a fee nobody agreed to.201func TestChangeIsCreditedBackAndIsWithdrawable(t *testing.T) {202 r := NewRegistry()203 id := plan(t, r, 1000, 100)204205 pay, err := r.Subscribe(id, bob, 2500, 500)206 urequire.NoError(t, err)207208 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)212213 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")217218 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}224225// Withdraw zeroes the credit before it reports it, so the second call of a226// 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)232233 got, err := r.Withdraw(alice)234 urequire.NoError(t, err)235 uassert.Equal(t, int64(1000), got)236237 _, err = r.Withdraw(alice)238 uassert.ErrorIs(t, err, ErrNothingToWithdraw)239240 _, err = r.Withdraw(carol)241 uassert.ErrorIs(t, err, ErrNothingToWithdraw, "an address owed nothing withdraws nothing")242243 uassert.Equal(t, int64(1000), r.EarnedBy(alice), "earnings outlive the withdrawal")244 uassert.Equal(t, int64(0), r.CreditOf(alice))245}246247// IsActive is strict on both sides: active at paidThrough-1, not at248// 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)255256 tests := []struct {257 name string258 now int64259 want bool260 }{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 }269270 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}276277func 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)282283 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)286287 _, err = r.Subscribe(id, carol, 1000, 501)288 uassert.ErrorIs(t, err, ErrPlanClosed)289290 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")293294 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)297298 _, err = r.Subscribe(id, carol, 1000, 501)299 uassert.NoError(t, err)300}301302func 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)308309 _, ok := r.Get(4242)310 uassert.False(t, ok)311}312313func TestOnePaymentCannotBuyUnboundedTime(t *testing.T) {314 r := NewRegistry()315 id := plan(t, r, 1, MaxPeriodBlocks)316317 _, 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))320321 // The ceiling itself is accepted, and the product is the one place the322 // 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}327328func TestSupportersAreDistinctAndInFirstPaymentOrder(t *testing.T) {329 r := NewRegistry()330 id := plan(t, r, 1000, 100)331332 _, 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)338339 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())345346 // The slice handed out is a copy: writing to it cannot reach the plan.347 got[0] = bob348 uassert.Equal(t, carol.String(), p.Supporters()[0].String())349}350351func TestEarningsAccumulateAcrossPlansAndSupporters(t *testing.T) {352 r := NewRegistry()353 first := plan(t, r, 1000, 100)354 second := plan(t, r, 2000, 200)355356 _, 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)360361 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}367368func 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)373374 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}382383func 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}387Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.