github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/rpc/jsonrpc/server/parse_test.go (about) 1 package server 2 3 import ( 4 "context" 5 "encoding/json" 6 "net/http" 7 "strconv" 8 "testing" 9 10 "github.com/stretchr/testify/assert" 11 12 "github.com/ari-anchor/sei-tendermint/libs/bytes" 13 ) 14 15 func TestParseJSONMap(t *testing.T) { 16 input := []byte(`{"value":"1234","height":22}`) 17 18 // naive is float,string 19 var p1 map[string]interface{} 20 err := json.Unmarshal(input, &p1) 21 if assert.NoError(t, err) { 22 h, ok := p1["height"].(float64) 23 if assert.True(t, ok, "%#v", p1["height"]) { 24 assert.EqualValues(t, 22, h) 25 } 26 v, ok := p1["value"].(string) 27 if assert.True(t, ok, "%#v", p1["value"]) { 28 assert.EqualValues(t, "1234", v) 29 } 30 } 31 32 // preloading map with values doesn't help 33 tmp := 0 34 p2 := map[string]interface{}{ 35 "value": &bytes.HexBytes{}, 36 "height": &tmp, 37 } 38 err = json.Unmarshal(input, &p2) 39 if assert.NoError(t, err) { 40 h, ok := p2["height"].(float64) 41 if assert.True(t, ok, "%#v", p2["height"]) { 42 assert.EqualValues(t, 22, h) 43 } 44 v, ok := p2["value"].(string) 45 if assert.True(t, ok, "%#v", p2["value"]) { 46 assert.EqualValues(t, "1234", v) 47 } 48 } 49 50 // preload here with *pointers* to the desired types 51 // struct has unknown types, but hard-coded keys 52 tmp = 0 53 p3 := struct { 54 Value interface{} `json:"value"` 55 Height interface{} `json:"height"` 56 }{ 57 Height: &tmp, 58 Value: &bytes.HexBytes{}, 59 } 60 err = json.Unmarshal(input, &p3) 61 if assert.NoError(t, err) { 62 h, ok := p3.Height.(*int) 63 if assert.True(t, ok, "%#v", p3.Height) { 64 assert.Equal(t, 22, *h) 65 } 66 v, ok := p3.Value.(*bytes.HexBytes) 67 if assert.True(t, ok, "%#v", p3.Value) { 68 assert.EqualValues(t, []byte{0x12, 0x34}, *v) 69 } 70 } 71 72 // simplest solution, but hard-coded 73 p4 := struct { 74 Value bytes.HexBytes `json:"value"` 75 Height int `json:"height"` 76 }{} 77 err = json.Unmarshal(input, &p4) 78 if assert.NoError(t, err) { 79 assert.EqualValues(t, 22, p4.Height) 80 assert.EqualValues(t, []byte{0x12, 0x34}, p4.Value) 81 } 82 83 // so, let's use this trick... 84 // dynamic keys on map, and we can deserialize to the desired types 85 var p5 map[string]*json.RawMessage 86 err = json.Unmarshal(input, &p5) 87 if assert.NoError(t, err) { 88 var h int 89 err = json.Unmarshal(*p5["height"], &h) 90 if assert.NoError(t, err) { 91 assert.Equal(t, 22, h) 92 } 93 94 var v bytes.HexBytes 95 err = json.Unmarshal(*p5["value"], &v) 96 if assert.NoError(t, err) { 97 assert.Equal(t, bytes.HexBytes{0x12, 0x34}, v) 98 } 99 } 100 } 101 102 func TestParseJSONArray(t *testing.T) { 103 input := []byte(`["1234",22]`) 104 105 // naive is float,string 106 var p1 []interface{} 107 err := json.Unmarshal(input, &p1) 108 if assert.NoError(t, err) { 109 v, ok := p1[0].(string) 110 if assert.True(t, ok, "%#v", p1[0]) { 111 assert.EqualValues(t, "1234", v) 112 } 113 h, ok := p1[1].(float64) 114 if assert.True(t, ok, "%#v", p1[1]) { 115 assert.EqualValues(t, 22, h) 116 } 117 } 118 119 // preloading map with values helps here (unlike map - p2 above) 120 tmp := 0 121 p2 := []interface{}{&bytes.HexBytes{}, &tmp} 122 err = json.Unmarshal(input, &p2) 123 if assert.NoError(t, err) { 124 v, ok := p2[0].(*bytes.HexBytes) 125 if assert.True(t, ok, "%#v", p2[0]) { 126 assert.EqualValues(t, []byte{0x12, 0x34}, *v) 127 } 128 h, ok := p2[1].(*int) 129 if assert.True(t, ok, "%#v", p2[1]) { 130 assert.EqualValues(t, 22, *h) 131 } 132 } 133 } 134 135 func TestParseJSONRPC(t *testing.T) { 136 type demoArgs struct { 137 Height int `json:"height,string"` 138 Name string `json:"name"` 139 } 140 demo := func(ctx context.Context, _ *demoArgs) error { return nil } 141 rfunc := NewRPCFunc(demo) 142 143 cases := []struct { 144 raw string 145 height int64 146 name string 147 fail bool 148 }{ 149 // should parse 150 {`["7", "flew"]`, 7, "flew", false}, 151 {`{"name": "john", "height": "22"}`, 22, "john", false}, 152 // defaults 153 {`{"name": "solo", "unused": "stuff"}`, 0, "solo", false}, 154 // should fail - wrong types/length 155 {`["flew", 7]`, 0, "", true}, 156 {`[7,"flew",100]`, 0, "", true}, 157 {`{"name": -12, "height": "fred"}`, 0, "", true}, 158 } 159 ctx := context.Background() 160 for idx, tc := range cases { 161 i := strconv.Itoa(idx) 162 vals, err := rfunc.parseParams(ctx, []byte(tc.raw)) 163 if tc.fail { 164 assert.Error(t, err, i) 165 } else { 166 assert.NoError(t, err, "%s: %+v", i, err) 167 assert.Equal(t, 2, len(vals), i) 168 p, ok := vals[1].Interface().(*demoArgs) 169 if assert.True(t, ok) { 170 assert.Equal(t, tc.height, int64(p.Height), i) 171 assert.Equal(t, tc.name, p.Name, i) 172 } 173 } 174 175 } 176 } 177 178 func TestParseURI(t *testing.T) { 179 // URI parameter parsing happens in two phases: 180 // 181 // Phase 1 swizzles the query parameters into JSON. The result of this 182 // phase must be valid JSON, but may fail the second stage. 183 // 184 // Phase 2 decodes the JSON to obtain the actual arguments. A failure at 185 // this stage means the JSON is not compatible with the target. 186 187 t.Run("Swizzle", func(t *testing.T) { 188 tests := []struct { 189 name string 190 url string 191 args []argInfo 192 want string 193 fail bool 194 }{ 195 { 196 name: "quoted numbers and strings", 197 url: `http://localhost?num="7"&str="flew"&neg="-10"`, 198 args: []argInfo{{name: "neg"}, {name: "num"}, {name: "str"}, {name: "other"}}, 199 want: `{"neg":-10,"num":7,"str":"flew"}`, 200 }, 201 { 202 name: "unquoted numbers and strings", 203 url: `http://localhost?num1=7&str1=cabbage&num2=-199&str2=hey+you`, 204 args: []argInfo{{name: "num1"}, {name: "num2"}, {name: "str1"}, {name: "str2"}, {name: "other"}}, 205 want: `{"num1":7,"num2":-199,"str1":"cabbage","str2":"hey you"}`, 206 }, 207 { 208 name: "quoted byte strings", 209 url: `http://localhost?left="Fahrvergnügen"&right="Applesauce"`, 210 args: []argInfo{{name: "left", isBinary: true}, {name: "right", isBinary: false}}, 211 want: `{"left":"RmFocnZlcmduw7xnZW4=","right":"Applesauce"}`, 212 }, 213 { 214 name: "hexadecimal byte strings", 215 url: `http://localhost?lower=0x626f62&upper=0X646F7567`, 216 args: []argInfo{{name: "upper", isBinary: true}, {name: "lower", isBinary: false}, {name: "other"}}, 217 want: `{"lower":"bob","upper":"ZG91Zw=="}`, 218 }, 219 { 220 name: "invalid hex odd length", 221 url: `http://localhost?bad=0xa`, 222 args: []argInfo{{name: "bad"}, {name: "superbad"}}, 223 fail: true, 224 }, 225 { 226 name: "invalid hex empty", 227 url: `http://localhost?bad=0x`, 228 args: []argInfo{{name: "bad"}}, 229 fail: true, 230 }, 231 { 232 name: "invalid quoted string", 233 url: `http://localhost?bad="double""`, 234 args: []argInfo{{name: "bad"}}, 235 fail: true, 236 }, 237 } 238 for _, test := range tests { 239 t.Run(test.name, func(t *testing.T) { 240 hreq, err := http.NewRequest("GET", test.url, nil) 241 if err != nil { 242 t.Fatalf("NewRequest for %q: %v", test.url, err) 243 } 244 245 bits, err := parseURLParams(test.args, hreq) 246 if err != nil && !test.fail { 247 t.Fatalf("Parse %q: unexpected error: %v", test.url, err) 248 } else if err == nil && test.fail { 249 t.Fatalf("Parse %q: got %#q, wanted error", test.url, string(bits)) 250 } 251 if got := string(bits); got != test.want { 252 t.Errorf("Parse %q: got %#q, want %#q", test.url, got, test.want) 253 } 254 }) 255 } 256 }) 257 258 t.Run("Decode", func(t *testing.T) { 259 type argValue struct { 260 Height json.Number `json:"height"` 261 Name string `json:"name"` 262 Flag bool `json:"flag"` 263 } 264 265 echo := NewRPCFunc(func(_ context.Context, arg *argValue) (*argValue, error) { 266 return arg, nil 267 }) 268 269 tests := []struct { 270 name string 271 url string 272 fail string 273 want interface{} 274 }{ 275 { 276 name: "valid all args", 277 url: `http://localhost?height=235&flag=true&name="bogart"`, 278 want: &argValue{ 279 Height: "235", 280 Flag: true, 281 Name: "bogart", 282 }, 283 }, 284 { 285 name: "valid partial args", 286 url: `http://localhost?height="1987"&name=free+willy`, 287 want: &argValue{ 288 Height: "1987", 289 Name: "free willy", 290 }, 291 }, 292 { 293 name: "invalid quoted number", 294 url: `http://localhost?height="-xx"`, 295 fail: "invalid number literal", 296 }, 297 { 298 name: "invalid unquoted number", 299 url: `http://localhost?height=25*q`, 300 fail: "invalid number literal", 301 }, 302 { 303 name: "invalid boolean", 304 url: `http://localhost?flag="garbage"`, 305 fail: "flag of type bool", 306 }, 307 } 308 for _, test := range tests { 309 t.Run(test.name, func(t *testing.T) { 310 hreq, err := http.NewRequest("GET", test.url, nil) 311 if err != nil { 312 t.Fatalf("NewRequest for %q: %v", test.url, err) 313 } 314 bits, err := parseURLParams(echo.args, hreq) 315 if err != nil { 316 t.Fatalf("Parse %#q: unexpected error: %v", test.url, err) 317 } 318 rsp, err := echo.Call(context.Background(), bits) 319 if test.want != nil { 320 assert.Equal(t, test.want, rsp) 321 } 322 if test.fail != "" { 323 assert.ErrorContains(t, err, test.fail) 324 } 325 }) 326 } 327 }) 328 }