token_test.gno
8.79 Kb · 299 lines
1package enumerable
2
3import (
4 "testing"
5
6 "gno.land/p/nt/grc721/v0"
7 "gno.land/p/nt/seqid/v0"
8 "gno.land/p/nt/testutils/v0"
9 "gno.land/p/nt/uassert/v0"
10 "gno.land/p/nt/urequire/v0"
11)
12
13// Same-realm cross: NewToken requires rlm.IsCurrent().
14func newCore(name, symbol string, id seqid.ID, rlm realm) (tok *grc721.Token, led *grc721.PrivateLedger) {
15 func(cur realm) {
16 tok, led = grc721.NewToken(name, symbol, id, cur)
17 }(cross(rlm))
18
19 return
20}
21
22func newEnum(cur realm) (enum *Enumerable, core *grc721.PrivateLedger, led *Ledger) {
23 _, coreLedger := newCore("Foo", "FOO", 0, cur)
24 enum, led = NewEnumerable(coreLedger)
25 return enum, coreLedger, led
26}
27
28func globalOrder(t *testing.T, enum *Enumerable) []string {
29 t.Helper()
30 out := []string{}
31 for i := int64(0); i < enum.TotalSupply(); i++ {
32 tid, err := enum.TokenByIndex(i)
33 urequire.NoError(t, err)
34 out = append(out, tid.String())
35 }
36 return out
37}
38
39func ownerOrder(t *testing.T, enum *Enumerable, owner address) []string {
40 t.Helper()
41 out := []string{}
42 for i := int64(0); ; i++ {
43 tid, err := enum.TokenOfOwnerByIndex(owner, i)
44 if err != nil {
45 break
46 }
47 out = append(out, tid.String())
48 }
49 return out
50}
51
52func eqStrings(t *testing.T, name string, got, want []string) {
53 t.Helper()
54 if len(got) != len(want) {
55 t.Errorf("%s: length got %d want %d (%v vs %v)", name, len(got), len(want), got, want)
56 return
57 }
58 for i := range want {
59 uassert.Equal(t, want[i], got[i])
60 }
61}
62
63func TestNewEnumerable(cur realm, t *testing.T) {
64 uassert.PanicsWithMessage(t, cur, "enumerable: nil core ledger", func() {
65 NewEnumerable(nil)
66 })
67
68 tok, coreLedger := newCore("Foo", "FOO", 0, cur)
69 enum, led := NewEnumerable(coreLedger)
70
71 urequire.True(t, enum != nil, "enum built")
72 urequire.True(t, led != nil, "ledger built")
73
74 tests := []struct {
75 name string
76 got string
77 want string
78 }{
79 {"ExtensionKind is enumerable", enum.ExtensionKind(), Kind},
80 {"ExtensionKind constant value", Kind, "enumerable"},
81 {"TokenID matches core token id", enum.TokenID(), tok.ID()},
82 }
83 for _, tt := range tests {
84 t.Run(tt.name, func(t *testing.T) {
85 uassert.Equal(t, tt.want, tt.got)
86 })
87 }
88
89 uassert.Equal(t, int64(0), enum.TotalSupply())
90}
91
92func TestReadIndices(cur realm, t *testing.T) {
93 alice := testutils.TestAddress("alice")
94 bob := testutils.TestAddress("bob")
95 carol := testutils.TestAddress("carol")
96
97 enum, coreLedger, _ := newEnum(cur)
98 urequire.NoError(t, coreLedger.Mint(alice, "1"))
99 urequire.NoError(t, coreLedger.Mint(alice, "2"))
100 urequire.NoError(t, coreLedger.Mint(bob, "3"))
101
102 uassert.Equal(t, int64(3), enum.TotalSupply())
103
104 tests := []struct {
105 name string
106 run func(t *testing.T)
107 }{
108 {"TokenByIndex 0 returns first minted", func(t *testing.T) {
109 tid, err := enum.TokenByIndex(0)
110 urequire.NoError(t, err)
111 uassert.Equal(t, "1", tid.String())
112 }},
113 {"TokenByIndex last returns last minted", func(t *testing.T) {
114 tid, err := enum.TokenByIndex(2)
115 urequire.NoError(t, err)
116 uassert.Equal(t, "3", tid.String())
117 }},
118 {"TokenByIndex negative returns ErrIndexOutOfRange", func(t *testing.T) {
119 _, err := enum.TokenByIndex(-1)
120 uassert.ErrorIs(t, err, ErrIndexOutOfRange)
121 }},
122 {"TokenByIndex out of range returns ErrIndexOutOfRange", func(t *testing.T) {
123 _, err := enum.TokenByIndex(3)
124 uassert.ErrorIs(t, err, ErrIndexOutOfRange)
125 }},
126 {"TokenOfOwnerByIndex returns owner's token", func(t *testing.T) {
127 tid, err := enum.TokenOfOwnerByIndex(alice, 1)
128 urequire.NoError(t, err)
129 uassert.Equal(t, "2", tid.String())
130 }},
131 {"TokenOfOwnerByIndex unknown owner returns ErrIndexOutOfRange", func(t *testing.T) {
132 _, err := enum.TokenOfOwnerByIndex(carol, 0)
133 uassert.ErrorIs(t, err, ErrIndexOutOfRange)
134 }},
135 {"TokenOfOwnerByIndex negative index returns ErrIndexOutOfRange", func(t *testing.T) {
136 _, err := enum.TokenOfOwnerByIndex(alice, -1)
137 uassert.ErrorIs(t, err, ErrIndexOutOfRange)
138 }},
139 {"TokenOfOwnerByIndex past end returns ErrIndexOutOfRange", func(t *testing.T) {
140 _, err := enum.TokenOfOwnerByIndex(alice, 2)
141 uassert.ErrorIs(t, err, ErrIndexOutOfRange)
142 }},
143 }
144 for _, tt := range tests {
145 t.Run(tt.name, tt.run)
146 }
147
148 eqStrings(t, "global order", globalOrder(t, enum), []string{"1", "2", "3"})
149 eqStrings(t, "alice order", ownerOrder(t, enum, alice), []string{"1", "2"})
150 eqStrings(t, "bob order", ownerOrder(t, enum, bob), []string{"3"})
151}
152
153func TestBurnGlobalSwapAndPop(cur realm, t *testing.T) {
154 alice := testutils.TestAddress("alice")
155
156 tests := []struct {
157 name string
158 burn grc721.TokenID
159 wantOrder []string
160 }{
161 {"burn last token truncates global list (idx == last)", "3", []string{"1", "2"}},
162 {"burn middle token swap-and-pops the global list (idx != last)", "2", []string{"1", "3"}},
163 {"burn first token swap-and-pops the global list (idx != last)", "1", []string{"3", "2"}},
164 }
165 for _, tt := range tests {
166 t.Run(tt.name, func(t *testing.T) {
167 enum, coreLedger, _ := newEnum(cur)
168 urequire.NoError(t, coreLedger.Mint(alice, "1"))
169 urequire.NoError(t, coreLedger.Mint(alice, "2"))
170 urequire.NoError(t, coreLedger.Mint(alice, "3"))
171
172 urequire.NoError(t, coreLedger.Burn(tt.burn))
173
174 uassert.Equal(t, int64(2), enum.TotalSupply())
175 eqStrings(t, tt.name, globalOrder(t, enum), tt.wantOrder)
176 })
177 }
178}
179
180func TestBurnOwnerSwapAndPop(cur realm, t *testing.T) {
181 alice := testutils.TestAddress("alice")
182
183 tests := []struct {
184 name string
185 mint []grc721.TokenID
186 burn grc721.TokenID
187 wantOwner []string
188 ownerEmpty bool
189 }{
190 {
191 name: "burn first of owner swap-and-pops per-owner list (idx != last)",
192 mint: []grc721.TokenID{"1", "2", "3"},
193 burn: "1",
194 wantOwner: []string{"3", "2"},
195 },
196 {
197 name: "burn middle of owner swap-and-pops per-owner list (idx != last)",
198 mint: []grc721.TokenID{"1", "2", "3"},
199 burn: "2",
200 wantOwner: []string{"1", "3"},
201 },
202 {
203 name: "burn last of owner truncates per-owner list (idx == last)",
204 mint: []grc721.TokenID{"1", "2", "3"},
205 burn: "3",
206 wantOwner: []string{"1", "2"},
207 },
208 {
209 name: "burn only token empties per-owner list (owned.Remove)",
210 mint: []grc721.TokenID{"1"},
211 burn: "1",
212 ownerEmpty: true,
213 },
214 }
215 for _, tt := range tests {
216 t.Run(tt.name, func(t *testing.T) {
217 enum, coreLedger, _ := newEnum(cur)
218 for _, tid := range tt.mint {
219 urequire.NoError(t, coreLedger.Mint(alice, tid))
220 }
221
222 urequire.NoError(t, coreLedger.Burn(tt.burn))
223
224 if tt.ownerEmpty {
225 _, err := enum.TokenOfOwnerByIndex(alice, 0)
226 uassert.ErrorIs(t, err, ErrIndexOutOfRange)
227 return
228 }
229 eqStrings(t, tt.name, ownerOrder(t, enum, alice), tt.wantOwner)
230 })
231 }
232}
233
234func TestTransfer(cur realm, t *testing.T) {
235 alice := testutils.TestAddress("alice")
236 bob := testutils.TestAddress("bob")
237
238 enum, coreLedger, _ := newEnum(cur)
239 urequire.NoError(t, coreLedger.Mint(alice, "1"))
240 urequire.NoError(t, coreLedger.Mint(alice, "2"))
241 urequire.NoError(t, coreLedger.Mint(bob, "3"))
242
243 tests := []struct {
244 name string
245 run func(t *testing.T)
246 }{
247 {"transfer 1 alice->bob swap-and-pops alice list", func(t *testing.T) {
248 urequire.NoError(t, coreLedger.TransferFrom(alice, alice, bob, "1"))
249 eqStrings(t, "alice after transfer", ownerOrder(t, enum, alice), []string{"2"})
250 eqStrings(t, "bob after transfer", ownerOrder(t, enum, bob), []string{"3", "1"})
251 }},
252 {"transfer 2 alice->bob empties alice list", func(t *testing.T) {
253 urequire.NoError(t, coreLedger.TransferFrom(alice, alice, bob, "2"))
254 _, err := enum.TokenOfOwnerByIndex(alice, 0)
255 uassert.ErrorIs(t, err, ErrIndexOutOfRange)
256 eqStrings(t, "bob owns all", ownerOrder(t, enum, bob), []string{"3", "1", "2"})
257 }},
258 {"global supply unchanged by transfers", func(t *testing.T) {
259 uassert.Equal(t, int64(3), enum.TotalSupply())
260 }},
261 }
262 for _, tt := range tests {
263 t.Run(tt.name, tt.run)
264 }
265}
266
267func TestDefensiveNoOps(cur realm, t *testing.T) {
268 alice := testutils.TestAddress("alice")
269 bob := testutils.TestAddress("bob")
270
271 enum, coreLedger, led := newEnum(cur)
272 urequire.NoError(t, coreLedger.Mint(alice, "1"))
273
274 tests := []struct {
275 name string
276 run func(t *testing.T)
277 }{
278 {"removeFromAll unknown id is a no-op", func(t *testing.T) {
279 led.removeFromAll("999")
280 uassert.Equal(t, int64(1), enum.TotalSupply())
281 }},
282 {"removeFromOwner unknown owner is a no-op", func(t *testing.T) {
283 led.removeFromOwner(bob, "1")
284 eqStrings(t, "alice untouched", ownerOrder(t, enum, alice), []string{"1"})
285 }},
286 {"removeFromOwner known owner unknown id is a no-op", func(t *testing.T) {
287 led.removeFromOwner(alice, "999")
288 eqStrings(t, "alice untouched", ownerOrder(t, enum, alice), []string{"1"})
289 }},
290 {"OnBurn of never-minted id is a no-op", func(t *testing.T) {
291 led.OnBurn("neverminted")
292 uassert.Equal(t, int64(1), enum.TotalSupply())
293 eqStrings(t, "alice untouched", ownerOrder(t, enum, alice), []string{"1"})
294 }},
295 }
296 for _, tt := range tests {
297 t.Run(tt.name, tt.run)
298 }
299}