package ldevents import ( "encoding/json" "testing" "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/launchdarkly/go-sdk-common/v3/ldreason" "github.com/launchdarkly/go-sdk-common/v3/lduser" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" m "github.com/launchdarkly/go-test-helpers/v3/matchers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( withoutReasons = NewEventFactory(false, fakeTimeFn) withReasons = NewEventFactory(true, fakeTimeFn) ) func withTestContextsAndConfigs(t *testing.T, action func(*testing.T, EventInputContext, EventsConfiguration)) { singleCtx := Context(ldcontext.New("user-key")) multiCtx := Context(ldcontext.NewMulti(ldcontext.New("user-key"), ldcontext.NewWithKind("org", "org-key"))) privateConfig := basicConfigWithoutPrivateAttrs() privateConfig.AllAttributesPrivate = true t.Run("single kind, no private attributes", func(t *testing.T) { action(t, singleCtx, basicConfigWithoutPrivateAttrs()) }) t.Run("multi-kind, no private attributes", func(t *testing.T) { action(t, multiCtx, basicConfigWithoutPrivateAttrs()) }) t.Run("single kind, with private attributes", func(t *testing.T) { action(t, singleCtx, privateConfig) }) t.Run("multi-kind, with private attributes", func(t *testing.T) { action(t, multiCtx, privateConfig) }) } func TestEventOutputFullEvents(t *testing.T) { withTestContextsAndConfigs(t, func(t *testing.T, context EventInputContext, config EventsConfiguration) { flag := FlagEventProperties{Key: "flagkey", Version: 100} formatter := eventOutputFormatter{ contextFormatter: newEventContextFormatter(config), config: config, } // In this test, we are assuming that the output of eventContextFormatter is correct with regard to // private attributes; those details are covered in the tests for eventContextFormatter itself. We // just want to verify here that eventOutputFormatter is actually *using* eventContextFormatter with // the specified configuration. contextJSON := contextJSON(context, config) contextKeys := expectedContextKeys(context.context) t.Run("feature", func(t *testing.T) { event1 := withoutReasons.NewEvaluationData(flag, context, ldreason.NewEvaluationDetail(ldvalue.String("v"), 1, noReason), false, ldvalue.String("dv"), "") verifyEventOutput(t, formatter, event1, m.JSONEqual(map[string]interface{}{ "kind": "feature", "creationDate": fakeTime, "key": flag.Key, "version": flag.Version, "contextKeys": contextKeys, "variation": 1, "value": "v", "default": "dv", })) event1r := withReasons.NewEvaluationData(flag, context, ldreason.NewEvaluationDetail(ldvalue.String("v"), 1, ldreason.NewEvalReasonFallthrough()), false, ldvalue.String("dv"), "") verifyEventOutput(t, formatter, event1r, m.JSONEqual(map[string]interface{}{ "kind": "feature", "creationDate": fakeTime, "key": flag.Key, "version": flag.Version, "contextKeys": contextKeys, "variation": 1, "value": "v", "default": "dv", "reason": json.RawMessage(`{"kind":"FALLTHROUGH"}`), })) event2 := withoutReasons.NewEvaluationData(flag, context, ldreason.EvaluationDetail{Value: ldvalue.String("v")}, false, ldvalue.String("dv"), "") event2.Variation = ldvalue.OptionalInt{} verifyEventOutput(t, formatter, event2, m.JSONEqual(map[string]interface{}{ "kind": "feature", "creationDate": fakeTime, "key": flag.Key, "version": flag.Version, "contextKeys": contextKeys, "value": "v", "default": "dv", })) event3 := withoutReasons.NewEvaluationData(flag, context, ldreason.NewEvaluationDetail(ldvalue.String("v"), 1, noReason), false, ldvalue.String("dv"), "pre") verifyEventOutput(t, formatter, event3, m.JSONEqual(map[string]interface{}{ "kind": "feature", "creationDate": fakeTime, "key": flag.Key, "version": flag.Version, "contextKeys": contextKeys, "variation": 1, "value": "v", "default": "dv", "prereqOf": "pre", })) event4 := withoutReasons.NewUnknownFlagEvaluationData("flagkey", context, ldvalue.String("dv"), ldreason.EvaluationReason{}) verifyEventOutput(t, formatter, event4, m.JSONEqual(map[string]interface{}{ "kind": "feature", "creationDate": fakeTime, "key": flag.Key, "contextKeys": contextKeys, "value": "dv", "default": "dv", })) }) t.Run("debug", func(t *testing.T) { event1 := withoutReasons.NewEvaluationData(flag, context, ldreason.NewEvaluationDetail(ldvalue.String("v"), 1, noReason), false, ldvalue.String("dv"), "") event1.debug = true verifyEventOutput(t, formatter, event1, m.JSONEqual(map[string]interface{}{ "kind": "debug", "creationDate": fakeTime, "key": flag.Key, "version": flag.Version, "context": contextJSON, "variation": 1, "value": "v", "default": "dv", })) }) t.Run("identify", func(t *testing.T) { event := withoutReasons.NewIdentifyEventData(context) verifyEventOutput(t, formatter, event, m.JSONEqual(map[string]interface{}{ "kind": "identify", "creationDate": fakeTime, "context": contextJSON, })) }) t.Run("custom", func(t *testing.T) { event1 := withoutReasons.NewCustomEventData("eventkey", context, ldvalue.Null(), false, 0) verifyEventOutput(t, formatter, event1, m.JSONEqual(map[string]interface{}{ "kind": "custom", "key": "eventkey", "creationDate": fakeTime, "contextKeys": contextKeys, })) event2 := withoutReasons.NewCustomEventData("eventkey", context, ldvalue.String("d"), false, 0) verifyEventOutput(t, formatter, event2, m.JSONEqual(map[string]interface{}{ "kind": "custom", "key": "eventkey", "creationDate": fakeTime, "contextKeys": contextKeys, "data": "d", })) event3 := withoutReasons.NewCustomEventData("eventkey", context, ldvalue.Null(), true, 2.5) verifyEventOutput(t, formatter, event3, m.JSONEqual(map[string]interface{}{ "kind": "custom", "key": "eventkey", "creationDate": fakeTime, "contextKeys": contextKeys, "metricValue": 2.5, })) }) t.Run("index", func(t *testing.T) { event := indexEvent{BaseEvent: BaseEvent{CreationDate: fakeTime, Context: context}} verifyEventOutput(t, formatter, event, m.JSONEqual(map[string]interface{}{ "kind": "index", "creationDate": fakeTime, "context": contextJSON, })) }) t.Run("raw", func(t *testing.T) { rawData := json.RawMessage(`{"kind":"alias","arbitrary":["we","don't","care","what's","in","here"]}`) event := rawEvent{data: rawData} verifyEventOutput(t, formatter, event, m.JSONEqual(rawData)) }) }) } func TestEventOutputSummaryEvents(t *testing.T) { user := Context(lduser.NewUser("u")) flag1v1 := FlagEventProperties{Key: "flag1", Version: 100} flag1v2 := FlagEventProperties{Key: "flag1", Version: 200} flag1Default := ldvalue.String("default1") flag2 := FlagEventProperties{Key: "flag2", Version: 1} flag2Default := ldvalue.String("default2") formatter := eventOutputFormatter{ contextFormatter: newEventContextFormatter(basicConfigWithoutPrivateAttrs()), config: basicConfigWithoutPrivateAttrs(), } t.Run("summary - single flag, single counter", func(t *testing.T) { es1 := newEventSummarizer() event1 := withoutReasons.NewEvaluationData(flag1v1, user, ldreason.NewEvaluationDetail(ldvalue.String("v"), 1, noReason), false, ldvalue.String("dv"), "") es1.summarizeEvent(event1) verifySummaryEventOutput(t, formatter, es1.snapshot(), m.JSONEqual(map[string]interface{}{ "kind": "summary", "startDate": fakeTime, "endDate": fakeTime, "features": map[string]interface{}{ "flag1": map[string]interface{}{ "counters": json.RawMessage(`[{"count":1,"value":"v","variation":1,"version":100}]`), "contextKinds": []string{"user"}, "default": "dv", }, }, })) es2 := newEventSummarizer() event2 := withoutReasons.NewEvaluationData(flag1v1, user, ldreason.EvaluationDetail{Value: ldvalue.String("dv")}, false, ldvalue.String("dv"), "") event2.Variation = ldvalue.OptionalInt{} es2.summarizeEvent(event2) verifySummaryEventOutput(t, formatter, es2.snapshot(), m.JSONEqual(map[string]interface{}{ "kind": "summary", "startDate": fakeTime, "endDate": fakeTime, "features": map[string]interface{}{ "flag1": map[string]interface{}{ "counters": json.RawMessage(`[{"count":1,"value":"dv","version":100}]`), "contextKinds": []string{"user"}, "default": "dv", }, }, })) es3 := newEventSummarizer() event3 := withoutReasons.NewUnknownFlagEvaluationData("flagkey", user, ldvalue.String("dv"), ldreason.EvaluationReason{}) es3.summarizeEvent(event3) verifySummaryEventOutput(t, formatter, es3.snapshot(), m.JSONEqual(map[string]interface{}{ "kind": "summary", "startDate": fakeTime, "endDate": fakeTime, "features": map[string]interface{}{ "flagkey": map[string]interface{}{ "counters": json.RawMessage(`[{"count":1,"value":"dv","unknown":true}]`), "contextKinds": []string{"user"}, "default": "dv", }, }, })) }) t.Run("summary - multiple counters", func(t *testing.T) { es := newEventSummarizer() es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v1, user, ldreason.NewEvaluationDetail(ldvalue.String("a"), 1, noReason), false, flag1Default, "")) es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v1, user, ldreason.NewEvaluationDetail(ldvalue.String("b"), 2, noReason), false, flag1Default, "")) es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v1, user, ldreason.NewEvaluationDetail(ldvalue.String("a"), 1, noReason), false, flag1Default, "")) es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v2, user, ldreason.NewEvaluationDetail(ldvalue.String("a"), 1, noReason), false, flag1Default, "")) es.summarizeEvent(withoutReasons.NewEvaluationData(flag2, user, ldreason.NewEvaluationDetail(ldvalue.String("c"), 3, noReason), false, flag2Default, "")) bytes, count := formatter.makeOutputEvents(nil, es.snapshot()) require.Equal(t, 1, count) // Using a nested matcher expression here, rather than an equality assertion on the whole JSON object, // because the ordering of array items in "counters" is indeterminate so we need m.ItemsInAnyOrder(). m.In(t).Assert(bytes, m.JSONArray().Should(m.Items( m.MapOf( m.KV("kind", m.Equal("summary")), m.KV("startDate", m.Not(m.BeNil())), m.KV("endDate", m.Not(m.BeNil())), m.KV("features", m.MapOf( m.KV("flag1", m.MapOf( m.KV("default", m.JSONEqual(flag1Default)), m.KV("counters", m.ItemsInAnyOrder( m.JSONStrEqual(`{"version":100,"variation":1,"value":"a","count":2}`), m.JSONStrEqual(`{"version":100,"variation":2,"value":"b","count":1}`), m.JSONStrEqual(`{"version":200,"variation":1,"value":"a","count":1}`), )), m.KV("contextKinds", m.Items(m.Equal("user"))), )), m.KV("flag2", m.MapOf( m.KV("default", m.JSONEqual(flag2Default)), m.KV("counters", m.ItemsInAnyOrder( m.JSONStrEqual(`{"version":1,"variation":3,"value":"c","count":1}`), )), m.KV("contextKinds", m.Items(m.Equal("user"))), )), )), ), ))) }) t.Run("summary with multiple context kinds", func(t *testing.T) { context1, context2, context3 := ldcontext.New("userkey1"), ldcontext.New("userkey2"), ldcontext.NewWithKind("org", "orgkey") es := newEventSummarizer() es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v1, Context(context1), ldreason.NewEvaluationDetail(ldvalue.String("a"), 1, noReason), false, flag1Default, "")) es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v1, Context(context2), ldreason.NewEvaluationDetail(ldvalue.String("b"), 2, noReason), false, flag1Default, "")) es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v1, Context(context3), ldreason.NewEvaluationDetail(ldvalue.String("a"), 1, noReason), false, flag1Default, "")) es.summarizeEvent(withoutReasons.NewEvaluationData(flag1v2, Context(context1), ldreason.NewEvaluationDetail(ldvalue.String("a"), 1, noReason), false, flag1Default, "")) es.summarizeEvent(withoutReasons.NewEvaluationData(flag2, Context(context1), ldreason.NewEvaluationDetail(ldvalue.String("c"), 3, noReason), false, flag2Default, "")) bytes, count := formatter.makeOutputEvents(nil, es.snapshot()) require.Equal(t, 1, count) m.In(t).Assert(bytes, m.JSONArray().Should(m.Items( m.MapOf( m.KV("kind", m.Equal("summary")), m.KV("startDate", m.Not(m.BeNil())), m.KV("endDate", m.Not(m.BeNil())), m.KV("features", m.MapOf( m.KV("flag1", m.MapOf( m.KV("default", m.JSONEqual(flag1Default)), m.KV("counters", m.Length().Should(m.Equal(3))), m.KV("contextKinds", m.ItemsInAnyOrder(m.Equal("user"), m.Equal("org"))), )), m.KV("flag2", m.MapOf( m.KV("default", m.JSONEqual(flag2Default)), m.KV("counters", m.Length().Should(m.Equal(1))), m.KV("contextKinds", m.Items(m.Equal("user"))), )), )), ), ))) }) t.Run("empty payload", func(t *testing.T) { bytes, count := formatter.makeOutputEvents([]anyEventOutput{}, eventSummary{}) assert.Nil(t, bytes) assert.Equal(t, 0, count) }) } func verifyEventOutput(t *testing.T, formatter eventOutputFormatter, event anyEventInput, jsonMatcher m.Matcher) { t.Helper() bytes, count := formatter.makeOutputEvents([]anyEventOutput{event}, eventSummary{}) require.Equal(t, 1, count) m.In(t).Assert(bytes, m.JSONArray().Should(m.Items(jsonMatcher))) } func verifySummaryEventOutput(t *testing.T, formatter eventOutputFormatter, summary eventSummary, jsonMatcher m.Matcher) { t.Helper() bytes, count := formatter.makeOutputEvents(nil, summary) require.Equal(t, 1, count) m.In(t).Assert(bytes, m.JSONArray().Should(m.Items(jsonMatcher))) }