1 package ldevents
2
3 import (
4 "encoding/json"
5 "sort"
6 "testing"
7
8 "github.com/launchdarkly/go-sdk-common/v3/ldattr"
9 "github.com/launchdarkly/go-sdk-common/v3/ldcontext"
10 "github.com/launchdarkly/go-sdk-common/v3/ldvalue"
11
12 "github.com/launchdarkly/go-jsonstream/v3/jwriter"
13 "github.com/launchdarkly/go-test-helpers/v3/jsonhelpers"
14
15 "github.com/stretchr/testify/assert"
16 "github.com/stretchr/testify/require"
17 )
18
19 func TestEventContextFormatterConstructor(t *testing.T) {
20 t.Run("empty", func(t *testing.T) {
21 f := newEventContextFormatter(EventsConfiguration{})
22 require.NotNil(t, f)
23
24 assert.False(t, f.allAttributesPrivate)
25 assert.Nil(t, f.privateAttributes)
26 })
27
28 t.Run("all private", func(t *testing.T) {
29 f := newEventContextFormatter(EventsConfiguration{
30 AllAttributesPrivate: true,
31 })
32 require.NotNil(t, f)
33
34 assert.True(t, f.allAttributesPrivate)
35 assert.Nil(t, f.privateAttributes)
36 })
37
38 t.Run("top-level private", func(t *testing.T) {
39 private1, private2 := ldattr.NewRef("name"), ldattr.NewRef("email")
40 f := newEventContextFormatter(EventsConfiguration{
41 PrivateAttributes: []ldattr.Ref{private1, private2},
42 })
43 require.NotNil(t, f)
44
45 assert.False(t, f.allAttributesPrivate)
46 require.NotNil(t, f.privateAttributes)
47 assert.Equal(t,
48 map[string]*privateAttrLookupNode{
49 "name": {attribute: &private1},
50 "email": {attribute: &private2},
51 },
52 f.privateAttributes)
53 })
54
55 t.Run("nested private", func(t *testing.T) {
56 private1, private2, private3 := ldattr.NewRef("/name"),
57 ldattr.NewRef("/address/street"), ldattr.NewRef("/address/city")
58 f := newEventContextFormatter(EventsConfiguration{
59 PrivateAttributes: []ldattr.Ref{private1, private2, private3},
60 })
61 require.NotNil(t, f)
62
63 assert.False(t, f.allAttributesPrivate)
64 require.NotNil(t, f.privateAttributes)
65 assert.Equal(t,
66 map[string]*privateAttrLookupNode{
67 "name": {attribute: &private1},
68 "address": {
69 children: map[string]*privateAttrLookupNode{
70 "street": {attribute: &private2},
71 "city": {attribute: &private3},
72 },
73 },
74 },
75 f.privateAttributes)
76 })
77 }
78
79 func TestCheckGlobalPrivateAttrRefs(t *testing.T) {
80 expectResult := func(t *testing.T, f eventContextFormatter, expectRedactedAttr *ldattr.Ref, expectHasNested bool, path ...string) {
81 redactedAttr, hasNested := f.checkGlobalPrivateAttrRefs(path)
82 assert.Equal(t, expectRedactedAttr, redactedAttr)
83 assert.Equal(t, expectHasNested, hasNested)
84 }
85
86 t.Run("empty", func(t *testing.T) {
87 f := newEventContextFormatter(EventsConfiguration{})
88 require.NotNil(t, f)
89
90 expectResult(t, f, nil, false, "name")
91 expectResult(t, f, nil, false, "address", "street")
92 })
93
94 t.Run("top-level private", func(t *testing.T) {
95 attrRef1, attrRef2 := ldattr.NewRef("name"), ldattr.NewRef("email")
96 f := newEventContextFormatter(EventsConfiguration{
97 PrivateAttributes: []ldattr.Ref{attrRef1, attrRef2},
98 })
99 require.NotNil(t, f)
100
101 expectResult(t, f, &attrRef1, false, "name")
102 expectResult(t, f, &attrRef2, false, "email")
103 expectResult(t, f, nil, false, "address")
104 expectResult(t, f, nil, false, "address", "street")
105 })
106
107 t.Run("nested private", func(t *testing.T) {
108 attrRef1, attrRef2, attrRef3 := ldattr.NewRef("name"),
109 ldattr.NewRef("/address/street"), ldattr.NewRef("/address/city")
110 f := newEventContextFormatter(EventsConfiguration{
111 PrivateAttributes: []ldattr.Ref{attrRef1, attrRef2, attrRef3},
112 })
113 require.NotNil(t, f)
114
115 expectResult(t, f, &attrRef1, false, "name")
116 expectResult(t, f, nil, true, "address")
117 expectResult(t, f, &attrRef2, false, "address", "street")
118 expectResult(t, f, &attrRef3, false, "address", "city")
119 expectResult(t, f, nil, false, "address", "zip")
120 })
121 }
122
123 func TestEventContextFormatterOutput(t *testing.T) {
124 objectValue := ldvalue.ObjectBuild().SetString("city", "SF").SetString("state", "CA").Build()
125
126 type params struct {
127 desc string
128 context ldcontext.Context
129 options EventsConfiguration
130 expectedJSON string
131 }
132 for _, p := range []params{
133 {
134 "no attributes private, single kind",
135 ldcontext.NewBuilder("my-key").Kind("org").
136 Name("my-name").
137 SetString("attr1", "value1").
138 SetValue("address", objectValue).
139 Build(),
140 EventsConfiguration{},
141 `{"kind": "org", "key": "my-key",
142 "name": "my-name", "attr1": "value1", "address": {"city": "SF", "state": "CA"}}`,
143 },
144 {
145 "no attributes private, multi-kind",
146 ldcontext.NewMulti(
147 ldcontext.NewBuilder("org-key").Kind("org").
148 Name("org-name").
149 Build(),
150 ldcontext.NewBuilder("user-key").
151 Name("user-name").
152 SetValue("address", objectValue).
153 Build(),
154 ),
155 EventsConfiguration{},
156 `{"kind": "multi",
157 "org": {"key": "org-key", "name": "org-name"},
158 "user": {"key": "user-key", "name": "user-name", "address": {"city": "SF", "state": "CA"}}}`,
159 },
160 {
161 "anonymous",
162 ldcontext.NewBuilder("my-key").Kind("org").
163 Anonymous(true).
164 Build(),
165 EventsConfiguration{},
166 `{"kind": "org", "key": "my-key", "anonymous": true}`,
167 },
168 {
169 "all attributes private globally, single kind",
170 ldcontext.NewBuilder("my-key").Kind("org").
171 Name("my-name").
172 SetString("attr1", "value1").
173 SetValue("address", objectValue).
174 Build(),
175 EventsConfiguration{AllAttributesPrivate: true},
176 `{"kind": "org", "key": "my-key",
177 "_meta": {"redactedAttributes": ["address", "attr1", "name"]}}`,
178 },
179 {
180 "all attributes private globally, multi-kind",
181 ldcontext.NewMulti(
182 ldcontext.NewBuilder("org-key").Kind("org").
183 Name("org-name").
184 Build(),
185 ldcontext.NewBuilder("user-key").
186 Name("user-name").
187 SetValue("address", objectValue).
188 Build(),
189 ),
190 EventsConfiguration{AllAttributesPrivate: true},
191 `{"kind": "multi",
192 "org": {"key": "org-key", "_meta": {"redactedAttributes": ["name"]}},
193 "user": {"key": "user-key", "_meta": {"redactedAttributes": ["address", "name"]}}}`,
194 },
195 {
196 "top-level attributes private globally, single kind",
197 ldcontext.NewBuilder("my-key").Kind("org").
198 Name("my-name").
199 SetString("attr1", "value1").
200 SetValue("address", objectValue).
201 Build(),
202 EventsConfiguration{PrivateAttributes: []ldattr.Ref{
203 ldattr.NewRef("/name"), ldattr.NewRef("/address")}},
204 `{"kind": "org", "key": "my-key", "attr1": "value1",
205 "_meta": {"redactedAttributes": ["/address", "/name"]}}`,
206 },
207 {
208 "top-level attributes private globally, multi-kind",
209 ldcontext.NewMulti(
210 ldcontext.NewBuilder("org-key").Kind("org").
211 Name("org-name").
212 SetString("attr1", "value1").
213 SetString("attr2", "value2").
214 Build(),
215 ldcontext.NewBuilder("user-key").
216 Name("user-name").
217 SetString("attr1", "value1").
218 SetString("attr3", "value3").
219 Build(),
220 ),
221 EventsConfiguration{PrivateAttributes: []ldattr.Ref{
222 ldattr.NewRef("/name"), ldattr.NewRef("/attr1"), ldattr.NewRef("/attr3")}},
223 `{"kind": "multi",
224 "org": {"key": "org-key", "attr2": "value2", "_meta": {"redactedAttributes": ["/attr1", "/name"]}},
225 "user": {"key": "user-key", "_meta": {"redactedAttributes": ["/attr1", "/attr3", "/name"]}}}`,
226 },
227 {
228 "top-level attributes private per context, single kind",
229 ldcontext.NewBuilder("my-key").Kind("org").
230 Name("my-name").
231 SetString("attr1", "value1").
232 SetValue("address", objectValue).
233 Private("name", "address").
234 Build(),
235 EventsConfiguration{},
236 `{"kind": "org", "key": "my-key", "attr1": "value1",
237 "_meta": {"redactedAttributes": ["address", "name"]}}`,
238 },
239 {
240 "top-level attributes private per context, multi-kind",
241 ldcontext.NewMulti(
242 ldcontext.NewBuilder("org-key").Kind("org").
243 SetString("attr1", "value1").
244 SetString("attr2", "value2").
245 Private("attr1").
246 Build(),
247 ldcontext.NewBuilder("user-key").
248 SetString("attr1", "value1").
249 SetString("attr3", "value3").
250 Private("attr3").
251 Build(),
252 ),
253 EventsConfiguration{},
254 `{"kind": "multi",
255 "org": {"key": "org-key", "attr2": "value2", "_meta": {"redactedAttributes": ["attr1"]}},
256 "user": {"key": "user-key", "attr1": "value1", "_meta": {"redactedAttributes": ["attr3"]}}}`,
257 },
258 {
259 "nested attribute private globally",
260 ldcontext.NewBuilder("my-key").Kind("org").
261 Name("my-name").
262 SetValue("address", objectValue).
263 Build(),
264 EventsConfiguration{PrivateAttributes: []ldattr.Ref{ldattr.NewRef("/address/city")}},
265 `{"kind": "org", "key": "my-key",
266 "name": "my-name", "address": {"state": "CA"},
267 "_meta": {"redactedAttributes": ["/address/city"]}}`,
268 },
269 {
270 "nested attribute private per context",
271 ldcontext.NewBuilder("my-key").Kind("org").
272 Name("my-name").
273 SetValue("address", objectValue).
274 PrivateRef(ldattr.NewRef("/address/city"), ldattr.NewRef("/name")).
275 Build(),
276 EventsConfiguration{},
277 `{"kind": "org", "key": "my-key", "address": {"state": "CA"},
278 "_meta": {"redactedAttributes": ["/address/city", "/name"]}}`,
279 },
280 {
281 "nested attribute private per context, superseded by top-level reference",
282 ldcontext.NewBuilder("my-key").Kind("org").
283 Name("my-name").
284 SetValue("address", objectValue).
285 PrivateRef(ldattr.NewRef("/address/city"), ldattr.NewRef("/address")).
286 Build(),
287 EventsConfiguration{},
288 `{"kind": "org", "key": "my-key",
289 "name": "my-name", "_meta": {"redactedAttributes": ["/address"]}}`,
290 },
291 {
292 "attribute name is escaped if necessary in redactedAttributes",
293 ldcontext.NewBuilder("my-key").Kind("org").
294 SetString("/a/b~c", "value").
295 Build(),
296 EventsConfiguration{AllAttributesPrivate: true},
297 `{"kind": "org", "key": "my-key",
298 "_meta": {"redactedAttributes": ["/~1a~1b~0c"]}}`,
299 },
300 } {
301 t.Run(p.desc, func(t *testing.T) {
302 f := newEventContextFormatter(p.options)
303 w := jwriter.NewWriter()
304 ec := Context(p.context)
305 f.WriteContext(&w, &ec)
306 require.NoError(t, w.Error())
307 actualJSON := sortPrivateAttributesInOutputJSON(w.Bytes())
308 jsonhelpers.AssertEqual(t, p.expectedJSON, actualJSON)
309 })
310 }
311 }
312
313 func TestPreserializedEventContextFormatterOutput(t *testing.T) {
314 userContextPlaceholder := ldcontext.New("user-key")
315 userContextJSON := `{"kind": "user", "key": "user-key", "name": "my-name",
316 "_meta": {"redactedAttributes": ["/address/city", "name"]}}`
317 orgContextPlaceholder := ldcontext.NewWithKind("org", "org-key")
318 multiContext := ldcontext.NewMulti(userContextPlaceholder, orgContextPlaceholder)
319 multiContextJSON := `{"kind": "multi",
320 "org": {"key": "org-key", "name": "org-name",
321 "_meta": {"redactedAttributes": ["email"]}},
322 "user": {"key": "user-key", "name": "my-name",
323 "_meta": {"redactedAttributes": ["/address/city", "name"]}}}`
324
325 type params struct {
326 desc string
327 eventContext EventInputContext
328 options EventsConfiguration
329 expectedJSON string
330 }
331 for _, p := range []params{
332 {
333 "single kind",
334 PreserializedContext(userContextPlaceholder, json.RawMessage(userContextJSON)),
335 EventsConfiguration{},
336 userContextJSON,
337
338
339
340 },
341 {
342 "multi-kind",
343 PreserializedContext(multiContext, json.RawMessage(multiContextJSON)),
344 EventsConfiguration{},
345 multiContextJSON,
346 },
347 {
348 "config options for private attributes are ignored",
349 PreserializedContext(userContextPlaceholder, json.RawMessage(userContextJSON)),
350 EventsConfiguration{AllAttributesPrivate: true},
351 userContextJSON,
352 },
353 } {
354 t.Run(p.desc, func(t *testing.T) {
355 f := newEventContextFormatter(p.options)
356 w := jwriter.NewWriter()
357 f.WriteContext(&w, &p.eventContext)
358 require.NoError(t, w.Error())
359 actualJSON := w.Bytes()
360 jsonhelpers.AssertEqual(t, p.expectedJSON, actualJSON)
361 })
362 }
363 }
364
365 func TestWriteInvalidContext(t *testing.T) {
366 badContext := ldcontext.New("")
367 f := newEventContextFormatter(EventsConfiguration{})
368 w := jwriter.NewWriter()
369 ec := Context(badContext)
370 f.WriteContext(&w, &ec)
371 assert.Equal(t, badContext.Err(), w.Error())
372 }
373
374 func sortPrivateAttributesInOutputJSON(data []byte) []byte {
375 parsed := ldvalue.Parse(data)
376 if parsed.Type() != ldvalue.ObjectType {
377 return data
378 }
379 var ret ldvalue.Value
380 if parsed.GetByKey("kind").StringValue() != "multi" {
381 ret = sortPrivateAttributesInSingleKind(parsed)
382 } else {
383 out := ldvalue.ObjectBuildWithCapacity(parsed.Count())
384 for k, v := range parsed.AsValueMap().AsMap() {
385 if k == "kind" {
386 out.Set(k, v)
387 } else {
388 out.Set(k, sortPrivateAttributesInSingleKind(v))
389 }
390 }
391 ret = out.Build()
392 }
393 return []byte(ret.JSONString())
394 }
395
396 func sortPrivateAttributesInSingleKind(parsed ldvalue.Value) ldvalue.Value {
397 out := ldvalue.ObjectBuildWithCapacity(parsed.Count())
398 for k, v := range parsed.AsValueMap().AsMap() {
399 if k != "_meta" {
400 out.Set(k, v)
401 continue
402 }
403 outMeta := ldvalue.ObjectBuildWithCapacity(v.Count())
404 for k1, v1 := range v.AsValueMap().AsMap() {
405 if k1 != "redactedAttributes" {
406 outMeta.Set(k1, v1)
407 continue
408 }
409 values := v1.AsValueArray().AsSlice()
410 sort.Slice(values, func(i, j int) bool {
411 return values[i].StringValue() < values[j].StringValue()
412 })
413 outMeta.Set(k1, ldvalue.ArrayOf(values...))
414 }
415 out.Set(k, outMeta.Build())
416 }
417 return out.Build()
418 }
419
View as plain text