...

Source file src/github.com/launchdarkly/go-sdk-events/v2/context_formatter_test.go

Documentation: github.com/launchdarkly/go-sdk-events/v2

     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") // note "true" indicating there are nested properties to filter
   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  			// It's deliberate that there is an unredacted "name" property here even though we also put
   338  			// "name" in the pre-redacted list; that's to prove that we are *not* applying any redaction
   339  			// logic at all when there is a pre-redacted list.
   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() // don't need to sort the private attrs here because they are copied straight from the input
   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