...

Source file src/github.com/launchdarkly/go-sdk-common/v3/ldcontext/context_unmarshaling_test.go

Documentation: github.com/launchdarkly/go-sdk-common/v3/ldcontext

     1  package ldcontext
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
    10  
    11  	"github.com/launchdarkly/go-jsonstream/v3/jreader"
    12  
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  )
    16  
    17  func makeContextUnmarshalUnimportantVariantsParams() []contextSerializationParams {
    18  	// These are test cases that only apply to unmarshaling, because marshaling will never produce this specific JSON.
    19  	return []contextSerializationParams{
    20  		// explicit null is same as unset for optional string attrs
    21  		{NewBuilder("my-key").Build(),
    22  			`{"kind": "user", "key": "my-key", "name": null}`},
    23  
    24  		// explicit null is same as unset for custom attrs
    25  		{NewBuilder("my-key").Build(),
    26  			`{"kind": "user", "key": "my-key", "customAttr": null}`},
    27  
    28  		// explicit false is same as unset for anonymous
    29  		{NewBuilder("my-key").Build(),
    30  			`{"kind": "user", "key": "my-key", "anonymous": false}`},
    31  
    32  		// _meta: {} is same as no _meta
    33  		{NewBuilder("my-key").Build(),
    34  			`{"kind": "user", "key": "my-key", "_meta": {}}`},
    35  
    36  		// _meta: null is same as no _meta
    37  		{NewBuilder("my-key").Build(),
    38  			`{"kind": "user", "key": "my-key", "_meta": null}`},
    39  
    40  		// privateAttributes: [] is same as no privateAttributes
    41  		{NewBuilder("my-key").Build(),
    42  			`{"kind": "user", "key": "my-key", "_meta": {"privateAttributes": null}}`},
    43  
    44  		// privateAttributes: null is same as no privateAttributes
    45  		{NewBuilder("my-key").Build(),
    46  			`{"kind": "user", "key": "my-key", "_meta": {"privateAttributes": null}}`},
    47  
    48  		// unrecognized properties within _meta are ignored
    49  		{NewBuilder("my-key").Build(),
    50  			`{"kind": "user", "key": "my-key", "_meta": {"unknownProp": false}}`},
    51  
    52  		// redactedAttributes is only a thing in the event output format, not the regular format
    53  		{NewBuilder("my-key").Build(),
    54  			`{"kind": "user", "key": "my-key", "_meta": {"redactedAttributes": ["name"]}}`},
    55  
    56  		// redactedAttributes and privateAttributes should not break deserialization
    57  		// These test cases were added due to an issue where having both would break deserialization depending on which came first.
    58  		{NewBuilder("my-key").Private("name").Build(),
    59  			`{"kind": "user", "key": "my-key", "_meta": {"privateAttributes": ["name"], "redactedAttributes": ["name"]}}`},
    60  		{NewBuilder("my-key").Private("name").Build(),
    61  			`{"kind": "user", "key": "my-key", "_meta": {"redactedAttributes": ["name"], "privateAttributes": ["name"]}}`},
    62  	}
    63  }
    64  
    65  func makeContextUnmarshalFromOldUserSchemaParams() []contextSerializationParams {
    66  	ret := []contextSerializationParams{
    67  		{New("key1"), `{"key": "key1"}`},
    68  
    69  		{NewBuilder("").setAllowEmptyKey(true).Build(), `{"key": ""}`}, // allowed only in old-style user JSON
    70  
    71  		{NewBuilder("key2").Name("my-name").Build(),
    72  			`{"key": "key2", "name": "my-name"}`},
    73  		{NewBuilder("key2").Build(),
    74  			`{"key": "key2", "name": null}`},
    75  
    76  		{contextWithSecondary(New("key3"), "value"),
    77  			`{"key": "key3", "secondary": "value"}`},
    78  		{New("key3"),
    79  			`{"key": "key3", "secondary": null}`},
    80  
    81  		{NewBuilder("key4").Anonymous(true).Build(),
    82  			`{"key": "key4", "anonymous": true}`},
    83  		{NewBuilder("key4").Build(),
    84  			`{"key": "key4", "anonymous": false}`},
    85  		{NewBuilder("key4").Build(),
    86  			`{"key": "key4", "anonymous": null}`},
    87  
    88  		{NewBuilder("key6").Build(),
    89  			`{"key": "key6", "custom": {}}`},
    90  		{NewBuilder("key6").Build(),
    91  			`{"key": "key6", "custom": null}`},
    92  		{NewBuilder("key6").Build(),
    93  			`{"key": "key6", "custom": {"attr1": null}}`},
    94  		{NewBuilder("key6").SetBool("attr1", true).Build(),
    95  			`{"key": "key6", "custom": {"attr1": true}}`},
    96  		{NewBuilder("key6").SetBool("attr1", false).Build(),
    97  			`{"key": "key6", "custom": {"attr1": false}}`},
    98  		{NewBuilder("key6").SetInt("attr1", 123).Build(),
    99  			`{"key": "key6", "custom": {"attr1": 123}}`},
   100  		{NewBuilder("key6").SetFloat64("attr1", 1.5).Build(),
   101  			`{"key": "key6", "custom": {"attr1": 1.5}}`},
   102  		{NewBuilder("key6").SetString("attr1", "xyz").Build(),
   103  			`{"key": "key6", "custom": {"attr1": "xyz"}}`},
   104  		{NewBuilder("key6").SetValue("attr1", ldvalue.ArrayOf(ldvalue.Int(10), ldvalue.Int(20))).Build(),
   105  			`{"key": "key6", "custom": {"attr1": [10, 20]}}`},
   106  		{NewBuilder("key6").SetValue("attr1", ldvalue.ObjectBuild().Set("a", ldvalue.Int(1)).Build()).Build(),
   107  			`{"key": "key6", "custom": {"attr1": {"a": 1}}}`},
   108  
   109  		{NewBuilder("key6").Name("x").Private("name", "email").Build(),
   110  			`{"key": "key6", "name": "x", "privateAttributeNames":["name", "email"]}`},
   111  		{NewBuilder("key6").Name("x").Private("name", "email").Build(),
   112  			`{"key": "key6", "name": "x", "privateAttributeNames":["name", "email"]}`},
   113  		{NewBuilder("key6").Name("x").Build(),
   114  			`{"key": "key6", "name": "x", "privateAttributeNames":[]}`},
   115  		{NewBuilder("key6").Name("x").Build(),
   116  			`{"key": "key6", "name": "x", "privateAttributeNames":null}`},
   117  
   118  		{NewBuilder("key7").Name("x").Build(),
   119  			`{"key": "key7", "unknownTopLevelPropIsIgnored": {"a": 1}, "name": "x"}`},
   120  
   121  		{NewBuilder("key8").Name("x").Build(),
   122  			`{"key": "key8", "name": "x", "privateAttrs": ["name"]}`},
   123  		// privateAttrs is only a thing in the event output format
   124  
   125  		{NewBuilder("key9").Name("x").SetInt("a", 1).Anonymous(true).Build(),
   126  			`{"key": "key9", "name": "x", "anonymous": true, "custom": ` +
   127  				`{"kind": ".", "key": ".", "name": ".", "anonymous": false, "_meta": true, "a": 1}}`},
   128  		// don't let custom attributes overwrite top-level properties with the same reserved names
   129  	}
   130  	for _, attr := range []string{"firstName", "lastName", "email", "country", "avatar", "ip"} {
   131  		ret = append(ret,
   132  			contextSerializationParams{
   133  				NewBuilder("user-key").SetString(attr, "x").Build(),
   134  				fmt.Sprintf(`{"key": "user-key", %q: "x"}`, attr),
   135  			},
   136  			contextSerializationParams{
   137  				NewBuilder("user-key").Build(),
   138  				fmt.Sprintf(`{"key": "user-key", %q: null}`, attr),
   139  			},
   140  		)
   141  	}
   142  	for _, customValue := range []ldvalue.Value{
   143  		ldvalue.Bool(true),
   144  		ldvalue.Bool(false),
   145  		ldvalue.Int(123),
   146  		ldvalue.Float64(1.5),
   147  		ldvalue.String("xyz"),
   148  		ldvalue.ArrayOf(ldvalue.Int(10), ldvalue.Int(20)),
   149  		ldvalue.ObjectBuild().Set("a", ldvalue.Int(1)).Build(),
   150  	} {
   151  		ret = append(ret,
   152  			contextSerializationParams{
   153  				NewBuilder("user-key").SetValue("my-attr", customValue).Build(),
   154  				fmt.Sprintf(`{"key": "user-key", "custom": {"my-attr": %s}}`, customValue.JSONString()),
   155  			},
   156  		)
   157  	}
   158  	return ret
   159  }
   160  
   161  func makeAllContextUnmarshalingParams() []contextSerializationParams {
   162  	params := makeContextMarshalingAndUnmarshalingParams()
   163  	params = append(params, makeContextUnmarshalUnimportantVariantsParams()...)
   164  	params = append(params, makeContextUnmarshalFromOldUserSchemaParams()...)
   165  	return params
   166  }
   167  
   168  func makeAllContextUnmarshalingEventOutputFormatParams() []contextSerializationParams {
   169  	var ret []contextSerializationParams
   170  	for _, p := range makeAllContextUnmarshalingParams() {
   171  		transformed := p
   172  		transformed.json = translateRegularContextJSONToEventOutputJSONAndViceVersa(transformed.json)
   173  		ret = append(ret, transformed)
   174  	}
   175  	return ret
   176  }
   177  
   178  func makeContextUnmarshalingErrorInputs() []string {
   179  	return []string{
   180  		`null`,
   181  		`false`,
   182  		`1`,
   183  		`"x"`,
   184  		`[]`,
   185  		`{}`,
   186  
   187  		// wrong type for top-level property
   188  		`{"kind": null}`,
   189  		`{"kind": true}`,
   190  		`{"kind": "org", "key": null}`,
   191  		`{"kind": "org", "key": true}`,
   192  		`{"kind": "multi", "org": null}`,
   193  		`{"kind": "multi", "org": true}`,
   194  		`{"kind": "org", "key": "my-key", "name": true}`,
   195  		`{"kind": "org", "key": "my-key", "anonymous": "yes"}}`,
   196  		`{"kind": "org", "key": "my-key", "anonymous": null}}`,
   197  
   198  		`{"kind": "org"}`,             // missing key
   199  		`{"kind": "user", "key": ""}`, // empty key not allowed in new-style context
   200  		`{"kind": "", "key": "x"}`,    // empty kind not allowed in new-style context
   201  		`{"kind": "ørg", "key": "x"}`, // illegal kind
   202  
   203  		// wrong type within _meta
   204  		`{"kind": "org", "key": "my-key", "_meta": true}}`,
   205  		`{"kind": "org", "key": "my-key", "_meta": {"privateAttributes": true}}}`,
   206  
   207  		`{"kind": "multi"}`,                                                       // multi kind with no kinds
   208  		`{"kind": "multi", "user": {"key": ""}}`,                                  // multi kind where subcontext fails validation
   209  		`{"kind": "multi", "user": {"key": true}}`,                                // multi kind where subcontext is malformed
   210  		`{"kind": "multi", "org": {"key": "x"}, "org": {"key": "y"}}`,             // multi kind with repeated kind
   211  		`{"kind": "multi", "multi": {"user": {"key": "a"}, "org": {"key": "x"}}}`, // multi within multi
   212  
   213  		// wrong types in old user schema
   214  		`{"key": null}`,
   215  		`{"key": true}`,
   216  		`{"key": "my-key", "secondary": true}`,
   217  		`{"key": "my-key", "anonymous": "x"}`,
   218  		`{"key": "my-key", "name": true}`,
   219  		`{"key": "my-key", "firstName": true}`,
   220  		`{"key": "my-key", "lastName": true}`,
   221  		`{"key": "my-key", "email": true}`,
   222  		`{"key": "my-key", "country": true}`,
   223  		`{"key": "my-key", "avatar": true}`,
   224  		`{"key": "my-key", "ip": true}`,
   225  		`{"key": "my-key", "custom": true}`,
   226  		`{"key": "my-key", "privateAttributeNames": true}`,
   227  
   228  		// missing key in old user schema
   229  		`{"name": "x"}`,
   230  	}
   231  }
   232  
   233  func makeContextUnmarshalingEventOutputFormatErrorInputs() []string {
   234  	var ret []string
   235  	for _, s := range makeContextUnmarshalingErrorInputs() {
   236  		ret = append(ret, translateRegularContextJSONToEventOutputJSONAndViceVersa(s))
   237  	}
   238  	return ret
   239  }
   240  
   241  func translateRegularContextJSONToEventOutputJSONAndViceVersa(s string) string {
   242  	// In the regular test inputs, redactedAttributes and privateAttrs are property names we do *not*
   243  	// expect to see because they are for the event output format, so the test inputs that reference
   244  	// them have a "this should be ignored" expectation.
   245  	s = strings.ReplaceAll(s, `"redactedAttributes"`, `<temp1>`)
   246  	s = strings.ReplaceAll(s, `"privateAttrs"`, `<temp2>`) // old schema
   247  
   248  	s = strings.ReplaceAll(s, `"privateAttributes"`, `"redactedAttributes"`)
   249  	s = strings.ReplaceAll(s, `"privateAttributeNames"`, `"privateAttrs"`) // old schema
   250  
   251  	// ...so, in the event output format, we use privateAttributes and privateAttributeNames for the
   252  	// "this should be ignored" cases.
   253  	s = strings.ReplaceAll(s, `<temp1>`, `"privateAttributes"`)
   254  	s = strings.ReplaceAll(s, `<temp2>`, `"privateAttributeNames"`)
   255  
   256  	return s
   257  }
   258  
   259  func contextUnmarshalingTests(
   260  	t *testing.T,
   261  	unmarshalSingleContextFn func(*Context, []byte) error,
   262  	unmarshalArrayFn func(*[]Context, []byte) error,
   263  ) {
   264  	t.Run("valid data", func(t *testing.T) {
   265  		for _, p := range makeAllContextUnmarshalingParams() {
   266  			t.Run(p.json, func(t *testing.T) {
   267  				var c Context
   268  				err := unmarshalSingleContextFn(&c, []byte(p.json))
   269  				assert.NoError(t, err)
   270  				assert.Equal(t, p.context, c)
   271  			})
   272  		}
   273  	})
   274  
   275  	t.Run("error cases", func(t *testing.T) {
   276  		for _, badJSON := range makeContextUnmarshalingErrorInputs() {
   277  			t.Run(badJSON, func(t *testing.T) {
   278  				var c Context
   279  				err := unmarshalSingleContextFn(&c, []byte(badJSON))
   280  				assert.Error(t, err)
   281  			})
   282  		}
   283  	})
   284  
   285  	if unmarshalArrayFn != nil {
   286  		t.Run("within an array", func(t *testing.T) {
   287  			// This test shows that in our streaming implementations, the unmarshaler leaves the input
   288  			// stream in the proper state at the end of the object, so it will correctly parse a
   289  			// subsequent value.
   290  			invariantSecondContext := New("simple")
   291  			invariantSecondContextJSON := `{"kind": "user", "key": "simple"}`
   292  			for _, p := range makeAllContextUnmarshalingParams() {
   293  				t.Run(p.json, func(t *testing.T) {
   294  					input := `[ ` + p.json + `, ` + invariantSecondContextJSON + ` ]`
   295  					var cs []Context
   296  					err := unmarshalArrayFn(&cs, []byte(input))
   297  					assert.NoError(t, err)
   298  					if assert.Len(t, cs, 2) {
   299  						assert.Equal(t, p.context, cs[0])
   300  						assert.Equal(t, invariantSecondContext, cs[1])
   301  					}
   302  				})
   303  			}
   304  		})
   305  	}
   306  
   307  	t.Run("unmarshaling twice", func(t *testing.T) {
   308  		// This test verifies that if you take an uninitialized Context{}, and unmarshal it from JSON, and
   309  		// then unmarshal the same instance again, you get the same result (i.e. fields are overwritten
   310  		// rather than being appended).
   311  		for _, p := range makeAllContextUnmarshalingParams() {
   312  			t.Run(p.json, func(t *testing.T) {
   313  				var c Context
   314  				err := unmarshalSingleContextFn(&c, []byte(p.json))
   315  				// Here we're using an if, rather than an assert, because we already asserted these conditions
   316  				// in the "valid data" test. If something is really wrong such that it's returning an error or
   317  				// the Equal test fails, we want that to show up more specifically in that test, rather than
   318  				// producing confounding results in this one.
   319  				if err == nil && c.Equal(p.context) {
   320  					c1 := c
   321  					require.NoError(t, unmarshalSingleContextFn(&c, []byte(p.json)))
   322  					assert.Equal(t, c1, c)
   323  				}
   324  			})
   325  		}
   326  	})
   327  }
   328  
   329  func jsonUnmarshalTestFn(c *Context, data []byte) error {
   330  	return json.Unmarshal(data, c)
   331  }
   332  
   333  func jsonStreamUnmarshalTestFn(c *Context, data []byte) error {
   334  	r := jreader.NewReader(data)
   335  	ContextSerialization.UnmarshalFromJSONReader(&r, c)
   336  	return r.Error()
   337  }
   338  
   339  func jsonStreamUnmarshalArrayTestFn(cs *[]Context, data []byte) error {
   340  	r := jreader.NewReader(data)
   341  	for arr := r.Array(); arr.Next(); {
   342  		var c Context
   343  		ContextSerialization.UnmarshalFromJSONReader(&r, &c)
   344  		*cs = append(*cs, c)
   345  	}
   346  	return r.Error()
   347  }
   348  
   349  func TestContextJSONUnmarshal(t *testing.T) {
   350  	contextUnmarshalingTests(t, jsonUnmarshalTestFn, nil)
   351  }
   352  
   353  func TestContextReadFromJSONReader(t *testing.T) {
   354  	contextUnmarshalingTests(t, jsonStreamUnmarshalTestFn, jsonStreamUnmarshalArrayTestFn)
   355  }
   356  
   357  func TestContextUnmarshalEventOutputFormat(t *testing.T) {
   358  	t.Run("valid data", func(t *testing.T) {
   359  		for _, p := range makeAllContextUnmarshalingEventOutputFormatParams() {
   360  			t.Run(p.json, func(t *testing.T) {
   361  				r := jreader.NewReader([]byte(p.json))
   362  				var c EventOutputContext
   363  				ContextSerialization.UnmarshalFromJSONReaderEventOutput(&r, &c)
   364  				assert.NoError(t, r.Error())
   365  				assert.Equal(t, p.context, c.Context)
   366  			})
   367  		}
   368  	})
   369  
   370  	t.Run("error cases", func(t *testing.T) {
   371  		for _, badJSON := range makeContextUnmarshalingEventOutputFormatErrorInputs() {
   372  			t.Run(badJSON, func(t *testing.T) {
   373  				r := jreader.NewReader([]byte(badJSON))
   374  				var c EventOutputContext
   375  				ContextSerialization.UnmarshalFromJSONReaderEventOutput(&r, &c)
   376  				assert.Error(t, r.Error())
   377  			})
   378  		}
   379  	})
   380  }
   381  
   382  func TestContextReadKindAndKeyOnly(t *testing.T) {
   383  	t.Run("valid data", func(t *testing.T) {
   384  		for _, p := range makeAllContextUnmarshalingParams() {
   385  			t.Run(p.json, func(t *testing.T) {
   386  				var fullContext, minimalContext Context
   387  
   388  				r1 := jreader.NewReader([]byte(p.json))
   389  				err := ContextSerialization.UnmarshalFromJSONReader(&r1, &fullContext)
   390  				require.NoError(t, err)
   391  				require.NoError(t, r1.Error())
   392  				r2 := jreader.NewReader([]byte(p.json))
   393  				err = ContextSerialization.UnmarshalWithKindAndKeyOnly(&r2, &minimalContext)
   394  				require.NoError(t, err)
   395  				require.NoError(t, r2.Error())
   396  
   397  				allFull, allMini := fullContext.GetAllIndividualContexts(nil), minimalContext.GetAllIndividualContexts(nil)
   398  				if assert.Len(t, allMini, len(allFull)) {
   399  					for i, fc := range allFull {
   400  						mc := allMini[i]
   401  						assert.Equal(t, fc.Kind(), mc.Kind())
   402  						assert.Equal(t, fc.Key(), mc.Key())
   403  					}
   404  				}
   405  				assert.Equal(t, fullContext.FullyQualifiedKey(), minimalContext.FullyQualifiedKey())
   406  			})
   407  		}
   408  	})
   409  
   410  	t.Run("error cases", func(t *testing.T) {
   411  		for _, badJSON := range []string{
   412  			// This is a deliberately shorter list than the error cases in the regular unmarshaling tests,
   413  			// because this method does not validate every property.
   414  			`null`,
   415  			`false`,
   416  			`1`,
   417  			`"x"`,
   418  			`[]`,
   419  			`{}`,
   420  
   421  			// wrong type for kind/key, or individual context was not an object
   422  			`{"kind": null}`,
   423  			`{"kind": true}`,
   424  			`{"kind": "org", "key": null}`,
   425  			`{"kind": "org", "key": true}`,
   426  			`{"kind": "multi", "org": null}`,
   427  			`{"kind": "multi", "org": true}`,
   428  
   429  			`{"kind": "org"}`,             // missing key
   430  			`{"kind": "user", "key": ""}`, // empty key not allowed in new-style context
   431  			`{"kind": "", "key": "x"}`,    // empty kind not allowed in new-style context
   432  			`{"kind": "ørg", "key": "x"}`, // illegal kind
   433  
   434  			`{"kind": "multi"}`,                                           // multi kind with no kinds
   435  			`{"kind": "multi", "user": {"key": ""}}`,                      // multi kind where subcontext fails validation
   436  			`{"kind": "multi", "user": {"key": true}}`,                    // multi kind where subcontext is malformed
   437  			`{"kind": "multi", "org": {"key": "x"}, "org": {"key": "y"}}`, // multi kind with repeated kind
   438  
   439  			// wrong types in old user schema
   440  			`{"key": null}`,
   441  			`{"key": true}`,
   442  
   443  			// missing key in old user schema
   444  			`{"name": "x"}`,
   445  		} {
   446  			t.Run(badJSON, func(t *testing.T) {
   447  				var c Context
   448  				r := jreader.NewReader([]byte(badJSON))
   449  				err := ContextSerialization.UnmarshalWithKindAndKeyOnly(&r, &c)
   450  				assert.Error(t, err)
   451  			})
   452  		}
   453  	})
   454  }
   455  
   456  func contextWithSecondary(c Context, s string) Context {
   457  	c.secondary = ldvalue.NewOptionalString(s)
   458  	return c
   459  }
   460  

View as plain text