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.

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}