...

Source file src/github.com/ory/x/decoderx/http_test.go

Documentation: github.com/ory/x/decoderx

     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  				// newsletter represents a special case for checkbox input with true/false and raw HTML.
   134  				"newsletter": {
   135  					"false", // comes from <input type="hidden" name="newsletter" value="false">
   136  					"true",  // comes from <input type="checkbox" name="newsletter" value="true" checked>
   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  				// newsletter represents a special case for checkbox input with true/false and raw HTML.
   156  				"newsletter": {
   157  					"false", // comes from <input type="hidden" name="newsletter" value="false">
   158  					"true",  // comes from <input type="checkbox" name="newsletter" value="true" checked>
   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  				// newsletter represents a special case for checkbox input with true/false and raw HTML.
   177  				"newsletter": {
   178  					"false", // comes from <input type="hidden" name="newsletter" value="false">
   179  					"true",  // comes from <input type="checkbox" name="newsletter" value="true" checked>
   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