github.com/stripe/stripe-go/v76@v76.25.0/form/form_test.go (about)

     1  package form
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"sync"
     7  	"testing"
     8  
     9  	assert "github.com/stretchr/testify/require"
    10  )
    11  
    12  type benchStruct struct {
    13  	Bool         bool              `form:"bool"`
    14  	Ignored      string            `form:"-"`
    15  	Int          int64             `form:"int64"`
    16  	String       string            `form:"string"`
    17  	SubSubStruct *testSubSubStruct `form:"subsubstruct"`
    18  }
    19  
    20  type testStruct struct {
    21  	// Note that only a pointer can implement the Appender interface, so only
    22  	// the pointer of testAppender is checked.
    23  	Appender *testAppender `form:"appender"`
    24  
    25  	Array    [3]string  `form:"array"`
    26  	ArrayPtr *[3]string `form:"array_ptr"`
    27  
    28  	Bool    bool  `form:"bool"`
    29  	BoolPtr *bool `form:"bool_ptr"`
    30  
    31  	Emptied bool `form:"emptied,empty"`
    32  
    33  	Float32    float32  `form:"float32"`
    34  	Float32Ptr *float32 `form:"float32_ptr"`
    35  
    36  	Float32Precise    float32  `form:"float32_precise,high_precision"`
    37  	Float32PrecisePtr *float32 `form:"float32_precise_ptr,high_precision"`
    38  
    39  	Float64    float64  `form:"float64"`
    40  	Float64Ptr *float64 `form:"float64_ptr"`
    41  
    42  	Float64Precise    float64  `form:"float64_precise,high_precision"`
    43  	Float64PrecisePtr *float64 `form:"float64_precise_ptr,high_precision"`
    44  
    45  	Ignored string `form:"-"`
    46  
    47  	Int      int    `form:"int"`
    48  	IntPtr   *int   `form:"int_ptr"`
    49  	Int8     int8   `form:"int8"`
    50  	Int8Ptr  *int8  `form:"int8_ptr"`
    51  	Int16    int16  `form:"int16"`
    52  	Int16Ptr *int16 `form:"int16_ptr"`
    53  	Int32    int32  `form:"int32"`
    54  	Int32Ptr *int32 `form:"int32_ptr"`
    55  	Int64    int64  `form:"int64"`
    56  	Int64Ptr *int64 `form:"int64_ptr"`
    57  
    58  	Map map[string]interface{} `form:"map"`
    59  
    60  	Slice    []string  `form:"slice"`
    61  	SlicePtr *[]string `form:"slice_ptr"`
    62  
    63  	String    string  `form:"string"`
    64  	StringPtr *string `form:"string_ptr"`
    65  
    66  	SubStruct    testSubStruct  `form:"substruct"`
    67  	SubStructPtr *testSubStruct `form:"substruct_ptr"`
    68  
    69  	SubStructFlat    testSubStruct  `form:"*"`
    70  	SubStructFlatPtr *testSubStruct `form:"*"`
    71  
    72  	Uuint      uint    `form:"uint"`
    73  	UuintPtr   *uint   `form:"uint_ptr"`
    74  	Uuint8     uint8   `form:"uint8"`
    75  	Uuint8Ptr  *uint8  `form:"uint8_ptr"`
    76  	Uuint16    uint16  `form:"uint16"`
    77  	Uuint16Ptr *uint16 `form:"uint16_ptr"`
    78  	Uuint32    uint32  `form:"uint32"`
    79  	Uuint32Ptr *uint32 `form:"uint32_ptr"`
    80  	Uuint64    uint64  `form:"uint64"`
    81  	Uuint64Ptr *uint64 `form:"uint64_ptr"`
    82  }
    83  
    84  type testAppender struct {
    85  	String string `form:"-"` // Value added manually
    86  }
    87  
    88  func (a *testAppender) AppendTo(values *Values, keyParts []string) {
    89  	values.Add(FormatKey(keyParts), a.String)
    90  }
    91  
    92  type testSubStruct struct {
    93  	SubSubStruct testSubSubStruct `form:"subsubstruct"`
    94  }
    95  
    96  type testSubSubStruct struct {
    97  	String string `form:"string"`
    98  }
    99  
   100  func init() {
   101  	Strict = true
   102  }
   103  
   104  func BenchmarkAppendTo(b *testing.B) {
   105  	// Disable strict mode for the duration of the benchmark (most real
   106  	// installations should not have it turned on)
   107  	Strict = false
   108  	defer func() {
   109  		Strict = true
   110  	}()
   111  
   112  	data := &benchStruct{
   113  		Bool:         true,
   114  		Ignored:      "123",
   115  		Int:          123,
   116  		String:       "123",
   117  		SubSubStruct: &testSubSubStruct{String: "123"},
   118  	}
   119  
   120  	for i := 0; i < b.N; i++ {
   121  		form := &Values{}
   122  		AppendTo(form, data)
   123  	}
   124  }
   125  
   126  func TestAppendTo(t *testing.T) {
   127  	var arrayVal = [3]string{"1", "2", "3"}
   128  	var arrayVal0 = [3]string{}
   129  
   130  	var boolValT = true
   131  	var boolValF = false
   132  
   133  	var float32Val float32 = 1.2345
   134  	var float32Val0 float32
   135  
   136  	var float32PreciseVal float32 = 0.123456789012
   137  
   138  	var float64Val = 1.2345
   139  	var float64Val0 = 0.0
   140  
   141  	var float64PreciseVal = 0.123456789012345678901234
   142  
   143  	var intVal = 123
   144  	var intVal0 = 0
   145  	var int8Val int8 = 123
   146  	var int8Val0 int8
   147  	var int16Val int16 = 123
   148  	var int16Val0 int16
   149  	var int32Val int32 = 123
   150  	var int32Val0 int32
   151  	var int64Val int64 = 123
   152  	var int64Val0 int64
   153  	var sliceVal = []string{"1", "2", "3"}
   154  	var sliceVal0 = []string{}
   155  
   156  	var stringVal = "123"
   157  	var stringVal0 = ""
   158  
   159  	var subStructVal = testSubStruct{
   160  		SubSubStruct: testSubSubStruct{
   161  			String: "123",
   162  		},
   163  	}
   164  
   165  	var uintVal uint = 123
   166  	var uintVal0 uint
   167  	var uint8Val uint8 = 123
   168  	var uint8Val0 uint8
   169  	var uint16Val uint16 = 123
   170  	var uint16Val0 uint16
   171  	var uint32Val uint32 = 123
   172  	var uint32Val0 uint32
   173  	var uint64Val uint64 = 123
   174  	var uint64Val0 uint64
   175  
   176  	testCases := []struct {
   177  		field string
   178  		data  *testStruct
   179  
   180  		// Set to a pointer to the desired value or nil if the value shouldn't
   181  		// be present.
   182  		want *string
   183  	}{
   184  		{"appender", &testStruct{Appender: &testAppender{String: "123"}}, stringPtr("123")},
   185  
   186  		{"array[2]", &testStruct{Array: arrayVal}, stringPtr("3")},
   187  		{"array", &testStruct{Array: arrayVal0}, nil},
   188  
   189  		{"array_ptr[2]", &testStruct{ArrayPtr: &arrayVal}, stringPtr("3")},
   190  		{"array_ptr", &testStruct{ArrayPtr: &arrayVal0}, nil},
   191  
   192  		{"bool", &testStruct{Bool: boolValT}, stringPtr("true")},
   193  		{"bool_ptr", &testStruct{}, nil},
   194  		{"bool_ptr", &testStruct{BoolPtr: &boolValT}, stringPtr("true")},
   195  		{"bool_ptr", &testStruct{BoolPtr: &boolValF}, stringPtr("false")},
   196  
   197  		{"emptied", &testStruct{Emptied: true}, stringPtr("")},
   198  
   199  		{"float32", &testStruct{Float32: float32Val}, stringPtr("1.2345")},
   200  		{"float32_ptr", &testStruct{Float32Ptr: &float32Val}, stringPtr("1.2345")},
   201  		{"float32_ptr", &testStruct{Float32Ptr: &float32Val0}, stringPtr("0.0000")},
   202  		{"float32_ptr", &testStruct{}, nil},
   203  
   204  		// Tests float32 with high precision
   205  		{"float32_precise", &testStruct{Float32Precise: float32PreciseVal}, stringPtr("0.12345679")},
   206  		{"float32_precise_ptr", &testStruct{Float32PrecisePtr: &float32PreciseVal}, stringPtr("0.12345679")},
   207  		{"float32_precise_ptr", &testStruct{Float32PrecisePtr: &float32Val0}, stringPtr("0")},
   208  		{"float32_precise_ptr", &testStruct{}, nil},
   209  
   210  		// The 32-bit test value we're using it already beyond the length of
   211  		// 32-bit precision. The 64-bit value (which starts with the same
   212  		// decimals) is well beyond it, and therefore encodes to the same
   213  		// value.
   214  		{"float32_precise", &testStruct{Float32Precise: float32(float64PreciseVal)}, stringPtr("0.12345679")},
   215  
   216  		{"float64", &testStruct{Float64: float64Val}, stringPtr("1.2345")},
   217  		{"float64_ptr", &testStruct{Float64Ptr: &float64Val}, stringPtr("1.2345")},
   218  		{"float64_ptr", &testStruct{Float64Ptr: &float64Val0}, stringPtr("0.0000")},
   219  		{"float64_ptr", &testStruct{}, nil},
   220  
   221  		// Tests float64 with high precision
   222  		{"float64_precise", &testStruct{Float64Precise: float64PreciseVal}, stringPtr("0.12345678901234568")},
   223  		{"float64_precise_ptr", &testStruct{Float64PrecisePtr: &float64PreciseVal}, stringPtr("0.12345678901234568")},
   224  		{"float64_precise_ptr", &testStruct{Float64PrecisePtr: &float64Val0}, stringPtr("0")},
   225  		{"float64_precise_ptr", &testStruct{}, nil},
   226  
   227  		{"int", &testStruct{Int: intVal}, stringPtr("123")},
   228  		{"int_ptr", &testStruct{IntPtr: &intVal}, stringPtr("123")},
   229  		{"int_ptr", &testStruct{IntPtr: &intVal0}, stringPtr("0")},
   230  		{"int_ptr", &testStruct{}, nil},
   231  		{"int8", &testStruct{Int8: int8Val}, stringPtr("123")},
   232  		{"int8_ptr", &testStruct{Int8Ptr: &int8Val}, stringPtr("123")},
   233  		{"int8_ptr", &testStruct{Int8Ptr: &int8Val0}, stringPtr("0")},
   234  		{"int8_ptr", &testStruct{}, nil},
   235  		{"int16", &testStruct{Int16: int16Val}, stringPtr("123")},
   236  		{"int16_ptr", &testStruct{Int16Ptr: &int16Val}, stringPtr("123")},
   237  		{"int16_ptr", &testStruct{Int16Ptr: &int16Val0}, stringPtr("0")},
   238  		{"int16_ptr", &testStruct{}, nil},
   239  		{"int32", &testStruct{Int32: int32Val}, stringPtr("123")},
   240  		{"int32_ptr", &testStruct{Int32Ptr: &int32Val}, stringPtr("123")},
   241  		{"int32_ptr", &testStruct{Int32Ptr: &int32Val0}, stringPtr("0")},
   242  		{"int32_ptr", &testStruct{}, nil},
   243  		{"int64", &testStruct{Int64: int64Val}, stringPtr("123")},
   244  		{"int64_ptr", &testStruct{Int64Ptr: &int64Val}, stringPtr("123")},
   245  		{"int64_ptr", &testStruct{Int64Ptr: &int64Val0}, stringPtr("0")},
   246  		{"int64_ptr", &testStruct{}, nil},
   247  
   248  		// Tests map
   249  		{
   250  			"map[foo]",
   251  			&testStruct{Map: map[string]interface{}{
   252  				"foo": "bar",
   253  			}},
   254  			stringPtr("bar"),
   255  		},
   256  
   257  		// Tests map with an empty value
   258  		{
   259  			"map[empty]",
   260  			&testStruct{Map: map[string]interface{}{
   261  				// Note that we use an empty integer instead of an empty string
   262  				// here because `Value`''s `Get` implementation will return an
   263  				// empty string for an unset value which means that we can't
   264  				// differentiate between a missing and empty value. The empty
   265  				// value for int64 is 0, so we can.
   266  				"empty": int64(0),
   267  			}},
   268  			stringPtr("0"),
   269  		},
   270  
   271  		// Tests map nested inside of another map
   272  		{
   273  			"map[foo][bar]",
   274  			&testStruct{Map: map[string]interface{}{
   275  				"foo": map[string]interface{}{"bar": "baz"},
   276  			}},
   277  			stringPtr("baz"),
   278  		},
   279  
   280  		{"slice[2]", &testStruct{Slice: sliceVal}, stringPtr("3")},
   281  		{"slice", &testStruct{Slice: []string{}}, stringPtr("")},
   282  		{"slice", &testStruct{Slice: nil}, nil},
   283  
   284  		{"slice_ptr[2]", &testStruct{SlicePtr: &sliceVal}, stringPtr("3")},
   285  
   286  		// A slice pointer given an explicit but empty slice should encode to
   287  		// an empty string (this tells the Stripe API to "zero" the array).
   288  		{"slice_ptr", &testStruct{SlicePtr: &sliceVal0}, stringPtr("")},
   289  
   290  		{"slice_ptr", &testStruct{SlicePtr: nil}, nil},
   291  
   292  		{"string", &testStruct{String: stringVal}, &stringVal},
   293  		{"string_ptr", &testStruct{StringPtr: &stringVal}, &stringVal},
   294  		{"string_ptr", &testStruct{StringPtr: &stringVal0}, &stringVal0},
   295  		{"string_ptr", &testStruct{}, nil},
   296  
   297  		{"substruct[subsubstruct][string]", &testStruct{SubStruct: subStructVal}, stringPtr("123")},
   298  		{"substruct_ptr[subsubstruct][string]", &testStruct{SubStructPtr: &subStructVal}, stringPtr("123")},
   299  
   300  		{"subsubstruct[string]", &testStruct{SubStructFlat: subStructVal}, stringPtr("123")},
   301  		{"subsubstruct[string]", &testStruct{SubStructFlatPtr: &subStructVal}, stringPtr("123")},
   302  
   303  		{"uint", &testStruct{Uuint: uintVal}, stringPtr("123")},
   304  		{"uint_ptr", &testStruct{UuintPtr: &uintVal}, stringPtr("123")},
   305  		{"uint_ptr", &testStruct{UuintPtr: &uintVal0}, stringPtr("0")},
   306  		{"uint_ptr", &testStruct{}, nil},
   307  		{"uint8", &testStruct{Uuint8: uint8Val}, stringPtr("123")},
   308  		{"uint8_ptr", &testStruct{Uuint8Ptr: &uint8Val}, stringPtr("123")},
   309  		{"uint8_ptr", &testStruct{Uuint8Ptr: &uint8Val0}, stringPtr("0")},
   310  		{"uint8_ptr", &testStruct{}, nil},
   311  		{"uint16", &testStruct{Uuint16: uint16Val}, stringPtr("123")},
   312  		{"uint16_ptr", &testStruct{Uuint16Ptr: &uint16Val}, stringPtr("123")},
   313  		{"uint16_ptr", &testStruct{Uuint16Ptr: &uint16Val0}, stringPtr("0")},
   314  		{"uint16_ptr", &testStruct{}, nil},
   315  		{"uint32", &testStruct{Uuint32: uint32Val}, stringPtr("123")},
   316  		{"uint32_ptr", &testStruct{Uuint32Ptr: &uint32Val}, stringPtr("123")},
   317  		{"uint32_ptr", &testStruct{Uuint32Ptr: &uint32Val0}, stringPtr("0")},
   318  		{"uint32_ptr", &testStruct{}, nil},
   319  		{"uint64", &testStruct{Uuint64: uint64Val}, stringPtr("123")},
   320  		{"uint64_ptr", &testStruct{Uuint64Ptr: &uint64Val}, stringPtr("123")},
   321  		{"uint64_ptr", &testStruct{Uuint64Ptr: &uint64Val0}, stringPtr("0")},
   322  		{"uint64_ptr", &testStruct{}, nil},
   323  	}
   324  	for _, tc := range testCases {
   325  		t.Run(tc.field, func(t *testing.T) {
   326  			form := &Values{}
   327  			AppendTo(form, tc.data)
   328  			values := form.ToValues()
   329  			t.Logf("values: %+v encoded: \"%+v\"", values, form.Encode())
   330  
   331  			actuals, ok := values[tc.field]
   332  			if ok {
   333  				if tc.want == nil {
   334  					assert.Fail(t, fmt.Sprintf(
   335  						"Value not expected for %s, but got '%+v'",
   336  						tc.field, actuals))
   337  				}
   338  			} else {
   339  				if tc.want == nil {
   340  					// Got no value and expected no value
   341  					return
   342  				}
   343  
   344  				assert.Fail(t, fmt.Sprintf(
   345  					"No value present for %s, but expected '%+v'",
   346  					tc.field, *tc.want))
   347  			}
   348  
   349  			if len(actuals) > 1 {
   350  				assert.Fail(t, fmt.Sprintf(
   351  					"Got more than one value for %s: %+v",
   352  					tc.field, actuals))
   353  			}
   354  
   355  			actual := actuals[0]
   356  			assert.Equal(t, *tc.want, actual)
   357  		})
   358  	}
   359  }
   360  
   361  func TestAppendTo_IgnoredFields(t *testing.T) {
   362  	form := &Values{}
   363  	data := &testStruct{Ignored: "value"}
   364  	AppendTo(form, data)
   365  	assert.Equal(t, &Values{}, form)
   366  }
   367  
   368  func TestAppendTo_ZeroValues(t *testing.T) {
   369  	form := &Values{}
   370  	data := &testStruct{}
   371  	AppendTo(form, data)
   372  	assert.Equal(t, &Values{}, form)
   373  }
   374  
   375  func TestAppendToPrefixed(t *testing.T) {
   376  	form := &Values{}
   377  	data := &testStruct{String: "foo"}
   378  	AppendToPrefixed(form, data, []string{"prefix"})
   379  	assert.Equal(t, []string{"foo"}, form.Get("prefix[string]"))
   380  }
   381  
   382  func TestEncode(t *testing.T) {
   383  	form := &Values{}
   384  	form.Add("foo", "bar")
   385  	form.Add("foo[bar]", "baz")
   386  	assert.Equal(t, "foo=bar&foo[bar]=baz", form.Encode())
   387  }
   388  
   389  func TestEncodeDeterministically(t *testing.T) {
   390  	data := map[string]string{
   391  		"first":  "foo",
   392  		"second": "bar",
   393  	}
   394  	initialForm := &Values{}
   395  	AppendTo(initialForm, data)
   396  	encoded := initialForm.Encode()
   397  
   398  	for i := 0; i < 100; i++ {
   399  		form := &Values{}
   400  		AppendTo(form, data)
   401  		assert.Equal(t, encoded, form.Encode(), "iteration %d", i)
   402  	}
   403  }
   404  
   405  func TestEncodeMapNonStringKey(t *testing.T) {
   406  	// Disable strict mode for this test, so the non-string key is ignored.
   407  	Strict = false
   408  	defer func() {
   409  		Strict = true
   410  	}()
   411  
   412  	form := &Values{}
   413  	assert.NotPanics(t, func() {
   414  		AppendTo(form, map[int]string{1: "foo"})
   415  	})
   416  	assert.Len(t, form.values, 0)
   417  }
   418  
   419  func TestFormatKey(t *testing.T) {
   420  	assert.Equal(t, "param", FormatKey([]string{"param"}))
   421  	assert.Equal(t, "param[key]", FormatKey([]string{"param", "key"}))
   422  	assert.Equal(t, "param[key][]", FormatKey([]string{"param", "key", ""}))
   423  	assert.Equal(t, "param[key][0]", FormatKey([]string{"param", "key", "0"}))
   424  }
   425  
   426  // The encoder uses a type cache for speed. This test is designed to help
   427  // verify that concurrent access to it is safe.
   428  //
   429  // I had good success in reproducing concurrent access errors on my computer
   430  // using this test, but given that we're just throwing lots of Goroutines out
   431  // there and hoping for an error, your mileage may vary depending on your
   432  // system. It may be necessary to increase the value of `n` or introduce other
   433  // constructs (although hopefully this package will stay concurrency-safe).
   434  func TestCacheConcurrency(t *testing.T) {
   435  	// Clear out anything in the existing cache
   436  	encoderCache.m = nil
   437  	structCache.m = nil
   438  
   439  	var wg sync.WaitGroup
   440  	n := 10
   441  	val := &testStruct{String: "123"}
   442  
   443  	for i := 0; i < n; i++ {
   444  		wg.Add(1)
   445  		go func() {
   446  			form := &Values{}
   447  			AppendTo(form, val)
   448  			wg.Done()
   449  		}()
   450  	}
   451  
   452  	wg.Wait()
   453  }
   454  
   455  func TestParseTag(t *testing.T) {
   456  	// Disable strict mode for the duration of this test so that we can test
   457  	// some malformed tags
   458  	Strict = false
   459  	defer func() {
   460  		Strict = true
   461  	}()
   462  
   463  	testCases := []struct {
   464  		tag         string
   465  		wantName    string
   466  		wantOptions *formOptions
   467  	}{
   468  		{"id", "id", nil},
   469  		{"id,empty", "id", &formOptions{Empty: true}},
   470  
   471  		// invalid invocations
   472  		{"id,", "id", nil},
   473  		{"id,,", "id", nil},
   474  		{"id,foo", "id", nil},
   475  		{"id,foo=bar", "id", nil},
   476  	}
   477  	for _, tc := range testCases {
   478  		t.Run(tc.tag, func(t *testing.T) {
   479  			name, options := parseTag(tc.tag)
   480  			assert.Equal(t, tc.wantName, name)
   481  			assert.Equal(t, tc.wantOptions, options)
   482  		})
   483  	}
   484  }
   485  
   486  func TestValues(t *testing.T) {
   487  	values := &Values{}
   488  
   489  	assert.Equal(t, "", values.Encode())
   490  	assert.True(t, values.Empty())
   491  
   492  	values = &Values{}
   493  	values.Add("foo", "bar")
   494  
   495  	assert.Equal(t, "foo=bar", values.Encode())
   496  	assert.False(t, values.Empty())
   497  	assert.Equal(t, []string{"bar"}, values.Get("foo"))
   498  
   499  	values = &Values{}
   500  	values.Add("foo", "bar")
   501  	values.Add("foo", "bar")
   502  	values.Add("baz", "bar")
   503  
   504  	assert.Equal(t, "foo=bar&foo=bar&baz=bar", values.Encode())
   505  	assert.Equal(t, []string{"bar", "bar"}, values.Get("foo"))
   506  	assert.Equal(t, []string{"bar"}, values.Get("baz"))
   507  
   508  	values.Set("foo", "firstbar")
   509  
   510  	assert.Equal(t, "foo=firstbar&foo=bar&baz=bar", values.Encode())
   511  	assert.Equal(t, []string{"firstbar", "bar"}, values.Get("foo"))
   512  	assert.Equal(t, []string{"bar"}, values.Get("baz"))
   513  
   514  	values.Set("new", "appended")
   515  
   516  	assert.Equal(t, "foo=firstbar&foo=bar&baz=bar&new=appended", values.Encode())
   517  
   518  	assert.Equal(t, url.Values{
   519  		"baz": {"bar"},
   520  		"foo": {"firstbar", "bar"},
   521  		"new": {"appended"},
   522  	}, values.ToValues())
   523  	assert.Equal(t, []string{"appended"}, values.Get("new"))
   524  
   525  	assert.Nil(t, values.Get("boguskey"))
   526  }
   527  
   528  //
   529  // Private functions
   530  //
   531  
   532  // Trivial helper to get is a string pointer from a string (because you're not
   533  // allowed to take the address of a literal directly).
   534  func stringPtr(s string) *string {
   535  	return &s
   536  }