Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

pullpayment_test.gno

6.37 Kb · 212 lines
  1package pullpayment
  2
  3import (
  4	"strconv"
  5	"strings"
  6	"testing"
  7
  8	"gno.land/p/nt/uassert/v0"
  9)
 10
 11func TestEmpty(t *testing.T) {
 12	l := New()
 13	uassert.True(t, l.IsEmpty())
 14	uassert.Equal(t, 0, l.Count())
 15	uassert.Equal(t, int64(0), l.TotalOwed())
 16	uassert.Equal(t, int64(0), l.Balance("nobody"))
 17	uassert.True(t, l.Consistent())
 18}
 19
 20func TestCreditAccumulates(t *testing.T) {
 21	l := New()
 22	uassert.NoError(t, l.Credit("alice", 100))
 23	uassert.NoError(t, l.Credit("alice", 50))
 24
 25	uassert.Equal(t, int64(150), l.Balance("alice"), "credits accumulate")
 26	uassert.Equal(t, 1, l.Count(), "the same payee is not counted twice")
 27	uassert.Equal(t, int64(150), l.TotalOwed())
 28	uassert.True(t, l.Consistent())
 29}
 30
 31func TestCreditRejectsNonPositive(t *testing.T) {
 32	l := New()
 33	uassert.ErrorIs(t, l.Credit("alice", 0), ErrBadAmount)
 34	uassert.ErrorIs(t, l.Credit("alice", -1), ErrBadAmount)
 35	uassert.True(t, l.IsEmpty(), "a rejected credit changes nothing")
 36}
 37
 38func TestWithdrawZeroesBalance(t *testing.T) {
 39	l := New()
 40	l.Credit("alice", 100)
 41
 42	got, err := l.Withdraw("alice")
 43	uassert.NoError(t, err)
 44	uassert.Equal(t, int64(100), got)
 45	uassert.Equal(t, int64(0), l.Balance("alice"), "the balance is gone before the caller transfers")
 46	uassert.Equal(t, int64(0), l.TotalOwed())
 47	uassert.Equal(t, int64(100), l.TotalWithdrawn())
 48	uassert.True(t, l.Consistent())
 49}
 50
 51// TestReentrantWithdrawGetsNothing is the security property the pattern exists
 52// for: the second entry finds the balance already zeroed.
 53func TestReentrantWithdrawGetsNothing(t *testing.T) {
 54	l := New()
 55	l.Credit("attacker", 1000)
 56
 57	first, err := l.Withdraw("attacker")
 58	uassert.NoError(t, err)
 59	uassert.Equal(t, int64(1000), first)
 60
 61	// Simulating the reentrant call the recipient would make from its handler,
 62	// BEFORE the outer caller has transferred anything.
 63	second, err := l.Withdraw("attacker")
 64	uassert.ErrorIs(t, err, ErrNothing, "a reentrant withdrawal must find nothing")
 65	uassert.Equal(t, int64(0), second)
 66	uassert.Equal(t, int64(1000), l.TotalWithdrawn(), "and must not be counted twice")
 67}
 68
 69func TestWithdrawUnknownPayee(t *testing.T) {
 70	l := New()
 71	_, err := l.Withdraw("nobody")
 72	uassert.ErrorIs(t, err, ErrNothing)
 73}
 74
 75func TestForfeit(t *testing.T) {
 76	l := New()
 77	l.Credit("alice", 100)
 78	l.Credit("bob", 40)
 79
 80	dropped, err := l.Forfeit("alice")
 81	uassert.NoError(t, err)
 82	uassert.Equal(t, int64(100), dropped)
 83	uassert.Equal(t, int64(40), l.TotalOwed())
 84	uassert.Equal(t, int64(0), l.TotalWithdrawn(), "a forfeit is not a withdrawal")
 85	uassert.True(t, l.Consistent())
 86
 87	_, err = l.Forfeit("alice")
 88	uassert.ErrorIs(t, err, ErrNothing)
 89}
 90
 91func TestPayeesSorted(t *testing.T) {
 92	l := New()
 93	l.Credit("carol", 3)
 94	l.Credit("alice", 1)
 95	l.Credit("bob", 2)
 96
 97	uassert.Equal(t, "alice,bob,carol", strings.Join(l.Payees(), ","))
 98}
 99
