github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/amino/json_test.go (about)

     1  package amino_test
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"reflect"
     8  	"strconv"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  
    16  	amino "github.com/gnolang/gno/tm2/pkg/amino"
    17  	"github.com/gnolang/gno/tm2/pkg/amino/pkg"
    18  )
    19  
    20  type Dummy struct{}
    21  
    22  var gopkg = reflect.TypeOf(Dummy{}).PkgPath()
    23  
    24  var transportPackage = pkg.NewPackage(gopkg, "amino_test", "").
    25  	WithTypes(&Transport{}, Car(""), insurancePlan(0), Boat(""), Plane{})
    26  
    27  func registerTransports(cdc *amino.Codec) {
    28  	cdc.RegisterPackage(transportPackage)
    29  }
    30  
    31  func TestMarshalJSON(t *testing.T) {
    32  	t.Parallel()
    33  
    34  	cdc := amino.NewCodec()
    35  	registerTransports(cdc)
    36  	cases := []struct {
    37  		in      interface{}
    38  		want    string
    39  		wantErr string
    40  	}{
    41  		{&noFields{}, "{}", ""},                        // #0
    42  		{&noExportedFields{a: 10, b: "foo"}, "{}", ""}, // #1
    43  		{nil, "null", ""},                              // #2
    44  		{&oneExportedField{}, `{"A":""}`, ""},          // #3
    45  		{Car(""), `""`, ""},                            // #4
    46  		{Car("Tesla"), `"Tesla"`, ""},                  // #5
    47  		{&oneExportedField{A: "Z"}, `{"A":"Z"}`, ""},   // #6
    48  		{[]string{"a", "bc"}, `["a","bc"]`, ""},        // #7
    49  		{
    50  			[]interface{}{"a", "bc", 10, 10.93, 1e3},
    51  			``, "unregistered",
    52  		}, // #8
    53  		{
    54  			aPointerField{Foo: new(int), Name: "name"},
    55  			`{"Foo":"0","nm":"name"}`, "",
    56  		}, // #9
    57  		{
    58  			aPointerFieldAndEmbeddedField{intPtr(11), "ap", nil, &oneExportedField{A: "foo"}},
    59  			`{"Foo":"11","nm":"ap","bz":{"A":"foo"}}`, "",
    60  		}, // #10
    61  		{
    62  			doublyEmbedded{
    63  				Inner: &aPointerFieldAndEmbeddedField{
    64  					intPtr(11), "ap", nil, &oneExportedField{A: "foo"},
    65  				},
    66  			},
    67  			`{"Inner":{"Foo":"11","nm":"ap","bz":{"A":"foo"}},"year":0}`, "",
    68  		}, // #11
    69  		{
    70  			struct{}{}, `{}`, "",
    71  		}, // #12
    72  		{
    73  			struct{ A int }{A: 10}, `{"A":"10"}`, "",
    74  		}, // #13
    75  		{
    76  			Transport{},
    77  			`{"Vehicle":null,"Capacity":"0"}`, "",
    78  		}, // #14
    79  		{
    80  			Transport{Vehicle: Car("Bugatti")},
    81  			`{"Vehicle":{"@type":"/amino_test.Car","value":"Bugatti"},"Capacity":"0"}`, "",
    82  		}, // #15
    83  		{
    84  			BalanceSheet{Assets: []Asset{Car("Corolla"), insurancePlan(1e7)}},
    85  			`{"assets":[{"@type":"/amino_test.Car","value":"Corolla"},{"@type":"/amino_test.insurancePlan","value":"10000000"}]}`, "",
    86  		}, // #16
    87  		{
    88  			Transport{Vehicle: Boat("Poseidon"), Capacity: 1789},
    89  			`{"Vehicle":{"@type":"/amino_test.Boat","value":"Poseidon"},"Capacity":"1789"}`, "",
    90  		}, // #17
    91  		{
    92  			withCustomMarshaler{A: &aPointerField{Foo: intPtr(12)}, F: customJSONMarshaler(10)},
    93  			`{"fx":"10","A":{"Foo":"12"}}`, "",
    94  		}, // #18 (NOTE: MarshalJSON of customJSONMarshaler has no effect)
    95  		{
    96  			func() json.Marshaler { v := customJSONMarshaler(10); return &v }(),
    97  			`"10"`, "",
    98  		}, // #19 (NOTE: MarshalJSON of customJSONMarshaler has no effect)
    99  		{
   100  			interfacePtr("a"), `{"@type":"/google.protobuf.StringValue","value":"a"}`, "",
   101  		}, // #20
   102  		{&fp{"Foo", 10}, `"Foo@10"`, ""}, // #21
   103  		{(*fp)(nil), "null", ""},         // #22
   104  		{
   105  			struct {
   106  				FP      *fp
   107  				Package string
   108  			}{FP: &fp{"Foo", 10}, Package: "bytes"},
   109  			`{"FP":"Foo@10","Package":"bytes"}`, "",
   110  		}, // #23
   111  	}
   112  
   113  	for i, tt := range cases {
   114  		t.Logf("Trying case #%v", i)
   115  		blob, err := cdc.MarshalJSON(tt.in)
   116  		if tt.wantErr != "" {
   117  			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
   118  				t.Errorf("#%d:\ngot:\n\t%v\nwant non-nil error containing\n\t%q", i,
   119  					err, tt.wantErr)
   120  			}
   121  			continue
   122  		}
   123  
   124  		if err != nil {
   125  			t.Errorf("#%d: unexpected error: %v\nblob: %v", i, err, tt.in)
   126  			continue
   127  		}
   128  		if g, w := string(blob), tt.want; g != w {
   129  			t.Errorf("#%d:\ngot:\n\t%s\nwant:\n\t%s", i, g, w)
   130  		}
   131  	}
   132  }
   133  
   134  func TestMarshalJSONTime(t *testing.T) {
   135  	t.Parallel()
   136  
   137  	cdc := amino.NewCodec()
   138  	registerTransports(cdc)
   139  
   140  	type SimpleStruct struct {
   141  		String string
   142  		Bytes  []byte
   143  		Time   time.Time
   144  	}
   145  
   146  	s := SimpleStruct{
   147  		String: "hello",
   148  		Bytes:  []byte("goodbye"),
   149  		Time:   time.Now().Round(0).UTC(), // strip monotonic.
   150  	}
   151  
   152  	b, err := cdc.MarshalJSON(s)
   153  	assert.Nil(t, err)
   154  
   155  	var s2 SimpleStruct
   156  	err = cdc.UnmarshalJSON(b, &s2)
   157  	assert.Nil(t, err)
   158  	assert.Equal(t, s, s2)
   159  }
   160  
   161  type fp struct {
   162  	Name    string
   163  	Version int
   164  }
   165  
   166  func (f fp) MarshalAmino() (string, error) {
   167  	return fmt.Sprintf("%v@%v", f.Name, f.Version), nil
   168  }
   169  
   170  func (f *fp) UnmarshalAmino(repr string) (err error) {
   171  	parts := strings.Split(repr, "@")
   172  	if len(parts) != 2 {
   173  		return fmt.Errorf("invalid format %v", repr)
   174  	}
   175  	f.Name = parts[0]
   176  	f.Version, err = strconv.Atoi(parts[1])
   177  	return
   178  }
   179  
   180  type innerFP struct {
   181  	PC uint64
   182  	FP *fp
   183  }
   184  
   185  // We don't support maps.
   186  func TestUnmarshalMap(t *testing.T) {
   187  	t.Parallel()
   188  
   189  	jsonBytes := []byte("dontcare")
   190  	obj := new(map[string]int)
   191  	cdc := amino.NewCodec()
   192  	assert.Panics(t, func() {
   193  		err := cdc.UnmarshalJSON(jsonBytes, &obj)
   194  		assert.Fail(t, "should have panicked but got err: %v", err)
   195  	})
   196  	assert.Panics(t, func() {
   197  		err := cdc.UnmarshalJSON(jsonBytes, obj)
   198  		assert.Fail(t, "should have panicked but got err: %v", err)
   199  	})
   200  	assert.Panics(t, func() {
   201  		bz, err := cdc.MarshalJSON(obj)
   202  		assert.Fail(t, "should have panicked but got bz: %X err: %v", bz, err)
   203  	})
   204  }
   205  
   206  func TestUnmarshalFunc(t *testing.T) {
   207  	t.Parallel()
   208  
   209  	jsonBytes := []byte(`"dontcare"`)
   210  	obj := func() {}
   211  	cdc := amino.NewCodec()
   212  	assert.Panics(t, func() {
   213  		err := cdc.UnmarshalJSON(jsonBytes, &obj)
   214  		assert.Fail(t, "should have panicked but got err: %v", err)
   215  	})
   216  
   217  	err := cdc.UnmarshalJSON(jsonBytes, obj)
   218  	// UnmarshalJSON expects a pointer
   219  	assert.Error(t, err)
   220  
   221  	// ... nor encoding it.
   222  	assert.Panics(t, func() {
   223  		bz, err := cdc.MarshalJSON(obj)
   224  		assert.Fail(t, "should have panicked but got bz: %X err: %v", bz, err)
   225  	})
   226  }
   227  
   228  func TestUnmarshalJSON(t *testing.T) {
   229  	t.Parallel()
   230  
   231  	cdc := amino.NewCodec()
   232  	registerTransports(cdc)
   233  	cases := []struct {
   234  		blob    string
   235  		in      interface{}
   236  		want    interface{}
   237  		wantErr string
   238  	}{
   239  		{ // #0
   240  			`null`, 2, nil, "expected a pointer",
   241  		},
   242  		{ // #1
   243  			`null`, new(int), new(int), "",
   244  		},
   245  		{ // #2
   246  			`"2"`, new(int), intPtr(2), "",
   247  		},
   248  		{ // #3
   249  			`{"null"}`, new(int), nil, "invalid character",
   250  		},
   251  		{ // #4
   252  			`{"Vehicle":null,"Capacity":"0"}`, new(Transport), new(Transport), "",
   253  		},
   254  		{ // #5
   255  			`{"Vehicle":{"@type":"/amino_test.Car","value":"Bugatti"},"Capacity":"10"}`,
   256  			new(Transport),
   257  			&Transport{
   258  				Vehicle:  Car("Bugatti"),
   259  				Capacity: 10,
   260  			}, "",
   261  		},
   262  		{ // #6
   263  			`"Bugatti"`, new(Car), func() *Car { c := Car("Bugatti"); return &c }(), "",
   264  		},
   265  		{ // #7
   266  			`["1", "2", "3"]`, new([]int), func() interface{} {
   267  				v := []int{1, 2, 3}
   268  				return &v
   269  			}(), "",
   270  		},
   271  		{ // #8
   272  			`["1", "2", "3"]`, new([]string), func() interface{} {
   273  				v := []string{"1", "2", "3"}
   274  				return &v
   275  			}(), "",
   276  		},
   277  		{ // #9
   278  			`[{"@type":"/google.protobuf.Int32Value","value":1},{"@type":"/google.protobuf.StringValue","value":"2"}]`,
   279  			new([]interface{}), &([]interface{}{int32(1), string("2")}), "",
   280  		},
   281  		{ // #10
   282  			`2.34`, floatPtr(2.34), nil, "float* support requires",
   283  		},
   284  		{ // #11
   285  			`"FooBar@1"`, new(fp), &fp{"FooBar", 1}, "",
   286  		},
   287  		{ // #12
   288  			`"10@0"`, new(fp), &fp{Name: "10"}, "",
   289  		},
   290  		{ // #13
   291  			`{"PC":"125","FP":"10@0"}`, new(innerFP), &innerFP{PC: 125, FP: &fp{Name: `10`}}, "",
   292  		},
   293  		{ // #14
   294  			`{"PC":"125","FP":"<FP-FOO>@0"}`, new(innerFP), &innerFP{PC: 125, FP: &fp{Name: `<FP-FOO>`}}, "",
   295  		},
   296  	}
   297  
   298  	for i, tt := range cases {
   299  		err := cdc.UnmarshalJSON([]byte(tt.blob), tt.in)
   300  		if tt.wantErr != "" {
   301  			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
   302  				t.Errorf("#%d:\ngot:\n\t%q\nwant non-nil error containing\n\t%q", i,
   303  					err, tt.wantErr)
   304  			}
   305  			continue
   306  		}
   307  
   308  		if err != nil {
   309  			t.Errorf("#%d: unexpected error: %v\nblob: %s\nin: %+v\n", i, err, tt.blob, tt.in)
   310  			continue
   311  		}
   312  		if g, w := tt.in, tt.want; !reflect.DeepEqual(g, w) {
   313  			gb, err := json.MarshalIndent(g, "", "  ")
   314  			require.NoError(t, err)
   315  			wb, err := json.MarshalIndent(w, "", "  ")
   316  			require.NoError(t, err)
   317  			t.Errorf("#%d:\ngot:\n\t%#v\n(%s)\n\nwant:\n\t%#v\n(%s)", i, g, gb, w, wb)
   318  		}
   319  	}
   320  }
   321  
   322  func TestJSONCodecRoundTrip(t *testing.T) {
   323  	t.Parallel()
   324  
   325  	cdc := amino.NewCodec()
   326  	registerTransports(cdc)
   327  	type allInclusive struct {
   328  		Tr      Transport `json:"trx"`
   329  		Vehicle Vehicle   `json:"v,omitempty"`
   330  		Comment string
   331  		Data    []byte
   332  	}
   333  
   334  	cases := []struct {
   335  		in      interface{}
   336  		want    interface{}
   337  		out     interface{}
   338  		wantErr string
   339  	}{
   340  		0: {
   341  			in: &allInclusive{
   342  				Tr: Transport{
   343  					Vehicle: Boat("Oracle"),
   344  				},
   345  				Comment: "To the Cosmos! баллинг в космос",
   346  				Data:    []byte("祝你好运"),
   347  			},
   348  			out: new(allInclusive),
   349  			want: &allInclusive{
   350  				Tr: Transport{
   351  					Vehicle: Boat("Oracle"),
   352  				},
   353  				Comment: "To the Cosmos! баллинг в космос",
   354  				Data:    []byte("祝你好运"),
   355  			},
   356  		},
   357  
   358  		1: {
   359  			in:   Transport{Vehicle: Plane{Name: "G6", MaxAltitude: 51e3}, Capacity: 18},
   360  			out:  new(Transport),
   361  			want: &Transport{Vehicle: Plane{Name: "G6", MaxAltitude: 51e3}, Capacity: 18},
   362  		},
   363  	}
   364  
   365  	for i, tt := range cases {
   366  		mBlob, err := cdc.MarshalJSON(tt.in)
   367  		if tt.wantErr != "" {
   368  			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
   369  				t.Errorf("#%d:\ngot:\n\t%q\nwant non-nil error containing\n\t%q", i,
   370  					err, tt.wantErr)
   371  			}
   372  			continue
   373  		}
   374  
   375  		if err != nil {
   376  			t.Errorf("#%d: unexpected error after MarshalJSON: %v", i, err)
   377  			continue
   378  		}
   379  
   380  		if err = cdc.UnmarshalJSON(mBlob, tt.out); err != nil {
   381  			t.Errorf("#%d: unexpected error after UnmarshalJSON: %v\nmBlob: %s", i, err, mBlob)
   382  			continue
   383  		}
   384  
   385  		// Now check that the input is exactly equal to the output
   386  		uBlob, err := cdc.MarshalJSON(tt.out)
   387  		assert.NoError(t, err)
   388  		if err := cdc.UnmarshalJSON(mBlob, tt.out); err != nil {
   389  			t.Errorf("#%d: unexpected error after second MarshalJSON: %v", i, err)
   390  			continue
   391  		}
   392  		if !reflect.DeepEqual(tt.want, tt.out) {
   393  			t.Errorf("#%d: After roundtrip UnmarshalJSON\ngot: \t%v\nwant:\t%v", i, tt.out, tt.want)
   394  		}
   395  		if !bytes.Equal(mBlob, uBlob) {
   396  			t.Errorf("#%d: After roundtrip MarshalJSON\ngot: \t%s\nwant:\t%s", i, uBlob, mBlob)
   397  		}
   398  	}
   399  }
   400  
   401  func intPtr(i int) *int {
   402  	return &i
   403  }
   404  
   405  func floatPtr(f float64) *float64 {
   406  	return &f
   407  }
   408  
   409  type (
   410  	noFields         struct{}
   411  	noExportedFields struct {
   412  		a int
   413  		b string
   414  	}
   415  )
   416  
   417  type oneExportedField struct {
   418  	A string
   419  }
   420  
   421  type aPointerField struct {
   422  	Foo  *int
   423  	Name string `json:"nm,omitempty"`
   424  }
   425  
   426  type doublyEmbedded struct {
   427  	Inner *aPointerFieldAndEmbeddedField
   428  	Year  int32 `json:"year"`
   429  }
   430  
   431  type aPointerFieldAndEmbeddedField struct {
   432  	Foo  *int
   433  	Name string `json:"nm,omitempty"`
   434  	*oneExportedField
   435  	B *oneExportedField `json:"bz,omitempty"`
   436  }
   437  
   438  type customJSONMarshaler int
   439  
   440  var _ json.Marshaler = (*customJSONMarshaler)(nil)
   441  
   442  func (cm customJSONMarshaler) MarshalJSON() ([]byte, error) {
   443  	return []byte(`"WRONG"`), nil
   444  }
   445  
   446  type withCustomMarshaler struct {
   447  	F customJSONMarshaler `json:"fx"`
   448  	A *aPointerField
   449  }
   450  
   451  type Transport struct {
   452  	Vehicle
   453  	Capacity int
   454  }
   455  
   456  type Vehicle interface {
   457  	Move() error
   458  }
   459  
   460  type Asset interface {
   461  	Value() float64
   462  }
   463  
   464  func (c Car) Value() float64 {
   465  	return 60000.0
   466  }
   467  
   468  type BalanceSheet struct {
   469  	Assets []Asset `json:"assets"`
   470  }
   471  
   472  type (
   473  	Car   string
   474  	Boat  string
   475  	Plane struct {
   476  		Name        string
   477  		MaxAltitude int64
   478  	}
   479  )
   480  type insurancePlan int
   481  
   482  func (ip insurancePlan) Value() float64 { return float64(ip) }
   483  
   484  func (c Car) Move() error   { return nil }
   485  func (b Boat) Move() error  { return nil }
   486  func (p Plane) Move() error { return nil }
   487  
   488  func interfacePtr(v interface{}) *interface{} {
   489  	return &v
   490  }
   491  
   492  // Test to ensure that Amino codec's time encoding/decoding roundtrip
   493  // produces the same result as the standard library json's.
   494  func TestAminoJSONTimeEncodeDecodeRoundTrip(t *testing.T) {
   495  	t.Parallel()
   496  
   497  	loc, err := time.LoadLocation("America/Los_Angeles")
   498  	require.NoError(t, err)
   499  	din := time.Date(2008, 9, 15, 14, 13, 12, 11109876, loc).Round(time.Millisecond).UTC()
   500  
   501  	cdc := amino.NewCodec()
   502  	blobAmino, err := cdc.MarshalJSON(din)
   503  	require.Nil(t, err, "amino.Codec.MarshalJSON should succeed")
   504  	var tAminoOut time.Time
   505  	require.Nil(t, cdc.UnmarshalJSON(blobAmino, &tAminoOut), "amino.Codec.UnmarshalJSON should succeed")
   506  	require.NotEqual(t, tAminoOut, time.Time{}, "amino.marshaled definitely isn't equal to zero time")
   507  	require.Equal(t, tAminoOut, din, "expecting marshaled in to be equal to marshaled out")
   508  
   509  	blobStdlib, err := json.Marshal(din)
   510  	require.Nil(t, err, "json.Marshal should succeed")
   511  	var tStdlibOut time.Time
   512  	require.Nil(t, json.Unmarshal(blobStdlib, &tStdlibOut), "json.Unmarshal should succeed")
   513  	require.NotEqual(t, tStdlibOut, time.Time{}, "stdlib.marshaled definitely isn't equal to zero time")
   514  	require.Equal(t, tStdlibOut, din, "expecting stdlib.marshaled to be equal to time in")
   515  
   516  	require.Equal(t, tAminoOut, tStdlibOut, "expecting amino.unmarshalled to be equal to json.unmarshalled")
   517  }
   518  
   519  func TestMarshalJSONIndent(t *testing.T) {
   520  	t.Parallel()
   521  
   522  	cdc := amino.NewCodec()
   523  	registerTransports(cdc)
   524  	obj := Transport{Vehicle: Car("Tesla")}
   525  	expected := fmt.Sprintf(`{
   526    "Vehicle": {
   527      "@type": "/amino_test.Car",
   528      "value": "Tesla"
   529    },
   530    "Capacity": "0"
   531  }`)
   532  
   533  	blob, err := cdc.MarshalJSONIndent(obj, "", "  ")
   534  	assert.Nil(t, err)
   535  	assert.Equal(t, expected, string(blob))
   536  }