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
19 return []contextSerializationParams{
20
21 {NewBuilder("my-key").Build(),
22 `{"kind": "user", "key": "my-key", "name": null}`},
23
24
25 {NewBuilder("my-key").Build(),
26 `{"kind": "user", "key": "my-key", "customAttr": null}`},
27
28
29 {NewBuilder("my-key").Build(),
30 `{"kind": "user", "key": "my-key", "anonymous": false}`},
31
32
33 {NewBuilder("my-key").Build(),
34 `{"kind": "user", "key": "my-key", "_meta": {}}`},
35
36
37 {NewBuilder("my-key").Build(),
38 `{"kind": "user", "key": "my-key", "_meta": null}`},
39
40
41 {NewBuilder("my-key").Build(),
42 `{"kind": "user", "key": "my-key", "_meta": {"privateAttributes": null}}`},
43
44
45 {NewBuilder("my-key").Build(),
46 `{"kind": "user", "key": "my-key", "_meta": {"privateAttributes": null}}`},
47
48
49 {NewBuilder("my-key").Build(),
50 `{"kind": "user", "key": "my-key", "_meta": {"unknownProp": false}}`},
51
52
53 {NewBuilder("my-key").Build(),
54 `{"kind": "user", "key": "my-key", "_meta": {"redactedAttributes": ["name"]}}`},
55
56
57
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": ""}`},
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
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
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
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"}`,
199 `{"kind": "user", "key": ""}`,
200 `{"kind": "", "key": "x"}`,
201 `{"kind": "ørg", "key": "x"}`,
202
203
204 `{"kind": "org", "key": "my-key", "_meta": true}}`,
205 `{"kind": "org", "key": "my-key", "_meta": {"privateAttributes": true}}}`,
206
207 `{"kind": "multi"}`,
208 `{"kind": "multi", "user": {"key": ""}}`,
209 `{"kind": "multi", "user": {"key": true}}`,
210 `{"kind": "multi", "org": {"key": "x"}, "org": {"key": "y"}}`,
211 `{"kind": "multi", "multi": {"user": {"key": "a"}, "org": {"key": "x"}}}`,
212
213
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
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
243
244
245 s = strings.ReplaceAll(s, `"redactedAttributes"`, `<temp1>`)
246 s = strings.ReplaceAll(s, `"privateAttrs"`, `<temp2>`)
247
248 s = strings.ReplaceAll(s, `"privateAttributes"`, `"redactedAttributes"`)
249 s = strings.ReplaceAll(s, `"privateAttributeNames"`, `"privateAttrs"`)
250
251
252
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
288
289
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
309
310
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
316
317
318
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
413
414 `null`,
415 `false`,
416 `1`,
417 `"x"`,
418 `[]`,
419 `{}`,
420
421
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"}`,
430 `{"kind": "user", "key": ""}`,
431 `{"kind": "", "key": "x"}`,
432 `{"kind": "ørg", "key": "x"}`,
433
434 `{"kind": "multi"}`,
435 `{"kind": "multi", "user": {"key": ""}}`,
436 `{"kind": "multi", "user": {"key": true}}`,
437 `{"kind": "multi", "org": {"key": "x"}, "org": {"key": "y"}}`,
438
439
440 `{"key": null}`,
441 `{"key": true}`,
442
443
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