token_test.gno
22.90 Kb · 750 lines
1package grc721
2
3import (
4 "strings"
5 "testing"
6
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
13func mustBalanceOf(t *testing.T, tok *Token, addr address) int64 {
14 balance, err := tok.BalanceOf(addr)
15 urequire.NoError(t, err)
16 return balance
17}
18
19// newTestToken builds via same-realm cross (NewToken requires rlm.IsCurrent()).
20func newTestToken(name, symbol string, id seqid.ID, rlm realm) (tok *Token, led *PrivateLedger) {
21 func(cur realm) {
22 tok, led = NewToken(name, symbol, id, cur)
23 }(cross(rlm))
24 return
25}
26
27type mockExtension struct {
28 mints, transfers, burns int
29 lastMintTo address
30 lastTransferFrom address
31 lastTransferTo address
32 lastBurn TokenID
33}
34
35func (m *mockExtension) ExtensionKind() string { return "mock" }
36
37func (m *mockExtension) OnMint(to address, tid TokenID) {
38 m.mints++
39 m.lastMintTo = to
40}
41
42func (m *mockExtension) OnTransfer(from, to address, tid TokenID) {
43 m.transfers++
44 m.lastTransferFrom = from
45 m.lastTransferTo = to
46}
47
48func (m *mockExtension) OnBurn(tid TokenID) {
49 m.burns++
50 m.lastBurn = tid
51}
52
53// Value type holding a slice: uncomparable, so `==` on two of them is a runtime fault.
54type journalExtension struct{ seen []TokenID }
55
56func (e journalExtension) ExtensionKind() string { return "journal" }
57func (e journalExtension) OnMint(to address, tid TokenID) {}
58func (e journalExtension) OnTransfer(from, to address, tid TokenID) {}
59func (e journalExtension) OnBurn(tid TokenID) {}
60
61var (
62 alice = testutils.TestAddress("alice")
63 bob = testutils.TestAddress("bob")
64 carl = testutils.TestAddress("carl")
65)
66
67func TestNewToken(cur realm, t *testing.T) {
68 tok, led := newTestToken("Foo NFT", "FOO", 0, cur)
69 urequire.False(t, tok == nil, "token should not be nil")
70 urequire.False(t, led == nil, "ledger should not be nil")
71
72 uassert.Equal(t, "Foo NFT", tok.GetName())
73 uassert.Equal(t, "FOO", tok.GetSymbol())
74 uassert.Equal(t, int64(0), tok.TotalSupply())
75 uassert.Equal(t, 0, tok.KnownAccounts())
76 uassert.True(t, strings.HasSuffix(tok.ID(), ".FOO."+seqid.ID(0).String()), "ID must end with .symbol.id")
77 uassert.True(t, strings.HasPrefix(tok.ID(), "gno.land/"), "ID must begin with the origin realm path")
78 uassert.Equal(t, tok.ID(), led.ReadToken().ID())
79}
80
81func TestNewTokenDistinctIDs(cur realm, t *testing.T) {
82 first, _ := newTestToken("Same", "DUP", 1, cur)
83 second, _ := newTestToken("Same", "DUP", 2, cur)
84 uassert.True(t, first.ID() != second.ID(), "distinct seqid must yield distinct IDs")
85
86 a, _ := newTestToken("Same", "DUP", 3, cur)
87 b, _ := newTestToken("Same", "DUP", 3, cur)
88 uassert.Equal(t, a.ID(), b.ID())
89}
90
91func TestNewTokenValidation(cur realm, t *testing.T) {
92 longName := strings.Repeat("a", MaxNameLen+1)
93 maxName := strings.Repeat("a", MaxNameLen)
94 longSymbol := strings.Repeat("A", MaxSymbolLen+1)
95
96 tests := []struct {
97 name string
98 tokenName string
99 symbol string
100 wantAbort bool
101 wantSubstr string
102 }{
103 {"valid name and symbol succeeds", "Foo", "FOO", false, ""},
104 {"symbol with full slug charset succeeds", "Foo", "aZ9_-", false, ""},
105 {"name at max length succeeds", maxName, "FOO", false, ""},
106 {"empty name returns ErrInvalidName", "", "FOO", true, "invalid token name"},
107 {"name over max length returns ErrInvalidName", longName, "FOO", true, "invalid token name"},
108 {"name with low control char returns ErrInvalidName", "Foo\x01", "FOO", true, "invalid token name"},
109 {"name with DEL control char returns ErrInvalidName", "Foo\x7f", "FOO", true, "invalid token name"},
110 {"empty symbol returns ErrInvalidSymbol", "Foo", "", true, "invalid token symbol"},
111 {"symbol over max length returns ErrInvalidSymbol", "Foo", longSymbol, true, "invalid token symbol"},
112 {"symbol with space returns ErrInvalidSymbol", "Foo", "has space", true, "invalid token symbol"},
113 {"symbol with dot returns ErrInvalidSymbol", "Foo", "FO.O", true, "invalid token symbol"},
114 {"symbol with slash returns ErrInvalidSymbol", "Foo", "FO/O", true, "invalid token symbol"},
115 }
116
117 for _, tc := range tests {
118 tc := tc
119 t.Run(tc.name, func(t *testing.T) {
120 if tc.wantAbort {
121 urequire.AbortsContains(t, cur, tc.wantSubstr, func() {
122 newTestToken(tc.tokenName, tc.symbol, 0, cur)
123 })
124 return
125 }
126 tok, _ := newTestToken(tc.tokenName, tc.symbol, 0, cur)
127 uassert.Equal(t, tc.tokenName, tok.GetName())
128 uassert.Equal(t, tc.symbol, tok.GetSymbol())
129 })
130 }
131}
132
133func TestNewTokenRejectsSpoofedRealm(cur realm, t *testing.T) {
134 // Stale outer cur in a fresh frame → ErrSpoofedRealm.
135 urequire.AbortsContains(t, cur, "rlm does not match the current crossing frame", func() {
136 stale := cur
137 func(cur realm) {
138 NewToken("Foo", "FOO", 0, stale)
139 }(cross(cur))
140 })
141}
142
143func TestMint(cur realm, t *testing.T) {
144 tests := []struct {
145 name string
146 action func(led *PrivateLedger) error
147 wantErr error
148 verify func(t *testing.T, tok *Token, led *PrivateLedger)
149 }{
150 {
151 name: "mint to valid address succeeds",
152 action: func(led *PrivateLedger) error {
153 return led.Mint(alice, "1")
154 },
155 wantErr: nil,
156 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
157 uassert.Equal(t, int64(1), tok.TotalSupply())
158 uassert.Equal(t, int64(1), mustBalanceOf(t, tok, alice))
159 uassert.Equal(t, 1, tok.KnownAccounts())
160 owner, err := tok.OwnerOf("1")
161 urequire.NoError(t, err)
162 uassert.Equal(t, alice, owner)
163 },
164 },
165 {
166 name: "mint to invalid address returns ErrInvalidAddress",
167 action: func(led *PrivateLedger) error {
168 return led.Mint(zeroAddress, "1")
169 },
170 wantErr: ErrInvalidAddress,
171 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
172 uassert.Equal(t, int64(0), tok.TotalSupply())
173 },
174 },
175 {
176 name: "mint duplicate token id returns ErrTokenIdAlreadyExists",
177 action: func(led *PrivateLedger) error {
178 urequire.NoError(t, led.Mint(alice, "1"))
179 return led.Mint(bob, "1")
180 },
181 wantErr: ErrTokenIdAlreadyExists,
182 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
183 owner, err := tok.OwnerOf("1")
184 urequire.NoError(t, err)
185 uassert.Equal(t, alice, owner)
186 uassert.Equal(t, int64(1), tok.TotalSupply())
187 },
188 },
189 {
190 name: "minting distinct ids to same owner accumulates balance",
191 action: func(led *PrivateLedger) error {
192 urequire.NoError(t, led.Mint(alice, "1"))
193 return led.Mint(alice, "2")
194 },
195 wantErr: nil,
196 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
197 uassert.Equal(t, int64(2), mustBalanceOf(t, tok, alice))
198 uassert.Equal(t, int64(2), tok.TotalSupply())
199 uassert.Equal(t, 1, tok.KnownAccounts())
200 },
201 },
202 }
203
204 for _, tc := range tests {
205 tc := tc
206 t.Run(tc.name, func(t *testing.T) {
207 tok, led := newTestToken("Foo", "FOO", 0, cur)
208 err := tc.action(led)
209 if tc.wantErr == nil {
210 uassert.NoError(t, err)
211 } else {
212 uassert.ErrorIs(t, err, tc.wantErr)
213 }
214 if tc.verify != nil {
215 tc.verify(t, tok, led)
216 }
217 })
218 }
219}
220
221func TestBurn(cur realm, t *testing.T) {
222 tests := []struct {
223 name string
224 action func(led *PrivateLedger) error
225 wantErr error
226 verify func(t *testing.T, tok *Token, led *PrivateLedger)
227 }{
228 {
229 name: "burn existing token clears all state",
230 action: func(led *PrivateLedger) error {
231 urequire.NoError(t, led.Mint(alice, "1"))
232 return led.Burn("1")
233 },
234 wantErr: nil,
235 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
236 uassert.Equal(t, int64(0), tok.TotalSupply())
237 uassert.Equal(t, int64(0), mustBalanceOf(t, tok, alice))
238 uassert.Equal(t, 0, tok.KnownAccounts())
239 _, err := tok.OwnerOf("1")
240 uassert.ErrorIs(t, err, ErrInvalidTokenId)
241 },
242 },
243 {
244 name: "burn clears any outstanding approval",
245 action: func(led *PrivateLedger) error {
246 urequire.NoError(t, led.Mint(alice, "1"))
247 urequire.NoError(t, led.Approve(alice, bob, "1"))
248 return led.Burn("1")
249 },
250 wantErr: nil,
251 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
252 uassert.Equal(t, 0, led.tokenApprovals.Size())
253 _, err := tok.GetApproved("1")
254 uassert.ErrorIs(t, err, ErrInvalidTokenId)
255 },
256 },
257 {
258 name: "burn missing token returns ErrInvalidTokenId",
259 action: func(led *PrivateLedger) error {
260 return led.Burn("999")
261 },
262 wantErr: ErrInvalidTokenId,
263 },
264 {
265 name: "burn one of two tokens keeps the account known",
266 action: func(led *PrivateLedger) error {
267 urequire.NoError(t, led.Mint(alice, "1"))
268 urequire.NoError(t, led.Mint(alice, "2"))
269 return led.Burn("1")
270 },
271 wantErr: nil,
272 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
273 uassert.Equal(t, int64(1), mustBalanceOf(t, tok, alice))
274 uassert.Equal(t, 1, tok.KnownAccounts())
275 uassert.Equal(t, int64(1), tok.TotalSupply())
276 },
277 },
278 }
279
280 for _, tc := range tests {
281 tc := tc
282 t.Run(tc.name, func(t *testing.T) {
283 tok, led := newTestToken("Foo", "FOO", 0, cur)
284 err := tc.action(led)
285 if tc.wantErr == nil {
286 uassert.NoError(t, err)
287 } else {
288 uassert.ErrorIs(t, err, tc.wantErr)
289 }
290 if tc.verify != nil {
291 tc.verify(t, tok, led)
292 }
293 })
294 }
295}
296
297func TestApprove(cur realm, t *testing.T) {
298 tests := []struct {
299 name string
300 action func(led *PrivateLedger) error
301 wantErr error
302 verify func(t *testing.T, tok *Token, led *PrivateLedger)
303 }{
304 {
305 name: "owner approves another account",
306 action: func(led *PrivateLedger) error {
307 urequire.NoError(t, led.Mint(alice, "1"))
308 return led.Approve(alice, bob, "1")
309 },
310 wantErr: nil,
311 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
312 approved, err := tok.GetApproved("1")
313 urequire.NoError(t, err)
314 uassert.Equal(t, bob, approved)
315 },
316 },
317 {
318 name: "operator approves on behalf of owner",
319 action: func(led *PrivateLedger) error {
320 urequire.NoError(t, led.Mint(alice, "1"))
321 urequire.NoError(t, led.SetApprovalForAll(alice, bob, true))
322 return led.Approve(bob, carl, "1")
323 },
324 wantErr: nil,
325 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
326 approved, err := tok.GetApproved("1")
327 urequire.NoError(t, err)
328 uassert.Equal(t, carl, approved)
329 },
330 },
331 {
332 name: "approve to zero address revokes an existing approval (EIP-721)",
333 action: func(led *PrivateLedger) error {
334 urequire.NoError(t, led.Mint(alice, "1"))
335 urequire.NoError(t, led.Approve(alice, bob, "1"))
336 return led.Approve(alice, zeroAddress, "1")
337 },
338 wantErr: nil,
339 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
340 _, err := tok.GetApproved("1")
341 uassert.ErrorIs(t, err, ErrTokenIdNotApproved)
342 },
343 },
344 {
345 name: "revoke with no prior approval is a no-op",
346 action: func(led *PrivateLedger) error {
347 urequire.NoError(t, led.Mint(alice, "1"))
348 return led.Approve(alice, zeroAddress, "1")
349 },
350 wantErr: nil,
351 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
352 _, err := tok.GetApproved("1")
353 uassert.ErrorIs(t, err, ErrTokenIdNotApproved)
354 },
355 },
356 {
357 name: "approve on missing token returns ErrInvalidTokenId",
358 action: func(led *PrivateLedger) error {
359 return led.Approve(alice, bob, "999")
360 },
361 wantErr: ErrInvalidTokenId,
362 },
363 {
364 name: "approve to current owner returns ErrApprovalToCurrentOwner",
365 action: func(led *PrivateLedger) error {
366 urequire.NoError(t, led.Mint(alice, "1"))
367 return led.Approve(alice, alice, "1")
368 },
369 wantErr: ErrApprovalToCurrentOwner,
370 },
371 {
372 name: "non-owner non-operator approve returns ErrCallerIsNotOwnerOrApproved",
373 action: func(led *PrivateLedger) error {
374 urequire.NoError(t, led.Mint(alice, "1"))
375 return led.Approve(bob, carl, "1")
376 },
377 wantErr: ErrCallerIsNotOwnerOrApproved,
378 },
379 }
380
381 for _, tc := range tests {
382 tc := tc
383 t.Run(tc.name, func(t *testing.T) {
384 tok, led := newTestToken("Foo", "FOO", 0, cur)
385 err := tc.action(led)
386 if tc.wantErr == nil {
387 uassert.NoError(t, err)
388 } else {
389 uassert.ErrorIs(t, err, tc.wantErr)
390 }
391 if tc.verify != nil {
392 tc.verify(t, tok, led)
393 }
394 })
395 }
396}
397
398func TestSetApprovalForAll(cur realm, t *testing.T) {
399 tests := []struct {
400 name string
401 action func(led *PrivateLedger) error
402 wantErr error
403 verify func(t *testing.T, tok *Token, led *PrivateLedger)
404 }{
405 {
406 name: "grant operator approval",
407 action: func(led *PrivateLedger) error {
408 return led.SetApprovalForAll(alice, bob, true)
409 },
410 wantErr: nil,
411 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
412 uassert.True(t, tok.IsApprovedForAll(alice, bob))
413 },
414 },
415 {
416 name: "revoke operator approval releases the node",
417 action: func(led *PrivateLedger) error {
418 urequire.NoError(t, led.SetApprovalForAll(alice, bob, true))
419 return led.SetApprovalForAll(alice, bob, false)
420 },
421 wantErr: nil,
422 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
423 uassert.False(t, tok.IsApprovedForAll(alice, bob))
424 uassert.Equal(t, 0, led.operatorApprovals.Size())
425 },
426 },
427 {
428 name: "revoke without a prior grant stores nothing",
429 action: func(led *PrivateLedger) error {
430 return led.SetApprovalForAll(alice, bob, false)
431 },
432 wantErr: nil,
433 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
434 uassert.False(t, tok.IsApprovedForAll(alice, bob))
435 uassert.Equal(t, 0, led.operatorApprovals.Size())
436 },
437 },
438 {
439 name: "invalid operator returns ErrInvalidAddress",
440 action: func(led *PrivateLedger) error {
441 return led.SetApprovalForAll(alice, zeroAddress, true)
442 },
443 wantErr: ErrInvalidAddress,
444 },
445 {
446 name: "owner equals operator returns ErrApprovalToCurrentOwner",
447 action: func(led *PrivateLedger) error {
448 return led.SetApprovalForAll(alice, alice, true)
449 },
450 wantErr: ErrApprovalToCurrentOwner,
451 },
452 }
453
454 for _, tc := range tests {
455 tc := tc
456 t.Run(tc.name, func(t *testing.T) {
457 tok, led := newTestToken("Foo", "FOO", 0, cur)
458 err := tc.action(led)
459 if tc.wantErr == nil {
460 uassert.NoError(t, err)
461 } else {
462 uassert.ErrorIs(t, err, tc.wantErr)
463 }
464 if tc.verify != nil {
465 tc.verify(t, tok, led)
466 }
467 })
468 }
469}
470
471func TestTransferFrom(cur realm, t *testing.T) {
472 tests := []struct {
473 name string
474 action func(led *PrivateLedger) error
475 wantErr error
476 verify func(t *testing.T, tok *Token, led *PrivateLedger)
477 }{
478 {
479 name: "owner transfers own token",
480 action: func(led *PrivateLedger) error {
481 urequire.NoError(t, led.Mint(alice, "1"))
482 return led.TransferFrom(alice, alice, bob, "1")
483 },
484 wantErr: nil,
485 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
486 owner, err := tok.OwnerOf("1")
487 urequire.NoError(t, err)
488 uassert.Equal(t, bob, owner)
489 uassert.Equal(t, int64(0), mustBalanceOf(t, tok, alice))
490 uassert.Equal(t, int64(1), mustBalanceOf(t, tok, bob))
491 },
492 },
493 {
494 name: "approved spender transfers and approval is cleared",
495 action: func(led *PrivateLedger) error {
496 urequire.NoError(t, led.Mint(alice, "1"))
497 urequire.NoError(t, led.Approve(alice, bob, "1"))
498 return led.TransferFrom(bob, alice, carl, "1")
499 },
500 wantErr: nil,
501 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
502 owner, err := tok.OwnerOf("1")
503 urequire.NoError(t, err)
504 uassert.Equal(t, carl, owner)
505 _, err = tok.GetApproved("1")
506 uassert.ErrorIs(t, err, ErrTokenIdNotApproved)
507 },
508 },
509 {
510 name: "operator transfers owner token",
511 action: func(led *PrivateLedger) error {
512 urequire.NoError(t, led.Mint(alice, "1"))
513 urequire.NoError(t, led.SetApprovalForAll(alice, bob, true))
514 return led.TransferFrom(bob, alice, carl, "1")
515 },
516 wantErr: nil,
517 verify: func(t *testing.T, tok *Token, led *PrivateLedger) {
518 owner, err := tok.OwnerOf("1")
519 urequire.NoError(t, err)
520 uassert.Equal(t, carl, owner)
521 },
522 },
523 {
524 name: "unauthorized spender returns ErrCallerIsNotOwnerOrApproved",
525 action: func(led *PrivateLedger) error {
526 urequire.NoError(t, led.Mint(alice, "1"))
527 return led.TransferFrom(bob, alice, carl, "1")
528 },
529 wantErr: ErrCallerIsNotOwnerOrApproved,
530 },
531 {
532 name: "spender on missing token returns ErrCallerIsNotOwnerOrApproved",
533 action: func(led *PrivateLedger) error {
534 return led.TransferFrom(alice, alice, bob, "999")
535 },
536 wantErr: ErrCallerIsNotOwnerOrApproved,
537 },
538 {
539 name: "transfer to self returns ErrCannotTransferToSelf",
540 action: func(led *PrivateLedger) error {
541 urequire.NoError(t, led.Mint(alice, "1"))
542 return led.TransferFrom(alice, alice, alice, "1")
543 },
544 wantErr: ErrCannotTransferToSelf,
545 },
546 {
547 name: "transfer from invalid address returns ErrInvalidAddress",
548 action: func(led *PrivateLedger) error {
549 urequire.NoError(t, led.Mint(alice, "1"))
550 return led.TransferFrom(alice, zeroAddress, bob, "1")
551 },
552 wantErr: ErrInvalidAddress,
553 },
554 {
555 name: "transfer to invalid address returns ErrInvalidAddress",
556 action: func(led *PrivateLedger) error {
557 urequire.NoError(t, led.Mint(alice, "1"))
558 return led.TransferFrom(alice, alice, zeroAddress, "1")
559 },
560 wantErr: ErrInvalidAddress,
561 },
562 {
563 name: "transfer from incorrect owner returns ErrTransferFromIncorrectOwner",
564 action: func(led *PrivateLedger) error {
565 urequire.NoError(t, led.Mint(alice, "1"))
566 return led.TransferFrom(alice, bob, carl, "1")
567 },
568 wantErr: ErrTransferFromIncorrectOwner,
569 },
570 }
571
572 for _, tc := range tests {
573 tc := tc
574 t.Run(tc.name, func(t *testing.T) {
575 tok, led := newTestToken("Foo", "FOO", 0, cur)
576 err := tc.action(led)
577 if tc.wantErr == nil {
578 uassert.NoError(t, err)
579 } else {
580 uassert.ErrorIs(t, err, tc.wantErr)
581 }
582 if tc.verify != nil {
583 tc.verify(t, tok, led)
584 }
585 })
586 }
587}
588
589func TestReadViews(cur realm, t *testing.T) {
590 tok, led := newTestToken("Foo NFT", "FOO", 0, cur)
591 urequire.NoError(t, led.Mint(alice, "1"))
592 urequire.NoError(t, led.Mint(alice, "2"))
593 urequire.NoError(t, led.Mint(bob, "3"))
594 urequire.NoError(t, led.Approve(alice, carl, "1"))
595 urequire.NoError(t, led.SetApprovalForAll(alice, bob, true))
596
597 tests := []struct {
598 name string
599 check func(t *testing.T)
600 }{
601 {"GetName returns collection name", func(t *testing.T) {
602 uassert.Equal(t, "Foo NFT", tok.GetName())
603 }},
604 {"GetSymbol returns collection symbol", func(t *testing.T) {
605 uassert.Equal(t, "FOO", tok.GetSymbol())
606 }},
607 {"ID carries realm, symbol and seqid", func(t *testing.T) {
608 uassert.True(t, strings.HasPrefix(tok.ID(), "gno.land/"))
609 uassert.True(t, strings.Contains(tok.ID(), ".FOO."))
610 }},
611 {"TotalSupply counts live tokens", func(t *testing.T) {
612 uassert.Equal(t, int64(3), tok.TotalSupply())
613 }},
614 {"KnownAccounts counts distinct holders", func(t *testing.T) {
615 uassert.Equal(t, 2, tok.KnownAccounts())
616 }},
617 {"BalanceOf of a holder", func(t *testing.T) {
618 uassert.Equal(t, int64(2), mustBalanceOf(t, tok, alice))
619 }},
620 {"BalanceOf of a non-holder is zero", func(t *testing.T) {
621 uassert.Equal(t, int64(0), mustBalanceOf(t, tok, carl))
622 }},
623 {"BalanceOf of the zero address returns ErrInvalidAddress", func(t *testing.T) {
624 _, err := tok.BalanceOf(zeroAddress)
625 uassert.ErrorIs(t, err, ErrInvalidAddress)
626 }},
627 {"OwnerOf existing token", func(t *testing.T) {
628 owner, err := tok.OwnerOf("3")
629 urequire.NoError(t, err)
630 uassert.Equal(t, bob, owner)
631 }},
632 {"OwnerOf missing token returns ErrInvalidTokenId", func(t *testing.T) {
633 _, err := tok.OwnerOf("999")
634 uassert.ErrorIs(t, err, ErrInvalidTokenId)
635 }},
636 {"GetApproved returns the approved account", func(t *testing.T) {
637 approved, err := tok.GetApproved("1")
638 urequire.NoError(t, err)
639 uassert.Equal(t, carl, approved)
640 }},
641 {"GetApproved on unapproved token returns ErrTokenIdNotApproved", func(t *testing.T) {
642 _, err := tok.GetApproved("2")
643 uassert.ErrorIs(t, err, ErrTokenIdNotApproved)
644 }},
645 {"GetApproved on missing token returns ErrInvalidTokenId", func(t *testing.T) {
646 _, err := tok.GetApproved("999")
647 uassert.ErrorIs(t, err, ErrInvalidTokenId)
648 }},
649 {"IsApprovedForAll true for operator", func(t *testing.T) {
650 uassert.True(t, tok.IsApprovedForAll(alice, bob))
651 }},
652 {"IsApprovedForAll false for non-operator", func(t *testing.T) {
653 uassert.False(t, tok.IsApprovedForAll(alice, carl))
654 }},
655 {"RenderHome renders header and counters", func(t *testing.T) {
656 out := tok.RenderHome()
657 uassert.True(t, strings.Contains(out, "# Foo NFT ($FOO)"), "header")
658 uassert.True(t, strings.Contains(out, "**Total supply**: 3"), "supply")
659 uassert.True(t, strings.Contains(out, "**Known accounts**: 2"), "accounts")
660 }},
661 }
662
663 for _, tc := range tests {
664 tc := tc
665 t.Run(tc.name, tc.check)
666 }
667}
668
669func TestRegisterExtensionNil(cur realm, t *testing.T) {
670 _, led := newTestToken("Foo", "FOO", 0, cur)
671 uassert.PanicsWithMessage(t, cur, "grc721: nil extension", func() {
672 led.RegisterExtension(nil)
673 })
674}
675
676func TestRegisterExtensionRejectsDuplicateKind(cur realm, t *testing.T) {
677 _, led := newTestToken("Foo", "FOO", 0, cur)
678 ext := &mockExtension{}
679 led.RegisterExtension(ext)
680
681 uassert.PanicsWithMessage(t, cur, "grc721: extension kind already registered: mock", func() {
682 led.RegisterExtension(ext)
683 })
684 uassert.PanicsWithMessage(t, cur, "grc721: extension kind already registered: mock", func() {
685 led.RegisterExtension(&mockExtension{})
686 })
687}
688
689func TestRegisterExtensionRejectsDuplicateKindOfUncomparableType(cur realm, t *testing.T) {
690 _, led := newTestToken("Foo", "FOO", 0, cur)
691 led.RegisterExtension(journalExtension{seen: []TokenID{"1"}})
692
693 uassert.PanicsWithMessage(t, cur, "grc721: extension kind already registered: journal", func() {
694 led.RegisterExtension(journalExtension{seen: []TokenID{"2"}})
695 })
696}
697
698func TestExtensionHookFanOut(cur realm, t *testing.T) {
699 tests := []struct {
700 name string
701 run func(t *testing.T, led *PrivateLedger, ext *mockExtension)
702 verify func(t *testing.T, ext *mockExtension)
703 }{
704 {
705 name: "mint, transfer and burn each notify the extension",
706 run: func(t *testing.T, led *PrivateLedger, ext *mockExtension) {
707 led.RegisterExtension(ext)
708 urequire.NoError(t, led.Mint(alice, "1"))
709 urequire.NoError(t, led.TransferFrom(alice, alice, bob, "1"))
710 urequire.NoError(t, led.Burn("1"))
711 },
712 verify: func(t *testing.T, ext *mockExtension) {
713 uassert.Equal(t, 1, ext.mints)
714 uassert.Equal(t, 1, ext.transfers)
715 uassert.Equal(t, alice, ext.lastTransferFrom)
716 uassert.Equal(t, bob, ext.lastTransferTo)
717 uassert.Equal(t, 1, ext.burns)
718 uassert.Equal(t, "1", ext.lastBurn.String())
719 },
720 },
721 {
722 name: "failed operations do not notify the extension",
723 run: func(t *testing.T, led *PrivateLedger, ext *mockExtension) {
724 led.RegisterExtension(ext)
725 uassert.ErrorIs(t, led.Mint(zeroAddress, "1"), ErrInvalidAddress)
726 uassert.ErrorIs(t, led.Burn("999"), ErrInvalidTokenId)
727 },
728 verify: func(t *testing.T, ext *mockExtension) {
729 uassert.Equal(t, 0, ext.mints)
730 uassert.Equal(t, 0, ext.burns)
731 },
732 },
733 }
734
735 for _, tc := range tests {
736 tc := tc
737 t.Run(tc.name, func(t *testing.T) {
738 _, led := newTestToken("Foo", "FOO", 0, cur)
739 ext := &mockExtension{}
740 tc.run(t, led, ext)
741 tc.verify(t, ext)
742 })
743 }
744}
745
746func TestReadToken(cur realm, t *testing.T) {
747 tok, led := newTestToken("Foo", "FOO", 0, cur)
748 uassert.Equal(t, tok.ID(), led.ReadToken().ID())
749 uassert.True(t, led.ReadToken() == tok, "ReadToken returns the same *Token")
750}