lists_test.gno
11.74 Kb · 301 lines
1package bolo
2
3import (
4 "math"
5 "strings"
6 "testing"
7
8 "gno.land/p/nt/seqid/v0"
9 "gno.land/p/nt/testutils/v0"
10)
11
12func TestPageSize(t *testing.T) {
13 for size, want := range map[int]int{math.MinInt: 1, -5: 1, 0: 1, 1: 1, 20: 20, 50: 50, 51: 50, math.MaxInt: 50} {
14 if got := pageSize(size); got != want {
15 t.Errorf("pageSize(%d) = %d, want %d", size, got, want)
16 }
17 }
18}
19
20func TestPageOffset(t *testing.T) {
21 for _, k := range []struct {
22 page, size, n, want int
23 ok bool
24 }{
25 {1, 10, 25, 0, true},
26 {3, 10, 25, 20, true},
27 {0, 10, 25, 0, true}, // below 1 reads as 1
28 {-7, 10, 25, 0, true},
29 {4, 10, 25, 0, false}, // past the end
30 {math.MaxInt, 50, 25, 0, false},
31 {1, 10, 0, 0, true}, // an empty tree has an empty first page
32 } {
33 got, ok := pageOffset(k.page, k.size, k.n)
34 if got != k.want || ok != k.ok {
35 t.Errorf("pageOffset(%d, %d, %d) = %d, %t; want %d, %t", k.page, k.size, k.n, got, ok, k.want, k.ok)
36 }
37 }
38}
39
40// indexCampaigns stores n campaigns, IDs first to first+n-1, as CreateCampaign leaves
41// them — in campaigns and every campaign index — organized by org for ben, in the
42// category "animals", and returns them with what removes them again.
43func indexCampaigns(first seqid.ID, n int, org, ben address) ([]*campaign, func()) {
44 var cs []*campaign
45 for i := 0; i < n; i++ {
46 c := testCampaign(first+seqid.ID(i), modeEscrow, 0)
47 c.category, c.organizer, c.beneficiary = "animals", org, ben
48 key := c.id.Binary()
49 campaigns.Set(key, c)
50 addActive(c)
51 byOrganizer.Set(org.String()+"/"+key, true)
52 byBeneficiary.Set(ben.String()+"/"+key, true)
53 cs = append(cs, c)
54 }
55 return cs, func() {
56 for _, c := range cs {
57 key := c.id.Binary()
58 campaigns.Remove(key)
59 dropActive(c)
60 byOrganizer.Remove(c.organizer.String() + "/" + key)
61 byBeneficiary.Remove(c.beneficiary.String() + "/" + key)
62 }
63 }
64}
65
66// pageAll follows a list's cursor to its end, at most 100 calls, and returns every
67// ID seen, in order, with the number of calls.
68func pageAll(list func(after string) ([]CampaignView, string)) ([]string, int) {
69 var ids []string
70 after, calls := "", 0
71 for calls < 100 {
72 views, next := list(after)
73 calls++
74 for _, v := range views {
75 ids = append(ids, v.ID)
76 }
77 if next == "" {
78 break
79 }
80 after = next
81 }
82 return ids, calls
83}
84
85// TestListCursor: a list paged to its end by cursor returns every campaign once,
86// newest first; next is "" exactly at the end (API §4).
87func TestListCursor(t *testing.T) {
88 org, ben := testutils.TestAddress("listorg"), testutils.TestAddress("listben")
89 _, unindex := indexCampaigns(9901, 7, org, ben)
90 defer unindex()
91 want := "9907 9906 9905 9904 9903 9902 9901"
92 for name, list := range map[string]func(string) ([]CampaignView, string){
93 "ListCampaigns": func(a string) ([]CampaignView, string) { return ListCampaigns(a, 3, "animals") },
94 "ListByOrganizer": func(a string) ([]CampaignView, string) { return ListByOrganizer(org, a, 3) },
95 "ListByBeneficiary": func(a string) ([]CampaignView, string) { return ListByBeneficiary(ben, a, 3) },
96 } {
97 ids, calls := pageAll(list)
98 if got := strings.Join(ids, " "); got != want || calls != 3 {
99 t.Errorf("%s: %q in %d calls, want %q in 3", name, got, calls, want)
100 }
101 }
102 // A full last page knows it is last: the list peeks one entry further.
103 if views, next := ListByOrganizer(org, "9904", 3); len(views) != 3 || next != "" {
104 t.Errorf("the last full page: %d views, next %q", len(views), next)
105 }
106 if views, next := ListByOrganizer(org, "", 3); len(views) != 3 || next != "9905" {
107 t.Errorf("the first page: %d views, next %q", len(views), next)
108 }
109 for _, after := range []string{"042", "+42", " 42", "x", "9901/1"} {
110 if !panicsWith(func() { ListByOrganizer(org, after, 3) }, ErrNotFound) {
111 t.Errorf("after %q: not ErrNotFound", after)
112 }
113 }
114}
115
116// TestListCampaignsPassed (Q6): a campaign past a deadline nobody recorded is left
117// out; one call reads at most four times size entries, so a page can come back short
118// or empty while next isn't "", and no campaign repeats or goes missing.
119func TestListCampaignsPassed(t *testing.T) {
120 org, ben := testutils.TestAddress("listorg"), testutils.TestAddress("listben")
121 cs, unindex := indexCampaigns(9911, 9, org, ben)
122 defer unindex()
123 for _, c := range cs[2:7] { // 9913–9917 are past their deadline, unrecorded
124 c.deadline = now() - 1
125 }
126 if views, next := ListCampaigns("9918", 1, "animals"); len(views) != 0 || next != "9914" {
127 t.Errorf("after 9918, size 1: %d views, next %q; want none, read up to 9914", len(views), next)
128 }
129 ids, _ := pageAll(func(a string) ([]CampaignView, string) { return ListCampaigns(a, 1, "animals") })
130 if got := strings.Join(ids, " "); got != "9919 9918 9912 9911" {
131 t.Errorf("paged to the end: %q", got)
132 }
133 // The other lists keep every campaign, closed and past a deadline included.
134 ids, _ = pageAll(func(a string) ([]CampaignView, string) { return ListByOrganizer(org, a, 50) })
135 if len(ids) != 9 {
136 t.Errorf("ListByOrganizer: %d campaigns, want 9", len(ids))
137 }
138}
139
140// TestListRanges: an address's range stops at the "/" after it, so an address that is
141// the start of a real one finds nothing (C12); a delisted campaign stays on its
142// organizer's and beneficiary's lists, off ListCampaigns; ListByDonor holds only the
143// campaigns an address gave to without anonymity.
144func TestListRanges(t *testing.T) {
145 org, ben := testutils.TestAddress("listorg"), testutils.TestAddress("listben")
146 cs, unindex := indexCampaigns(9921, 3, org, ben)
147 defer unindex()
148 if views, next := ListByOrganizer(address(org.String()[:20]), "", 50); len(views) != 0 || next != "" {
149 t.Errorf("a prefix of the organizer's address: %d views", len(views))
150 }
151 if views, _ := ListByOrganizer(ben, "", 50); len(views) != 0 {
152 t.Errorf("the beneficiary as organizer: %d views", len(views))
153 }
154
155 delisted := cs[1]
156 delisted.state.listed = false
157 dropActive(delisted)
158 defer addActive(delisted) // unindex drops it again
159 if views, _ := ListCampaigns("", 50, "animals"); len(views) != 2 {
160 t.Errorf("ListCampaigns with one delisted: %d views", len(views))
161 }
162 for name, views := range map[string][]CampaignView{
163 "ListByOrganizer": first(ListByOrganizer(org, "", 50)),
164 "ListByBeneficiary": first(ListByBeneficiary(ben, "", 50)),
165 } {
166 if len(views) != 3 || views[1].Listed || views[1].ID != "9922" {
167 t.Errorf("%s: %d views, the delisted one %v", name, len(views), views[1].Listed)
168 }
169 }
170
171 public, anonymous := testutils.TestAddress("listpublic"), testutils.TestAddress("listanon")
172 key := public.String() + "/" + cs[0].id.Binary()
173 byDonor.Set(key, true)
174 defer byDonor.Remove(key)
175 if views, _ := ListByDonor(public, "", 50); len(views) != 1 || views[0].ID != "9921" {
176 t.Errorf("ListByDonor, a public donor: %d views", len(views))
177 }
178 if views, _ := ListByDonor(anonymous, "", 50); len(views) != 0 {
179 t.Errorf("ListByDonor, a donor whose gifts were all anonymous: %d views", len(views))
180 }
181}
182
183func first(views []CampaignView, _ string) []CampaignView { return views }
184
185// TestCampaignLists: a campaign's receipts, withdrawals, refunds and updates, newest
186// first, by page number; a refund's donor blanked when all their gifts were anonymous.
187func TestCampaignLists(t *testing.T) {
188 c := testCampaign(9931, modeEscrow, 0)
189 id, unstore := storeCampaign(c)
190 defer unstore()
191 public, anonymous := testutils.TestAddress("listpublic"), testutils.TestAddress("listanon")
192 c.contributions.Set(public.String(), &contribution{amount: 30, public: true})
193 c.contributions.Set(anonymous.String(), &contribution{amount: 20})
194 for i := 1; i <= 5; i++ {
195 seq := seqid.ID(i)
196 c.receipts.Set(seq.Binary(), &receipt{seq: uint64(i), donor: public, amount: int64(i)})
197 c.withdrawals.Set(seq.Binary(), &withdrawal{seq: uint64(i), amount: int64(10 * i)})
198 c.updates.Set(seq.Binary(), &update{seq: uint64(i), body: "update " + itoa(int64(i))})
199 }
200 c.refunds.Set(seqid.ID(1).Binary(), &refund{seq: 1, amount: 3, donor: public, kind: windowOpen})
201 c.refunds.Set(seqid.ID(2).Binary(), &refund{seq: 2, amount: 2, donor: anonymous, kind: windowOpen})
202
203 seqs := func(n []int64) string {
204 var out []string
205 for _, s := range n {
206 out = append(out, itoa(s))
207 }
208 return strings.Join(out, " ")
209 }
210 receipts := func(page, size int) string {
211 var n []int64
212 for _, v := range ListReceipts(id, page, size) {
213 n = append(n, v.Seq)
214 }
215 return seqs(n)
216 }
217 for _, k := range []struct {
218 page, size int
219 want string
220 }{
221 {1, 2, "5 4"}, {2, 2, "3 2"}, {3, 2, "1"}, {4, 2, ""}, {0, 2, "5 4"}, {-1, 2, "5 4"},
222 {1, 0, "5"}, {1, 1000, "5 4 3 2 1"}, {math.MaxInt, 2, ""},
223 } {
224 if got := receipts(k.page, k.size); got != k.want {
225 t.Errorf("ListReceipts(page %d, size %d) = %q, want %q", k.page, k.size, got, k.want)
226 }
227 }
228 if w := ListWithdrawals(id, 1, 2); len(w) != 2 || w[0].Seq != 5 || w[0].Amount != 50 {
229 t.Errorf("ListWithdrawals: %v", w)
230 }
231 if u := ListUpdates(id, 2, 3); len(u) != 2 || u[0].Seq != 2 || u[0].Body != "update 2" {
232 t.Errorf("ListUpdates, page 2: %v", u)
233 }
234 r := ListRefunds(id, 1, 50)
235 if len(r) != 2 || r[0].Seq != 2 || r[0].Donor != "" || r[1].Donor != public || r[1].Kind != windowOpen {
236 t.Errorf("ListRefunds: %v", r)
237 }
238}
239
240// TestGetStats reads the counters the writes keep (API §2.4); the writes are counted
241// in z_stats_filetest.gno.
242func TestGetStats(t *testing.T) {
243 saved := stats
244 defer func() { stats = saved }()
245 stats = statsState{open: 3, settled: 2}
246 s := GetStats()
247 if s.Campaigns != campaigns.Size() || s.Open != 3 || s.Settled != 2 {
248 t.Errorf("GetStats: %v", s)
249 }
250}
251
252// TestListCrafted: an address or a category holding "/" can't make a list read a key
253// that isn't one of its own entries — it finds nothing, and never panics unkeyed.
254func TestListCrafted(t *testing.T) {
255 org, ben := testutils.TestAddress("listorg"), testutils.TestAddress("listben")
256 cs, unindex := indexCampaigns(0x2f00, 1, org, ben) // its key's seventh byte is "/"
257 defer unindex()
258 bin := cs[0].id.Binary()
259 crafted := address(org.String() + "/" + bin[:6])
260 if views, next := ListByOrganizer(crafted, "", 50); len(views) != 0 || next != "" {
261 t.Errorf("an address ending in a key's first bytes: %d views", len(views))
262 }
263 if views, _ := ListCampaigns("", 50, "animals/"+bin[:6]); len(views) != 0 {
264 t.Errorf("a category ending in a key's first bytes: %d views", len(views))
265 }
266}
267
268// TestListCraftedAddress: an address or a category ending in "/" and a key's bytes
269// names nothing, so its list comes back empty (C12). Campaigns 47 and 48 end their keys
270// in "/" and "0", which a reverse traversal's inclusive end would reach.
271func TestListCraftedAddress(t *testing.T) {
272 org, ben := testutils.TestAddress("listorg"), testutils.TestAddress("listben")
273 _, unindex := indexCampaigns(47, 2, org, ben)
274 defer unindex()
275 tail := "/\x00\x00\x00\x00\x00\x00\x00"
276 for name, list := range map[string]func() ([]CampaignView, string){
277 "organizer": func() ([]CampaignView, string) { return ListByOrganizer(address(org.String()+tail), "", 50) },
278 "beneficiary": func() ([]CampaignView, string) { return ListByBeneficiary(address(ben.String()+tail), "", 50) },
279 "category": func() ([]CampaignView, string) { return ListCampaigns("", 50, "animals"+tail) },
280 } {
281 if views, next := list(); len(views) != 0 || next != "" {
282 t.Errorf("%s: %d views, next %q", name, len(views), next)
283 }
284 }
285}
286
287// TestListRangeStops: an organizer's list holds its own campaigns only, though another
288// organizer's entries sort right after them.
289func TestListRangeStops(t *testing.T) {
290 org, later := testutils.TestAddress("listorg"), testutils.TestAddress("listpub")
291 if org.String() >= later.String() {
292 t.Fatalf("fixture: %s must sort before %s", org, later)
293 }
294 _, unindex := indexCampaigns(9951, 2, org, org)
295 defer unindex()
296 _, unindex2 := indexCampaigns(9953, 2, later, later)
297 defer unindex2()
298 if views, next := ListByOrganizer(org, "", 50); len(views) != 2 || next != "" {
299 t.Errorf("ListByOrganizer: %d views, next %q; want 2, \"\"", len(views), next)
300 }
301}