views_test.gno
5.32 Kb · 140 lines
1package bolo
2
3import (
4 "chain"
5 "testing"
6
7 "gno.land/p/nt/avl/v0"
8 "gno.land/p/nt/seqid/v0"
9 "gno.land/p/nt/testutils/v0"
10)
11
12// testCampaign builds a campaign in memory, stored nowhere: views and the deadline
13// helper read only what they are given.
14func testCampaign(id seqid.ID, mode string, deadline int64) *campaign {
15 return &campaign{
16 id: id, category: "water", mode: mode, deadline: deadline, createdAt: now(),
17 organizer: testutils.TestAddress("organizer"), beneficiary: testutils.TestAddress("beneficiary"),
18 params: Params{1_000_000, 90 * secondsPerDay, 60 * secondsPerDay, 30 * secondsPerDay},
19 text: &campaignText{title: "Clean water"},
20 state: &campaignState{status: statusOpen, listed: true},
21 contributions: avl.NewTree(), receipts: avl.NewTree(), withdrawals: avl.NewTree(),
22 refunds: avl.NewTree(), updates: avl.NewTree(),
23 }
24}
25
26const heightsPerDay = 17280 // five seconds a height (spike S10)
27
28func TestRecordDeadline(cur realm, t *testing.T) {
29 c := testCampaign(9001, modeEscrow, now()+secondsPerDay)
30 key := c.id.Binary()
31 active.Set(key, true)
32 activeByCategory.Set("water/"+key, true)
33 defer dropActive(c)
34
35 recordDeadline(cur, c)
36 if c.state.status != statusOpen || !active.Has(key) {
37 t.Fatalf("closed before its deadline")
38 }
39 testing.SkipHeights(2 * heightsPerDay)
40 recordDeadline(cur, c)
41 s := c.state
42 if s.status != statusClosed || s.closedAt != c.deadline || s.closeReason != reasonDeadline {
43 t.Errorf("after the deadline: %s at %d (%s), want closed at the deadline %d", s.status, s.closedAt, s.closeReason, c.deadline)
44 }
45 if active.Has(key) || activeByCategory.Has("water/"+key) {
46 t.Errorf("a closed campaign is still in the active indexes")
47 }
48 // A second call changes nothing.
49 s.closeReason = reasonOrganizer
50 recordDeadline(cur, c)
51 if s.closeReason != reasonOrganizer {
52 t.Errorf("recordDeadline rewrote a closed campaign")
53 }
54}
55
56func TestCampaignView(t *testing.T) {
57 // Live Params unlike every test campaign's snapshot: a view reading them would
58 // show it (INV-5).
59 defer func(p Params) { params = p }(params)
60 params = Params{CreationDeposit: 7, DormancyPeriod: 1, ExitDelay: 2, BondHold: 3}
61
62 // Open, no deadline: only dormancy has a time.
63 c := testCampaign(9002, modeEscrow, 0)
64 c.state.lastDonationAt = now() + 5
65 v := campaignView(c)
66 if v.Status != statusOpen || v.ClosableByAnyoneAt != now()+5+c.params.DormancyPeriod ||
67 v.FinalizableAt != 0 || v.ExitOpensAt != 0 || v.BondReleasableAt != 0 || v.ExitOpen {
68 t.Errorf("open, no deadline: %v", v)
69 }
70
71 // Open with a deadline: finalizable at the deadline while nothing is tracked,
72 // once the exit opens when something is.
73 d := testCampaign(9003, modeEscrow, now()+10*secondsPerDay)
74 if v := campaignView(d); v.FinalizableAt != d.deadline {
75 t.Errorf("open, nothing tracked: FinalizableAt = %d, want %d", v.FinalizableAt, d.deadline)
76 }
77 d.state.raised = 500
78 if v := campaignView(d); v.Balance != 500 || v.NetRaised != 500 || v.FinalizableAt != d.deadline+d.params.ExitDelay {
79 t.Errorf("open, 500 tracked: %v", v)
80 }
81
82 // Past the deadline: closed at it, without a write.
83 testing.SkipHeights(11 * heightsPerDay)
84 v = campaignView(d)
85 if v.Status != statusClosed || v.CloseReason != reasonDeadline || v.ClosedAt != d.deadline {
86 t.Errorf("past the deadline: %s, %s, %d", v.Status, v.CloseReason, v.ClosedAt)
87 }
88 if d.state.status != statusOpen {
89 t.Errorf("a view wrote the close")
90 }
91 if v.ClosableByAnyoneAt != 0 || v.BondReleasableAt != d.deadline+d.params.BondHold ||
92 v.ExitOpensAt != d.deadline+d.params.ExitDelay || v.FinalizableAt != v.ExitOpensAt || v.ExitOpen {
93 t.Errorf("closed, never withdrawn: %v", v)
94 }
95
96 // The exit window opens only after a withdrawal, once its delay has passed.
97 d.state.everWithdrawn = true
98 if campaignView(d).ExitOpen {
99 t.Errorf("exit open before its delay")
100 }
101 testing.SkipHeights(61 * heightsPerDay)
102 if !campaignView(d).ExitOpen {
103 t.Errorf("exit not open after its delay")
104 }
105 d.state.everWithdrawn = false
106 if campaignView(d).ExitOpen {
107 t.Errorf("exit open after its delay without any withdrawal")
108 }
109 // Nor once everything tracked was withdrawn: there is nothing to reclaim.
110 d.state.everWithdrawn, d.state.withdrawn = true, d.state.raised
111 if campaignView(d).ExitOpen {
112 t.Errorf("exit open on an emptied escrow")
113 }
114
115 // Direct mode: nothing tracked, no exit, finalizable at the close.
116 e := testCampaign(9004, modeDirect, now()+secondsPerDay)
117 e.state.raised = 700
118 testing.SkipHeights(2 * heightsPerDay)
119 if v := campaignView(e); v.Balance != 0 || v.NetRaised != 700 || v.ExitOpensAt != 0 || v.FinalizableAt != e.deadline {
120 t.Errorf("direct, closed: %v", v)
121 }
122
123 // Settled: nothing left to finalize; the bond's release time stays.
124 e.state.status, e.state.closedAt, e.state.closeReason = statusSettled, e.deadline, reasonDeadline
125 if v := campaignView(e); v.FinalizableAt != 0 || v.BondReleasableAt != e.deadline+e.params.BondHold {
126 t.Errorf("settled: %v", v)
127 }
128
129 // Untracked: what the escrow holds beyond the books, never below 0.
130 f := testCampaign(9005, modeEscrow, 0)
131 f.state.raised = 300
132 testing.IssueCoins(escrowAddr(f.id), chain.NewCoins(chain.NewCoin("ugnot", 1000)))
133 if v := campaignView(f); v.Untracked != 700 {
134 t.Errorf("Untracked = %d, want 700", v.Untracked)
135 }
136 f.state.raised = 5000
137 if v := campaignView(f); v.Untracked != 0 {
138 t.Errorf("Untracked = %d, want 0 when the books exceed the bank", v.Untracked)
139 }
140}