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}