Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}