1package enumerable23import (4 "testing"56 "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)1213// 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))1819 return20}2122func 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, led26}2728func 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 out37}3839func 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 break46 }47 out = append(out, tid.String())48 }49 return out50}5152func 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 return57 }58 for i := range want {59 uassert.Equal(t, want[i], got[i])60 }61}6263func TestNewEnumerable(cur realm, t *testing.T) {64 uassert.PanicsWithMessage(t, cur, "enumerable: nil core ledger", func() {65 NewEnumerable(nil)66 })6768 tok, coreLedger := newCore("Foo", "FOO", 0, cur)69 enum, led := NewEnumerable(coreLedger)7071 urequire.True(t, enum != nil, "enum built")72 urequire.True(t, led != nil, "ledger built")7374 tests := []struct {75 name string76 got string77 want string78 }{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 }8889 uassert.Equal(t, int64(0), enum.TotalSupply())90}9192func TestReadIndices(cur realm, t *testing.T) {93 alice := testutils.TestAddress("alice")94 bob := testutils.TestAddress("bob")95 carol := testutils.TestAddress("carol")9697 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"))101102 uassert.Equal(t, int64(3), enum.TotalSupply())103104 tests := []struct {105 name string106 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 }147148 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}152153func TestBurnGlobalSwapAndPop(cur realm, t *testing.T) {154 alice := testutils.TestAddress("alice")155156 tests := []struct {157 name string158 burn grc721.TokenID159 wantOrder []string160 }{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"))171172 urequire.NoError(t, coreLedger.Burn(tt.burn))173174 uassert.Equal(t, int64(2), enum.TotalSupply())175 eqStrings(t, tt.name, globalOrder(t, enum), tt.wantOrder)176 })177 }178}179180func TestBurnOwnerSwapAndPop(cur realm, t *testing.T) {181 alice := testutils.TestAddress("alice")182183 tests := []struct {184 name string185 mint []grc721.TokenID186 burn grc721.TokenID187 wantOwner []string188 ownerEmpty bool189 }{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 }221222 urequire.NoError(t, coreLedger.Burn(tt.burn))223224 if tt.ownerEmpty {225 _, err := enum.TokenOfOwnerByIndex(alice, 0)226 uassert.ErrorIs(t, err, ErrIndexOutOfRange)227 return228 }229 eqStrings(t, tt.name, ownerOrder(t, enum, alice), tt.wantOwner)230 })231 }232}233234func TestTransfer(cur realm, t *testing.T) {235 alice := testutils.TestAddress("alice")236 bob := testutils.TestAddress("bob")237238 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"))242243 tests := []struct {244 name string245 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}266267func TestDefensiveNoOps(cur realm, t *testing.T) {268 alice := testutils.TestAddress("alice")269 bob := testutils.TestAddress("bob")270271 enum, coreLedger, led := newEnum(cur)272 urequire.NoError(t, coreLedger.Mint(alice, "1"))273274 tests := []struct {275 name string276 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}300Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.