github.com/icyphox/x@v0.0.355-0.20220311094250-029bd783e8b8/decoderx/http_test.go (about) 1 package decoderx 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "net/http/httptest" 11 "net/url" 12 "sync" 13 "testing" 14 15 "github.com/ory/x/assertx" 16 17 "github.com/tidwall/gjson" 18 19 "github.com/pkg/errors" 20 "github.com/stretchr/testify/assert" 21 "github.com/stretchr/testify/require" 22 23 "github.com/ory/jsonschema/v3" 24 ) 25 26 var ctx = context.Background() 27 28 func newRequest(t *testing.T, method, url string, body io.Reader, ct string) *http.Request { 29 req := httptest.NewRequest(method, url, body) 30 req.Header.Set("Content-Type", ct) 31 return req 32 } 33 34 func TestHTTPFormDecoder(t *testing.T) { 35 for k, tc := range []struct { 36 d string 37 request *http.Request 38 contentType string 39 options []HTTPDecoderOption 40 expected string 41 expectedError string 42 }{ 43 { 44 d: "should fail because the method is GET", 45 request: &http.Request{Header: map[string][]string{}, Method: "GET"}, 46 expectedError: "HTTP Request Method", 47 }, 48 { 49 d: "should fail because the body is empty", 50 request: &http.Request{Header: map[string][]string{}, Method: "POST"}, 51 expectedError: "Content-Length", 52 }, 53 { 54 d: "should fail because content type is missing", 55 request: newRequest(t, "POST", "/", nil, ""), 56 expectedError: "Content-Length", 57 }, 58 { 59 d: "should fail because content type is missing", 60 request: newRequest(t, "POST", "/", bytes.NewBufferString("foo"), ""), 61 expectedError: "Content-Type", 62 }, 63 { 64 d: "should pass with json without validation", 65 request: newRequest(t, "POST", "/", bytes.NewBufferString(`{"foo":"bar"}`), httpContentTypeJSON), 66 expected: `{"foo":"bar"}`, 67 }, 68 { 69 d: "should fail json if content type is not accepted", 70 request: newRequest(t, "POST", "/", bytes.NewBufferString(`{"foo":"bar"}`), httpContentTypeJSON), 71 options: []HTTPDecoderOption{HTTPFormDecoder()}, 72 expectedError: "Content-Type: application/json", 73 }, 74 { 75 d: "should fail json if validation fails", 76 request: newRequest(t, "POST", "/", bytes.NewBufferString(`{"foo":"bar", "bar":"baz"}`), httpContentTypeJSON), 77 options: []HTTPDecoderOption{HTTPJSONDecoder(), MustHTTPRawJSONSchemaCompiler([]byte(`{ 78 "$id": "https://example.com/config.schema.json", 79 "$schema": "http://json-schema.org/draft-07/schema#", 80 "type": "object", 81 "properties": { 82 "foo": { 83 "type": "number" 84 }, 85 "bar": { 86 "type": "string" 87 } 88 } 89 }`), 90 )}, 91 expectedError: "expected number, but got string", 92 expected: `{ "bar": "baz", "foo": "bar" }`, 93 }, 94 { 95 d: "should pass json with validation", 96 request: newRequest(t, "POST", "/", bytes.NewBufferString(`{"foo":"bar"}`), httpContentTypeJSON), 97 options: []HTTPDecoderOption{HTTPJSONDecoder(), MustHTTPRawJSONSchemaCompiler([]byte(`{ 98 "$id": "https://example.com/config.schema.json", 99 "$schema": "http://json-schema.org/draft-07/schema#", 100 "type": "object", 101 "properties": { 102 "foo": { 103 "type": "string" 104 } 105 } 106 }`), 107 ), 108 }, 109 expected: `{"foo":"bar"}`, 110 }, 111 { 112 d: "should fail form request when form is used but only json is allowed", 113 request: newRequest(t, "POST", "/", bytes.NewBufferString(url.Values{"foo": {"bar"}}.Encode()), httpContentTypeURLEncodedForm), 114 options: []HTTPDecoderOption{HTTPJSONDecoder()}, 115 expectedError: "Content-Type: application/x-www-form-urlencoded", 116 }, 117 { 118 d: "should fail form request when schema is missing", 119 request: newRequest(t, "POST", "/", bytes.NewBufferString(url.Values{"foo": {"bar"}}.Encode()), httpContentTypeURLEncodedForm), 120 options: []HTTPDecoderOption{}, 121 expectedError: "no validation schema was provided", 122 }, 123 { 124 d: "should fail form request when schema does not validate request", 125 request: newRequest(t, "POST", "/", bytes.NewBufferString(url.Values{"bar": {"bar"}}.Encode()), httpContentTypeURLEncodedForm), 126 options: []HTTPDecoderOption{HTTPJSONSchemaCompiler("stub/schema.json", nil)}, 127 expectedError: `missing properties: "foo"`, 128 }, 129 { 130 d: "should pass form request and type assert data", 131 request: newRequest(t, "POST", "/", bytes.NewBufferString(url.Values{ 132 "name.first": {"Aeneas"}, 133 "name.last": {"Rekkas"}, 134 "age": {"29"}, 135 "ratio": {"0.9"}, 136 "consent": {"true"}, 137 138 // newsletter represents a special case for checkbox input with true/false and raw HTML. 139 "newsletter": { 140 "false", // comes from <input type="hidden" name="newsletter" value="false"> 141 "true", // comes from <input type="checkbox" name="newsletter" value="true" checked> 142 }, 143 }.Encode()), httpContentTypeURLEncodedForm), 144 options: []HTTPDecoderOption{HTTPJSONSchemaCompiler("stub/person.json", nil)}, 145 expected: `{ 146 "name": {"first": "Aeneas", "last": "Rekkas"}, 147 "age": 29, 148 "newsletter": true, 149 "consent": true, 150 "ratio": 0.9 151 }`, 152 }, 153 { 154 d: "should mark the correct fields when nested objects are required", 155 request: newRequest(t, "POST", "/", bytes.NewBufferString(url.Values{ 156 // newsletter represents a special case for checkbox input with true/false and raw HTML. 157 "foo": {"bar"}, 158 }.Encode()), httpContentTypeURLEncodedForm), 159 options: []HTTPDecoderOption{ 160 HTTPJSONSchemaCompiler("stub/consent.json", nil), 161 HTTPKeepRequestBody(true), 162 HTTPDecoderSetValidatePayloads(false), 163 HTTPDecoderUseQueryAndBody(), 164 HTTPDecoderAllowedMethods("POST", "GET"), 165 HTTPDecoderJSONFollowsFormFormat(), 166 }, 167 expected: `{ 168 "traits": { 169 "consent": { 170 "inner": {} 171 }, 172 "notrequired": {} 173 } 174 }`, 175 }, 176 { 177 d: "should pass form request with payload in query and type assert data", 178 request: newRequest(t, "POST", "/?age=29", bytes.NewBufferString(url.Values{ 179 "name.first": {"Aeneas"}, 180 "name.last": {"Rekkas"}, 181 "ratio": {"0.9"}, 182 "consent": {"true"}, 183 // newsletter represents a special case for checkbox input with true/false and raw HTML. 184 "newsletter": { 185 "false", // comes from <input type="hidden" name="newsletter" value="false"> 186 "true", // comes from <input type="checkbox" name="newsletter" value="true" checked> 187 }, 188 }.Encode()), httpContentTypeURLEncodedForm), 189 options: []HTTPDecoderOption{HTTPJSONSchemaCompiler("stub/person.json", nil)}, 190 expected: `{ 191 "name": {"first": "Aeneas", "last": "Rekkas"}, 192 "newsletter": true, 193 "consent": true, 194 "ratio": 0.9 195 }`, 196 }, 197 { 198 d: "should pass form request with payload in query and type assert data", 199 request: newRequest(t, "POST", "/?age=29", bytes.NewBufferString(url.Values{ 200 "name.first": {"Aeneas"}, 201 "name.last": {"Rekkas"}, 202 "ratio": {"0.9"}, 203 "consent": {"true"}, 204 // newsletter represents a special case for checkbox input with true/false and raw HTML. 205 "newsletter": { 206 "false", // comes from <input type="hidden" name="newsletter" value="false"> 207 "true", // comes from <input type="checkbox" name="newsletter" value="true" checked> 208 }, 209 }.Encode()), httpContentTypeURLEncodedForm), 210 options: []HTTPDecoderOption{ 211 HTTPDecoderUseQueryAndBody(), 212 HTTPJSONSchemaCompiler("stub/person.json", nil), 213 }, 214 expected: `{ 215 "name": {"first": "Aeneas", "last": "Rekkas"}, 216 "age": 29, 217 "newsletter": true, 218 "consent": true, 219 "ratio": 0.9 220 }`, 221 }, 222 { 223 d: "should fail form request if empty values are sent because of required fields", 224 request: newRequest(t, "POST", "/?age=29", bytes.NewBufferString(url.Values{ 225 "name.first": {""}, 226 "name.last": {""}, 227 "name2.first": {""}, 228 "name2.last": {""}, 229 "ratio": {""}, 230 "ratio2": {""}, 231 "age": {""}, 232 "age2": {""}, 233 "consent": {""}, 234 "consent2": {""}, 235 // newsletter represents a special case for checkbox input with true/false and raw HTML. 236 "newsletter": {""}, 237 "newsletter2": {""}, 238 }.Encode()), httpContentTypeURLEncodedForm), 239 options: []HTTPDecoderOption{ 240 HTTPDecoderUseQueryAndBody(), 241 HTTPJSONSchemaCompiler("stub/required-defaults.json", nil), 242 }, 243 expectedError: `I[#/name2] S[#/properties/name2/required] missing properties: "first"`, 244 }, 245 { 246 d: "should fail json request formatted as form if payload is invalid", 247 request: newRequest(t, "POST", "/", bytes.NewBufferString(`{"name.first":"Aeneas", "name.last":"Rekkas","age":"not-a-number"}`), httpContentTypeJSON), 248 options: []HTTPDecoderOption{HTTPJSONSchemaCompiler("stub/person.json", nil)}, 249 expectedError: "expected integer, but got string", 250 }, 251 { 252 d: "should pass JSON request formatted as a form", 253 request: newRequest(t, "POST", "/", bytes.NewBufferString(`{ 254 "name.first": "Aeneas", 255 "name.last": "Rekkas", 256 "age": 29, 257 "ratio": 0.9, 258 "consent": false, 259 "newsletter": true 260 }`), httpContentTypeJSON), 261 options: []HTTPDecoderOption{HTTPDecoderJSONFollowsFormFormat(), 262 HTTPJSONSchemaCompiler("stub/person.json", nil)}, 263 expected: `{ 264 "name": {"first": "Aeneas", "last": "Rekkas"}, 265 "age": 29, 266 "newsletter": true, 267 "consent": false, 268 "ratio": 0.9 269 }`, 270 }, 271 { 272 d: "should pass JSON request formatted as a form", 273 request: newRequest(t, "POST", "/?age=29", bytes.NewBufferString(`{ 274 "name.first": "Aeneas", 275 "name.last": "Rekkas", 276 "ratio": 0.9, 277 "consent": false, 278 "newsletter": true 279 }`), httpContentTypeJSON), 280 options: []HTTPDecoderOption{HTTPDecoderJSONFollowsFormFormat(), 281 HTTPJSONSchemaCompiler("stub/person.json", nil)}, 282 expected: `{ 283 "name": {"first": "Aeneas", "last": "Rekkas"}, 284 "newsletter": true, 285 "consent": false, 286 "ratio": 0.9 287 }`, 288 }, 289 { 290 d: "should pass JSON request formatted as a JSON even if HTTPDecoderJSONFollowsFormFormat is used", 291 request: newRequest(t, "POST", "/?age=29", bytes.NewBufferString(`{ 292 "name": {"first": "Aeneas", "last": "Rekkas"}, 293 "ratio": 0.9, 294 "consent": false, 295 "newsletter": true 296 }`), httpContentTypeJSON), 297 options: []HTTPDecoderOption{HTTPDecoderJSONFollowsFormFormat(), 298 HTTPJSONSchemaCompiler("stub/person.json", nil)}, 299 expected: `{ 300 "name": {"first": "Aeneas", "last": "Rekkas"}, 301 "newsletter": true, 302 "consent": false, 303 "ratio": 0.9 304 }`, 305 }, 306 { 307 d: "should not retry indefinitely if key does not exist", 308 request: newRequest(t, "POST", "/?age=29", bytes.NewBufferString(`{ 309 "not-foo": "bar" 310 }`), httpContentTypeJSON), 311 options: []HTTPDecoderOption{HTTPDecoderJSONFollowsFormFormat(), 312 HTTPJSONSchemaCompiler("stub/schema.json", nil)}, 313 expectedError: "I[#] S[#/required] missing properties", 314 }, 315 { 316 d: "should indicate the true missing fields from nested form", 317 request: newRequest(t, "POST", "/", bytes.NewBufferString(url.Values{"leaf": {"foo"}}.Encode()), httpContentTypeURLEncodedForm), 318 options: []HTTPDecoderOption{ 319 HTTPDecoderUseQueryAndBody(), 320 HTTPDecoderSetIgnoreParseErrorsStrategy(ParseErrorIgnoreConversionErrors), 321 HTTPJSONSchemaCompiler("stub/nested.json", nil)}, 322 expectedError: `I[#/node/node/node] S[#/properties/node/properties/node/properties/node/required] missing properties: "leaf"`, 323 }, 324 { 325 d: "should pass JSON request formatted as a form", 326 request: newRequest(t, "POST", "/?age=29", bytes.NewBufferString(`{ 327 "name.first": "Aeneas", 328 "name.last": "Rekkas", 329 "ratio": 0.9, 330 "consent": false, 331 "newsletter": true 332 }`), httpContentTypeJSON), 333 options: []HTTPDecoderOption{ 334 HTTPDecoderUseQueryAndBody(), 335 HTTPDecoderJSONFollowsFormFormat(), 336 HTTPJSONSchemaCompiler("stub/person.json", nil)}, 337 expected: `{ 338 "name": {"first": "Aeneas", "last": "Rekkas"}, 339 "age": 29, 340 "newsletter": true, 341 "consent": false, 342 "ratio": 0.9 343 }`, 344 }, 345 { 346 d: "should pass JSON request GET request", 347 request: newRequest(t, "GET", "/?"+url.Values{ 348 "name.first": {"Aeneas"}, 349 "name.last": {"Rekkas"}, 350 "age": {"29"}, 351 "ratio": {"0.9"}, 352 "consent": {"false"}, 353 "newsletter": {"true"}, 354 }.Encode(), nil, ""), 355 options: []HTTPDecoderOption{ 356 HTTPJSONSchemaCompiler("stub/person.json", nil), 357 HTTPDecoderAllowedMethods("GET"), 358 }, 359 expected: `{ 360 "name": {"first": "Aeneas", "last": "Rekkas"}, 361 "age": 29, 362 "newsletter": true, 363 "consent": false, 364 "ratio": 0.9 365 }`, 366 }, 367 { 368 d: "should fail because json is not an object when using form format", 369 request: newRequest(t, "POST", "/", bytes.NewBufferString(`[]`), httpContentTypeJSON), 370 options: []HTTPDecoderOption{HTTPDecoderJSONFollowsFormFormat(), 371 HTTPJSONSchemaCompiler("stub/person.json", nil)}, 372 expectedError: "be an object", 373 }, 374 { 375 d: "should work with ParseErrorIgnoreConversionErrors", 376 request: newRequest(t, "POST", "/", bytes.NewBufferString(url.Values{ 377 "ratio": {"foobar"}, 378 }.Encode()), httpContentTypeURLEncodedForm), 379 options: []HTTPDecoderOption{ 380 HTTPJSONSchemaCompiler("stub/person.json", nil), 381 HTTPDecoderSetIgnoreParseErrorsStrategy(ParseErrorIgnoreConversionErrors), 382 HTTPDecoderSetValidatePayloads(false), 383 }, 384 expected: `{"name": {}, "ratio": "foobar"}`, 385 }, 386 { 387 d: "should work with ParseErrorIgnoreConversionErrors", 388 request: newRequest(t, "POST", "/", bytes.NewBufferString(url.Values{ 389 "ratio": {"foobar"}, 390 }.Encode()), httpContentTypeURLEncodedForm), 391 options: []HTTPDecoderOption{HTTPJSONSchemaCompiler("stub/person.json", nil), HTTPDecoderSetIgnoreParseErrorsStrategy(ParseErrorUseEmptyValueOnConversionErrors)}, 392 expected: `{"name": {}, "ratio": 0.0}`, 393 }, 394 { 395 d: "should work with ParseErrorIgnoreConversionErrors", 396 request: newRequest(t, "POST", "/", bytes.NewBufferString(url.Values{ 397 "ratio": {"foobar"}, 398 }.Encode()), httpContentTypeURLEncodedForm), 399 options: []HTTPDecoderOption{HTTPJSONSchemaCompiler("stub/person.json", nil), HTTPDecoderSetIgnoreParseErrorsStrategy(ParseErrorReturnOnConversionErrors)}, 400 expectedError: `strconv.ParseFloat: parsing "foobar"`, 401 }, 402 { 403 d: "should interpret numbers as string if mandated by the schema", 404 request: newRequest(t, "POST", "/", bytes.NewBufferString(url.Values{ 405 "name.first": {"12345"}, 406 }.Encode()), httpContentTypeURLEncodedForm), 407 options: []HTTPDecoderOption{HTTPJSONSchemaCompiler("stub/person.json", nil), HTTPDecoderSetIgnoreParseErrorsStrategy(ParseErrorUseEmptyValueOnConversionErrors)}, 408 expected: `{"name": {"first": "12345"}}`, 409 }, 410 } { 411 t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { 412 dec := NewHTTP() 413 var destination json.RawMessage 414 err := dec.Decode(tc.request, &destination, tc.options...) 415 if tc.expectedError != "" { 416 if e, ok := errors.Cause(err).(*jsonschema.ValidationError); ok { 417 t.Logf("%+v", e) 418 } 419 require.Error(t, err) 420 require.Contains(t, fmt.Sprintf("%+v", err), tc.expectedError) 421 if len(tc.expected) > 0 { 422 assert.JSONEq(t, tc.expected, string(destination)) 423 } 424 return 425 } 426 427 require.NoError(t, err) 428 assertx.EqualAsJSON(t, json.RawMessage(tc.expected), destination) 429 }) 430 } 431 432 t.Run("description=read body twice", func(t *testing.T) { 433 var wg sync.WaitGroup 434 wg.Add(1) 435 436 dec := NewHTTP() 437 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 438 defer wg.Done() 439 440 var destination json.RawMessage 441 require.NoError(t, dec.Decode(r, &destination, HTTPJSONSchemaCompiler("stub/person.json", nil), HTTPKeepRequestBody(true))) 442 assert.EqualValues(t, "12345", gjson.GetBytes(destination, "name.first").String()) 443 444 require.NoError(t, dec.Decode(r, &destination, HTTPJSONSchemaCompiler("stub/person.json", nil), HTTPKeepRequestBody(true))) 445 assert.EqualValues(t, "12345", gjson.GetBytes(destination, "name.first").String()) 446 })) 447 t.Cleanup(ts.Close) 448 449 _, err := ts.Client().PostForm(ts.URL, url.Values{"name.first": {"12345"}}) 450 require.NoError(t, err) 451 452 wg.Wait() 453 }) 454 }