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