100func TestIterateSortedAndStops(t *testing.T) {
101	l := New()
102	l.Credit("b", 2)
103	l.Credit("a", 1)
104
105	var seen []string
106	l.Iterate(func(p string, amt int64) bool {
107		seen = append(seen, p+":"+strconv.FormatInt(amt, 10))
108		return false
109	})
110	uassert.Equal(t, "a:1,b:2", strings.Join(seen, ","))
111
112	n := 0
113	l.Iterate(func(p string, amt int64) bool { n++; return true })
114	uassert.Equal(t, 1, n)
115}
116
117func TestCreditMany(t *testing.T) {
118	l := New()
119	err := l.CreditMany([]string{"a", "b", "c"}, []int64{10, 20, 30})
120	uassert.NoError(t, err)
121
122	uassert.Equal(t, int64(10), l.Balance("a"))
123	uassert.Equal(t, int64(30), l.Balance("c"))
124	uassert.Equal(t, int64(60), l.TotalOwed())
125	uassert.True(t, l.Consistent())
126}
127
128// TestCreditManyIsAllOrNothing pins that a bad entry applies none of the split.
129func TestCreditManyIsAllOrNothing(t *testing.T) {
130	l := New()
131	l.Credit("existing", 5)
132
133	err := l.CreditMany([]string{"a", "b"}, []int64{10, -1})
134	uassert.ErrorIs(t, err, ErrBadAmount)
135
136	uassert.Equal(t, int64(0), l.Balance("a"), "the valid entry must not be applied either")
137	uassert.Equal(t, int64(5), l.TotalOwed(), "the ledger is untouched")
138	uassert.Equal(t, 1, l.Count())
139	uassert.True(t, l.Consistent())
140}
141
142func TestCreditManyMismatchedLengths(t *testing.T) {
143	l := New()
144	uassert.ErrorIs(t, l.CreditMany([]string{"a"}, []int64{1, 2}), ErrBadAmount)
145	uassert.True(t, l.IsEmpty())
146}
147
148func TestCreditManyAccumulates(t *testing.T) {
149	l := New()
150	l.Credit("a", 5)
151	uassert.NoError(t, l.CreditMany([]string{"a", "b"}, []int64{10, 1}))
152	uassert.Equal(t, int64(15), l.Balance("a"), "adds to the existing balance")
153	uassert.Equal(t, int64(16), l.TotalOwed())
154}
155
156func TestOverflowGuard(t *testing.T) {
157	l := New()
158	uassert.NoError(t, l.Credit("whale", maxInt64))
159	uassert.ErrorIs(t, l.Credit("whale", 1), ErrOverflow)
160	uassert.ErrorIs(t, l.Credit("other", 1), ErrOverflow, "the total would overflow too")
161	uassert.Equal(t, maxInt64, l.TotalOwed(), "a refused credit changes nothing")
162	uassert.True(t, l.Consistent())
163}
164
165func TestCreditManyOverflowGuard(t *testing.T) {
166	l := New()
167	l.Credit("a", maxInt64-5)
168	uassert.ErrorIs(t, l.CreditMany([]string{"b", "c"}, []int64{10, 10}), ErrOverflow)
169	uassert.Equal(t, int64(0), l.Balance("b"), "nothing applied")
170}
171
172func TestMaxPayees(t *testing.T) {
173	l := New()
174	for i := 0; i < MaxPayees; i++ {
175		uassert.NoError(t, l.Credit("p"+strconv.Itoa(i), 1))
176	}
177	uassert.Equal(t, MaxPayees, l.Count())
178
179	uassert.ErrorIs(t, l.Credit("overflow", 1), ErrFull, "a full ledger refuses a new payee")
180	uassert.NoError(t, l.Credit("p0", 1), "existing payees can still be credited")
181	uassert.Equal(t, int64(2), l.Balance("p0"))
182}
183
184func TestCreditManyRespectsCapacity(t *testing.T) {
185	l := New()
186	for i := 0; i < MaxPayees-1; i++ {
187		l.Credit("p"+strconv.Itoa(i), 1)
188	}
189	uassert.ErrorIs(t, l.CreditMany([]string{"x", "y"}, []int64{1, 1}), ErrFull,
190		"the batch would exceed capacity, so none of it applies")
191	uassert.Equal(t, int64(0), l.Balance("x"))
192}
193
194// TestFullLifecycle walks a realistic split-then-claim sequence.
195func TestFullLifecycle(t *testing.T) {
196	l := New()
197	uassert.NoError(t, l.CreditMany([]string{"alice", "bob", "carol"}, []int64{500, 300, 200}))
198	uassert.Equal(t, int64(1000), l.TotalOwed(), "the realm must hold 1000 in reserve")
199
200	got, _ := l.Withdraw("bob")
201	uassert.Equal(t, int64(300), got)
202	uassert.Equal(t, int64(700), l.TotalOwed(), "reserve drops by exactly what was paid")
203
204	l.Credit("alice", 100)
205	got, _ = l.Withdraw("alice")
206	uassert.Equal(t, int64(600), got, "a later credit is included in one withdrawal")
207
208	uassert.Equal(t, int64(200), l.TotalOwed())
209	uassert.Equal(t, int64(900), l.TotalWithdrawn())
210	uassert.Equal(t, "carol", strings.Join(l.Payees(), ","))
211	uassert.True(t, l.Consistent())
212}