1package metadata23import (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// newToken builds via same-realm cross (NewToken requires rlm.IsCurrent()).14func newToken(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 return19}2021func newMeta(id seqid.ID, rlm realm) (*grc721.PrivateLedger, *Metadata, *Ledger) {22 _, tokenLedger := newToken("Foo", "FOO", id, rlm)23 meta, metaLedger := NewMetadata(tokenLedger)24 return tokenLedger, meta, metaLedger25}2627func TestNewMetadata(cur realm, t *testing.T) {28 tests := []struct {29 name string30 nilLedger bool31 }{32 {"nil token ledger panics", true},33 {"valid token ledger constructs read view and ledger", false},34 }3536 for _, tt := range tests {37 t.Run(tt.name, func(t *testing.T) {38 if tt.nilLedger {39 uassert.PanicsWithMessage(t, cur, "metadata: nil core ledger", func() {40 NewMetadata(nil)41 })42 return43 }4445 _, tokenLedger := newToken("Foo", "FOO", 0, cur)46 meta, metaLedger := NewMetadata(tokenLedger)4748 uassert.True(t, meta != nil)49 uassert.True(t, metaLedger != nil)50 uassert.Equal(t, Kind, meta.ExtensionKind())51 uassert.Equal(t, "metadata", meta.ExtensionKind())52 uassert.Equal(t, tokenLedger.ReadToken().ID(), meta.TokenID())53 })54 }55}5657func TestSetTokenURI(cur realm, t *testing.T) {58 tests := []struct {59 name string60 mint bool61 tid grc721.TokenID62 uri string63 wantErr error64 }{65 {"unminted token returns ErrTokenNotMinted", false, "404", "ipfs://x", ErrTokenNotMinted},66 {"minted token sets uri", true, "1", "ipfs://uri-1", nil},67 {"empty uri on minted token is allowed", true, "1", "", nil},68 }6970 for _, tt := range tests {71 t.Run(tt.name, func(t *testing.T) {72 tokenLedger, meta, metaLedger := newMeta(0, cur)73 if tt.mint {74 urequire.NoError(t, tokenLedger.Mint(testutils.TestAddress("alice"), tt.tid))75 }7677 err := metaLedger.SetTokenURI(tt.tid, tt.uri)78 if tt.wantErr != nil {79 uassert.ErrorIs(t, err, tt.wantErr)80 uassert.False(t, meta.HasTokenURI(tt.tid))81 return82 }8384 urequire.NoError(t, err)85 uassert.True(t, meta.HasTokenURI(tt.tid))86 got, err := meta.TokenURI(tt.tid)87 urequire.NoError(t, err)88 uassert.Equal(t, tt.uri, got)89 })90 }91}9293func TestSetTokenURIOverwrite(cur realm, t *testing.T) {94 tokenLedger, meta, metaLedger := newMeta(0, cur)95 urequire.NoError(t, tokenLedger.Mint(testutils.TestAddress("alice"), "1"))9697 urequire.NoError(t, metaLedger.SetTokenURI("1", "ipfs://first"))98 urequire.NoError(t, metaLedger.SetTokenURI("1", "ipfs://second"))99100 got, err := meta.TokenURI("1")101 urequire.NoError(t, err)102 uassert.Equal(t, "ipfs://second", got)103}104105func TestSetTokenMetadata(cur realm, t *testing.T) {106 full := Data{107 Image: "ipfs://img-1",108 ImageData: "<svg/>",109 ExternalURL: "https://example.com/1",110 Description: "first",111 Name: "Token One",112 Attributes: []Trait{{DisplayType: "string", TraitType: "rarity", Value: "rare"}},113 BackgroundColor: "ffffff",114 AnimationURL: "ipfs://anim-1",115 YoutubeURL: "https://youtu.be/abc",116 }117118 tests := []struct {119 name string120 mint bool121 tid grc721.TokenID122 data Data123 wantErr error124 }{125 {"unminted token returns ErrTokenNotMinted", false, "404", Data{Name: "x"}, ErrTokenNotMinted},126 {"minted token stores full metadata", true, "1", full, nil},127 {"minted token stores zero-value metadata", true, "2", Data{}, nil},128 }129130 for _, tt := range tests {131 t.Run(tt.name, func(t *testing.T) {132 tokenLedger, meta, metaLedger := newMeta(0, cur)133 if tt.mint {134 urequire.NoError(t, tokenLedger.Mint(testutils.TestAddress("alice"), tt.tid))135 }136137 err := metaLedger.SetTokenMetadata(tt.tid, tt.data)138 if tt.wantErr != nil {139 uassert.ErrorIs(t, err, tt.wantErr)140 uassert.False(t, meta.HasMetadata(tt.tid))141 return142 }143144 urequire.NoError(t, err)145 uassert.True(t, meta.HasMetadata(tt.tid))146 got, err := meta.TokenMetadata(tt.tid)147 urequire.NoError(t, err)148 uassert.Equal(t, tt.data.Name, got.Name)149 uassert.Equal(t, tt.data.Description, got.Description)150 uassert.Equal(t, tt.data.Image, got.Image)151 uassert.Equal(t, len(tt.data.Attributes), len(got.Attributes))152 if len(tt.data.Attributes) > 0 {153 uassert.Equal(t, tt.data.Attributes[0].Value, got.Attributes[0].Value)154 uassert.Equal(t, tt.data.Attributes[0].TraitType, got.Attributes[0].TraitType)155 }156 })157 }158}159160func TestSetTokenMetadataOverwrite(cur realm, t *testing.T) {161 tokenLedger, meta, metaLedger := newMeta(0, cur)162 urequire.NoError(t, tokenLedger.Mint(testutils.TestAddress("alice"), "1"))163164 urequire.NoError(t, metaLedger.SetTokenMetadata("1", Data{Name: "first"}))165 urequire.NoError(t, metaLedger.SetTokenMetadata("1", Data{Name: "second"}))166167 got, err := meta.TokenMetadata("1")168 urequire.NoError(t, err)169 uassert.Equal(t, "second", got.Name)170}171172func TestReadsOnUnsetToken(cur realm, t *testing.T) {173 tests := []struct {174 name string175 fn func(t *testing.T, meta *Metadata)176 }{177 {"TokenURI on unset token returns ErrNoTokenURI", func(t *testing.T, meta *Metadata) {178 uri, err := meta.TokenURI("1")179 uassert.Equal(t, "", uri)180 uassert.ErrorIs(t, err, ErrNoTokenURI)181 }},182 {"TokenMetadata on unset token returns ErrNoMetadata", func(t *testing.T, meta *Metadata) {183 data, err := meta.TokenMetadata("1")184 uassert.Equal(t, "", data.Name)185 uassert.ErrorIs(t, err, ErrNoMetadata)186 }},187 {"HasTokenURI is false on unset token", func(t *testing.T, meta *Metadata) {188 uassert.False(t, meta.HasTokenURI("1"))189 }},190 {"HasMetadata is false on unset token", func(t *testing.T, meta *Metadata) {191 uassert.False(t, meta.HasMetadata("1"))192 }},193 }194195 for _, tt := range tests {196 t.Run(tt.name, func(t *testing.T) {197 _, meta, _ := newMeta(0, cur)198 tt.fn(t, meta)199 })200 }201}202203func TestOnBurnClearsMetadata(cur realm, t *testing.T) {204 alice := testutils.TestAddress("alice")205 tokenLedger, meta, metaLedger := newMeta(0, cur)206207 urequire.NoError(t, tokenLedger.Mint(alice, "1"))208 urequire.NoError(t, metaLedger.SetTokenURI("1", "ipfs://uri-1"))209 urequire.NoError(t, metaLedger.SetTokenMetadata("1", Data{Name: "one"}))210 uassert.True(t, meta.HasTokenURI("1"))211 uassert.True(t, meta.HasMetadata("1"))212213 urequire.NoError(t, tokenLedger.Burn("1"))214 uassert.False(t, meta.HasTokenURI("1"))215 uassert.False(t, meta.HasMetadata("1"))216217 _, err := meta.TokenURI("1")218 uassert.ErrorIs(t, err, ErrNoTokenURI)219 _, err = meta.TokenMetadata("1")220 uassert.ErrorIs(t, err, ErrNoMetadata)221222 urequire.NoError(t, tokenLedger.Mint(alice, "1"))223 uassert.False(t, meta.HasTokenURI("1"))224 uassert.False(t, meta.HasMetadata("1"))225}226227func TestHooks(cur realm, t *testing.T) {228 alice := testutils.TestAddress("alice")229 bob := testutils.TestAddress("bob")230 tokenLedger, meta, metaLedger := newMeta(0, cur)231232 urequire.NoError(t, tokenLedger.Mint(alice, "1"))233 urequire.NoError(t, metaLedger.SetTokenURI("1", "ipfs://uri-1"))234 urequire.NoError(t, metaLedger.SetTokenMetadata("1", Data{Name: "one"}))235236 metaLedger.OnMint(alice, "1")237 metaLedger.OnTransfer(alice, bob, "1")238 uassert.True(t, meta.HasTokenURI("1"))239 uassert.True(t, meta.HasMetadata("1"))240 uri, err := meta.TokenURI("1")241 urequire.NoError(t, err)242 uassert.Equal(t, "ipfs://uri-1", uri)243244 metaLedger.OnBurn("1")245 uassert.False(t, meta.HasTokenURI("1"))246 uassert.False(t, meta.HasMetadata("1"))247}248Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.