diff --git a/splitio/common/impressionlistener/dtos.go b/splitio/common/impressionlistener/dtos.go index 111df2a7..e6aae466 100644 --- a/splitio/common/impressionlistener/dtos.go +++ b/splitio/common/impressionlistener/dtos.go @@ -9,6 +9,7 @@ type ImpressionForListener struct { Label string `json:"label"` BucketingKey string `json:"bucketingKey,omitempty"` Pt int64 `json:"pt,omitempty"` + Properties string `json:"properties,omitempty"` } // ImpressionsForListener struct for payload diff --git a/splitio/common/impressionlistener/listener_test.go b/splitio/common/impressionlistener/listener_test.go index 4a8fec47..d9a3a656 100644 --- a/splitio/common/impressionlistener/listener_test.go +++ b/splitio/common/impressionlistener/listener_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/splitio/go-split-commons/v9/dtos" + "github.com/stretchr/testify/assert" ) func TestImpressionListener(t *testing.T) { @@ -15,54 +16,220 @@ func TestImpressionListener(t *testing.T) { reqsDone := make(chan struct{}, 1) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { reqsDone <- struct{}{} }() - if r.URL.Path != "/someUrl" && r.Method != "POST" { - t.Error("Invalid request. Should be POST to /someUrl") - } + // Verify request method and path + assert.False(t, r.URL.Path != "/someUrl" && r.Method != "POST", "Invalid request. Should be POST to /someUrl") + // Read and parse request body body, err := ioutil.ReadAll(r.Body) r.Body.Close() + assert.False(t, err != nil, "Error reading body") if err != nil { - t.Error("Error reading body") return } var all impressionListenerPostBody err = json.Unmarshal(body, &all) + assert.False(t, err != nil, "Error parsing json: %v", err) if err != nil { - t.Errorf("Error parsing json: %s", err) return } - if all.SdkVersion != "go-1.1.1" || all.MachineIP != "1.2.3.4" || all.MachineName != "ip-1-2-3-4" { - t.Error("invalid metadata") - } + // Verify metadata + assert.False(t, all.SdkVersion != "go-1.1.1" || all.MachineIP != "1.2.3.4" || all.MachineName != "ip-1-2-3-4", "invalid metadata") + // Verify impressions imps := all.Impressions - if len(imps) != 2 { - t.Error("invalid number of impression groups received") - return - } + assert.Equal(t, 2, len(imps), "Should have 2 impression groups") + + // Verify first impression group (t1) + assert.Equal(t, "t1", imps[0].TestName, "First group should be t1") + assert.Equal(t, 2, len(imps[0].KeyImpressions), "t1 should have 2 impressions") + + // Verify first impression of t1 + assert.Equal(t, "k1", imps[0].KeyImpressions[0].KeyName, "t1 first impression should have correct key name") + assert.Equal(t, "on", imps[0].KeyImpressions[0].Treatment, "t1 first impression should have correct treatment") + assert.Equal(t, int64(1), imps[0].KeyImpressions[0].Time, "t1 first impression should have correct time") + assert.Equal(t, int64(2), imps[0].KeyImpressions[0].ChangeNumber, "t1 first impression should have correct change number") + assert.Equal(t, "l1", imps[0].KeyImpressions[0].Label, "t1 first impression should have correct label") + assert.Equal(t, "b1", imps[0].KeyImpressions[0].BucketingKey, "t1 first impression should have correct bucketing key") + assert.Equal(t, int64(1), imps[0].KeyImpressions[0].Pt, "t1 first impression should have correct pt") + + // Verify second impression of t1 + assert.Equal(t, "k2", imps[0].KeyImpressions[1].KeyName, "t1 second impression should have correct key name") + assert.Equal(t, "on", imps[0].KeyImpressions[1].Treatment, "t1 second impression should have correct treatment") + assert.Equal(t, int64(1), imps[0].KeyImpressions[1].Time, "t1 second impression should have correct time") + assert.Equal(t, int64(2), imps[0].KeyImpressions[1].ChangeNumber, "t1 second impression should have correct change number") + assert.Equal(t, "l1", imps[0].KeyImpressions[1].Label, "t1 second impression should have correct label") + assert.Equal(t, "b1", imps[0].KeyImpressions[1].BucketingKey, "t1 second impression should have correct bucketing key") + assert.Equal(t, int64(1), imps[0].KeyImpressions[1].Pt, "t1 second impression should have correct pt") + + // Verify second impression group (t2) + assert.Equal(t, "t2", imps[1].TestName, "Second group should be t2") + assert.Equal(t, 2, len(imps[1].KeyImpressions), "t2 should have 2 impressions") + + // Verify first impression of t2 + assert.Equal(t, "k1", imps[1].KeyImpressions[0].KeyName, "t2 first impression should have correct key name") + assert.Equal(t, "off", imps[1].KeyImpressions[0].Treatment, "t2 first impression should have correct treatment") + assert.Equal(t, int64(2), imps[1].KeyImpressions[0].Time, "t2 first impression should have correct time") + assert.Equal(t, int64(3), imps[1].KeyImpressions[0].ChangeNumber, "t2 first impression should have correct change number") + assert.Equal(t, "l2", imps[1].KeyImpressions[0].Label, "t2 first impression should have correct label") + assert.Equal(t, "b2", imps[1].KeyImpressions[0].BucketingKey, "t2 first impression should have correct bucketing key") + assert.Equal(t, int64(2), imps[1].KeyImpressions[0].Pt, "t2 first impression should have correct pt") + + // Verify second impression of t2 + assert.Equal(t, "k2", imps[1].KeyImpressions[1].KeyName, "t2 second impression should have correct key name") + assert.Equal(t, "off", imps[1].KeyImpressions[1].Treatment, "t2 second impression should have correct treatment") + assert.Equal(t, int64(2), imps[1].KeyImpressions[1].Time, "t2 second impression should have correct time") + assert.Equal(t, int64(3), imps[1].KeyImpressions[1].ChangeNumber, "t2 second impression should have correct change number") + assert.Equal(t, "l2", imps[1].KeyImpressions[1].Label, "t2 second impression should have correct label") + assert.Equal(t, "b2", imps[1].KeyImpressions[1].BucketingKey, "t2 second impression should have correct bucketing key") + assert.Equal(t, int64(3), imps[1].KeyImpressions[1].Pt, "t2 second impression should have correct pt") + })) + defer ts.Close() + + listener, err := NewImpressionBulkListener(ts.URL, 10, nil) + assert.False(t, err != nil, "error cannot be nil: %v", err) + + err = listener.Start() + assert.False(t, err != nil, "start() should not fail. Got: %v", err) + defer listener.Stop(true) + + listener.Submit([]ImpressionsForListener{ + ImpressionsForListener{ + TestName: "t1", + KeyImpressions: []ImpressionForListener{ + ImpressionForListener{ + KeyName: "k1", + Treatment: "on", + Time: 1, + ChangeNumber: 2, + Label: "l1", + BucketingKey: "b1", + Pt: 1, + }, + ImpressionForListener{ + KeyName: "k2", + Treatment: "on", + Time: 1, + ChangeNumber: 2, + Label: "l1", + BucketingKey: "b1", + Pt: 1, + }, + }, + }, + ImpressionsForListener{ + TestName: "t2", + KeyImpressions: []ImpressionForListener{ + ImpressionForListener{ + KeyName: "k1", + Treatment: "off", + Time: 2, + ChangeNumber: 3, + Label: "l2", + BucketingKey: "b2", + Pt: 2, + }, + ImpressionForListener{ + KeyName: "k2", + Treatment: "off", + Time: 2, + ChangeNumber: 3, + Label: "l2", + BucketingKey: "b2", + Pt: 3, + }, + }, + }, + }, &dtos.Metadata{SDKVersion: "go-1.1.1", MachineIP: "1.2.3.4", MachineName: "ip-1-2-3-4"}) + + <-reqsDone +} + +func TestImpressionListenerWithProperties(t *testing.T) { - if imps[0].TestName != "t1" || len(imps[0].KeyImpressions) != 2 { - t.Errorf("invalid ipmressions for t1") + reqsDone := make(chan struct{}, 1) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { reqsDone <- struct{}{} }() + // Verify request method and path + assert.False(t, r.URL.Path != "/someUrl" && r.Method != "POST", "Invalid request. Should be POST to /someUrl") + + // Read and parse request body + body, err := ioutil.ReadAll(r.Body) + r.Body.Close() + assert.False(t, err != nil, "Error reading body") + if err != nil { return } - if imps[1].TestName != "t2" || len(imps[1].KeyImpressions) != 2 { - t.Errorf("invalid ipmressions for t2") + var all impressionListenerPostBody + err = json.Unmarshal(body, &all) + assert.False(t, err != nil, "Error parsing json: %v", err) + if err != nil { return } + + // Verify metadata + assert.False(t, all.SdkVersion != "go-1.1.1" || all.MachineIP != "1.2.3.4" || all.MachineName != "ip-1-2-3-4", "invalid metadata") + + // Verify impressions + imps := all.Impressions + assert.Equal(t, 2, len(imps), "Should have 2 impression groups") + + // Verify first impression group (t1) + assert.Equal(t, "t1", imps[0].TestName, "First group should be t1") + assert.Equal(t, 2, len(imps[0].KeyImpressions), "t1 should have 2 impressions") + + // Verify first impression of t1 + assert.Equal(t, "k1", imps[0].KeyImpressions[0].KeyName, "t1 first impression should have correct key name") + assert.Equal(t, "on", imps[0].KeyImpressions[0].Treatment, "t1 first impression should have correct treatment") + assert.Equal(t, int64(1), imps[0].KeyImpressions[0].Time, "t1 first impression should have correct time") + assert.Equal(t, int64(2), imps[0].KeyImpressions[0].ChangeNumber, "t1 first impression should have correct change number") + assert.Equal(t, "l1", imps[0].KeyImpressions[0].Label, "t1 first impression should have correct label") + assert.Equal(t, "b1", imps[0].KeyImpressions[0].BucketingKey, "t1 first impression should have correct bucketing key") + assert.Equal(t, int64(1), imps[0].KeyImpressions[0].Pt, "t1 first impression should have correct pt") + assert.Equal(t, "{'prop':'val'}", imps[0].KeyImpressions[0].Properties, "First impression of t1 should have properties") + + // Verify second impression of t1 + assert.Equal(t, "k2", imps[0].KeyImpressions[1].KeyName, "t1 second impression should have correct key name") + assert.Equal(t, "on", imps[0].KeyImpressions[1].Treatment, "t1 second impression should have correct treatment") + assert.Equal(t, int64(1), imps[0].KeyImpressions[1].Time, "t1 second impression should have correct time") + assert.Equal(t, int64(2), imps[0].KeyImpressions[1].ChangeNumber, "t1 second impression should have correct change number") + assert.Equal(t, "l1", imps[0].KeyImpressions[1].Label, "t1 second impression should have correct label") + assert.Equal(t, "b1", imps[0].KeyImpressions[1].BucketingKey, "t1 second impression should have correct bucketing key") + assert.Equal(t, int64(1), imps[0].KeyImpressions[1].Pt, "t1 second impression should have correct pt") + // Second impression should not have properties + assert.Empty(t, imps[0].KeyImpressions[1].Properties, "Second impression of t1 should not have properties") + + // Verify second impression group (t2) + assert.Equal(t, "t2", imps[1].TestName, "Second group should be t2") + assert.Equal(t, 2, len(imps[1].KeyImpressions), "t2 should have 2 impressions") + + // Verify first impression of t2 + assert.Equal(t, "k1", imps[1].KeyImpressions[0].KeyName, "t2 first impression should have correct key name") + assert.Equal(t, "off", imps[1].KeyImpressions[0].Treatment, "t2 first impression should have correct treatment") + assert.Equal(t, int64(2), imps[1].KeyImpressions[0].Time, "t2 first impression should have correct time") + assert.Equal(t, int64(3), imps[1].KeyImpressions[0].ChangeNumber, "t2 first impression should have correct change number") + assert.Equal(t, "l2", imps[1].KeyImpressions[0].Label, "t2 first impression should have correct label") + assert.Equal(t, "b2", imps[1].KeyImpressions[0].BucketingKey, "t2 first impression should have correct bucketing key") + assert.Equal(t, int64(2), imps[1].KeyImpressions[0].Pt, "t2 first impression should have correct pt") + + // Verify second impression of t2 + assert.Equal(t, "k2", imps[1].KeyImpressions[1].KeyName, "t2 second impression should have correct key name") + assert.Equal(t, "off", imps[1].KeyImpressions[1].Treatment, "t2 second impression should have correct treatment") + assert.Equal(t, int64(2), imps[1].KeyImpressions[1].Time, "t2 second impression should have correct time") + assert.Equal(t, int64(3), imps[1].KeyImpressions[1].ChangeNumber, "t2 second impression should have correct change number") + assert.Equal(t, "l2", imps[1].KeyImpressions[1].Label, "t2 second impression should have correct label") + assert.Equal(t, "b2", imps[1].KeyImpressions[1].BucketingKey, "t2 second impression should have correct bucketing key") + assert.Equal(t, int64(3), imps[1].KeyImpressions[1].Pt, "t2 second impression should have correct pt") })) defer ts.Close() listener, err := NewImpressionBulkListener(ts.URL, 10, nil) - if err != nil { - t.Error("error cannot be nil: ", err) - } + assert.False(t, err != nil, "error cannot be nil: %v", err) - if err = listener.Start(); err != nil { - t.Error("start() should not fail. Got: ", err) - } + err = listener.Start() + assert.False(t, err != nil, "start() should not fail. Got: %v", err) defer listener.Stop(true) listener.Submit([]ImpressionsForListener{ @@ -77,6 +244,7 @@ func TestImpressionListener(t *testing.T) { Label: "l1", BucketingKey: "b1", Pt: 1, + Properties: "{'prop':'val'}", }, ImpressionForListener{ KeyName: "k2", diff --git a/splitio/common/impressionlistener/mocks/listener.go b/splitio/common/impressionlistener/mocks/listener.go index b834e9ad..7f181136 100644 --- a/splitio/common/impressionlistener/mocks/listener.go +++ b/splitio/common/impressionlistener/mocks/listener.go @@ -2,6 +2,7 @@ package mocks import ( "github.com/splitio/split-synchronizer/v5/splitio/common/impressionlistener" + "github.com/stretchr/testify/mock" "github.com/splitio/go-split-commons/v9/dtos" ) @@ -23,3 +24,22 @@ func (l *ImpressionBulkListenerMock) Start() error { func (l *ImpressionBulkListenerMock) Stop(blocking bool) error { return l.StopCall(blocking) } + +type MockImpressionBulkListener struct { + mock.Mock +} + +func (l *MockImpressionBulkListener) Submit(imps []impressionlistener.ImpressionsForListener, metadata *dtos.Metadata) error { + args := l.Called(imps, metadata) + return args.Error(1) +} + +func (l *MockImpressionBulkListener) Start() error { + args := l.Called() + return args.Error(1) +} + +func (l *MockImpressionBulkListener) Stop(blocking bool) error { + args := l.Called() + return args.Error(1) +} diff --git a/splitio/producer/task/impressions.go b/splitio/producer/task/impressions.go index e6d6c680..e6567e35 100644 --- a/splitio/producer/task/impressions.go +++ b/splitio/producer/task/impressions.go @@ -165,6 +165,7 @@ func (i *ImpressionsPipelineWorker) sendImpressionsToListener(b *impBatches) { Label: ki.Label, BucketingKey: ki.BucketingKey, Pt: ki.Pt, + Properties: ki.Properties, }) } payload = append(payload, forTest) @@ -268,6 +269,7 @@ func (s *impsWithMetadata) add(i *dtos.Impression) { Label: i.Label, BucketingKey: i.BucketingKey, Pt: i.Pt, + Properties: i.Properties, }) s.count++ } diff --git a/splitio/producer/task/impressions_test.go b/splitio/producer/task/impressions_test.go index 13b5e5ab..dca0c829 100644 --- a/splitio/producer/task/impressions_test.go +++ b/splitio/producer/task/impressions_test.go @@ -12,6 +12,7 @@ import ( "time" "github.com/splitio/split-synchronizer/v5/splitio/producer/evcalc" + "github.com/splitio/split-synchronizer/v5/splitio/common/impressionlistener" "github.com/splitio/go-split-commons/v9/dtos" "github.com/splitio/go-split-commons/v9/provisional" @@ -19,6 +20,8 @@ import ( "github.com/splitio/go-split-commons/v9/storage/inmemory" "github.com/splitio/go-split-commons/v9/storage/mocks" "github.com/splitio/go-toolkit/v5/logging" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) type trackingAllocator struct { @@ -108,6 +111,10 @@ func newTrackingAllocator() *trackingAllocator { } func makeSerializedImpressions(metadatas int, features int, keys int) [][]byte { + return makeSerializedImpressionsWithProperties(metadatas, features, keys, false) +} + +func makeSerializedImpressionsWithProperties(metadatas int, features int, keys int, withProperties bool) [][]byte { result := func(r []byte, _ error) []byte { return r } imps := make([][]byte, 0, metadatas*features*keys) for mindex := 0; mindex < metadatas; mindex++ { @@ -115,9 +122,17 @@ func makeSerializedImpressions(metadatas int, features int, keys int) [][]byte { for findex := 0; findex < features; findex++ { feature := "feat_" + strconv.Itoa(findex) for kindex := 0; kindex < keys; kindex++ { + imp := dtos.Impression{ + FeatureName: feature, + KeyName: "key_" + strconv.Itoa(kindex), + Time: int64(1 + mindex*findex*kindex), + } + if withProperties { + imp.Properties = "{'prop':'val'}" + } imps = append(imps, result(json.Marshal(&dtos.ImpressionQueueObject{ Metadata: metadata, - Impression: dtos.Impression{FeatureName: feature, KeyName: "key_" + strconv.Itoa(kindex), Time: int64(1 + mindex*findex*kindex)}, + Impression: imp, }))) } } @@ -166,6 +181,187 @@ func TestMemoryIsProperlyReturned(t *testing.T) { poolWrapper.validate(t) } +func TestImpressionsWithPropertiesIntegration(t *testing.T) { + + var mtx sync.Mutex + impsByMachineName := make(map[string]int, 3) + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Error("error reading body") + } + + var ti []dtos.ImpressionsDTO + if err := json.Unmarshal(body, &ti); err != nil { + t.Error("error deserializing body: ", err) + } + + impressionCount := 0 + for _, feature := range ti { + for _, imp := range feature.KeyImpressions { + impressionCount++ + assert.Equal(t, "{'prop':'val'}", imp.Properties, "Impression should have properties") + } + } + + machine := r.Header.Get("SplitSDKMachineName") + mtx.Lock() + impsByMachineName[machine] = impsByMachineName[machine] + impressionCount + mtx.Unlock() + })) + defer server.Close() + + imps := makeSerializedImpressionsWithProperties(3, 4, 20, true) + var calls int64 + st := &mocks.MockImpressionStorage{ + PopNRawCall: func(int64) ([]string, int64, error) { + atomic.AddInt64(&calls, 1) + return []string{}, 0, nil + }, + } + + impressionCounter := strategy.NewImpressionsCounter() + impressionObserver, _ := strategy.NewImpressionObserver(500) + strategy := strategy.NewOptimizedImpl(impressionObserver, impressionCounter, &inmemory.TelemetryStorage{}, false) + + w, err := NewImpressionWorker(&ImpressionWorkerConfig{ + EvictionMonitor: evcalc.New(1), + Logger: logging.NewLogger(nil), + ImpressionsListener: nil, + Storage: st, + URL: server.URL, + Apikey: "someApikey", + FetchSize: 100, + ImpressionManager: provisional.NewImpressionManager(strategy), + }) + if err != nil { + t.Error("there should be no error. Got: ", err) + } + + sinker := make(chan interface{}, 100) + w.Process(imps, sinker) + + for i := 0; i < 3; i++ { + i := <-sinker + req, err := w.BuildRequest(i) + if err != nil { + t.Error("there should be no error. Got: ", err) + } + + // Make the actual HTTP request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + t.Error("error making request: ", err) + } + resp.Body.Close() + + if asRecyclable, ok := i.(recyclable); ok { + asRecyclable.recycle() + } + } + + if len(impsByMachineName) != 3 { + t.Error("wrong number of machines reporting impressions") + } + + for _, count := range impsByMachineName { + if count != 80 { + t.Error("wrong number of impressions per machine") + } + } +} + +type mockImpressionBulkListener struct { + mock.Mock +} + +func (m *mockImpressionBulkListener) Submit(imps []impressionlistener.ImpressionsForListener, metadata *dtos.Metadata) error { + args := m.Called(imps, metadata) + return args.Error(0) +} + +func (m *mockImpressionBulkListener) Start() error { + args := m.Called() + return args.Error(0) +} + +func (m *mockImpressionBulkListener) Stop(blocking bool) error { + args := m.Called(blocking) + return args.Error(0) +} + +func TestSendImpressionsToListener(t *testing.T) { + // Create a mock listener + mockListener := new(mockImpressionBulkListener) + + // Setup expectations + mockListener.On("Submit", mock.MatchedBy(func(imps []impressionlistener.ImpressionsForListener) bool { + if len(imps) != 1 { + return false + } + if len(imps[0].KeyImpressions) != 1 { + return false + } + imp := imps[0].KeyImpressions[0] + return imps[0].TestName == "test-feature" && + imp.KeyName == "test-key" && + imp.Treatment == "on" && + imp.Time == int64(123) && + imp.Properties == "{'prop':'val'}" && + imp.Label == "test-label" && + imp.BucketingKey == "test-bucket" && + imp.ChangeNumber == int64(456) && + imp.Pt == int64(789) + }), mock.MatchedBy(func(metadata *dtos.Metadata) bool { + return metadata.SDKVersion == "go-1.1.1" && + metadata.MachineIP == "1.2.3.4" && + metadata.MachineName == "test-machine" + })).Return(nil).Once() + + // Create a worker with the mock listener + w, err := NewImpressionWorker(&ImpressionWorkerConfig{ + EvictionMonitor: evcalc.New(1), + Logger: logging.NewLogger(nil), + ImpressionsListener: mockListener, + Storage: mocks.MockImpressionStorage{}, + URL: "http://test", + Apikey: "someApikey", + FetchSize: 100, + ImpressionManager: provisional.NewImpressionManager(strategy.NewOptimizedImpl(nil, nil, &inmemory.TelemetryStorage{}, false)), + }) + assert.NoError(t, err) + + // Create test impressions + batches := newImpBatches(w.pool) + + // Add test impressions + batches.add(&dtos.ImpressionQueueObject{ + Metadata: dtos.Metadata{ + SDKVersion: "go-1.1.1", + MachineIP: "1.2.3.4", + MachineName: "test-machine", + }, + Impression: dtos.Impression{ + FeatureName: "test-feature", + KeyName: "test-key", + Treatment: "on", + Time: 123, + Properties: "{'prop':'val'}", + Label: "test-label", + BucketingKey: "test-bucket", + ChangeNumber: 456, + Pt: 789, + }, + }) + + // Send impressions to listener + w.sendImpressionsToListener(batches) + + // Verify that all expected calls were made + mockListener.AssertExpectations(t) +} + func TestImpressionsIntegration(t *testing.T) { var mtx sync.Mutex diff --git a/splitio/proxy/controllers/events.go b/splitio/proxy/controllers/events.go index 9ff43270..15a41c6c 100644 --- a/splitio/proxy/controllers/events.go +++ b/splitio/proxy/controllers/events.go @@ -281,6 +281,7 @@ func (c *EventsServerController) submitImpressionsToListener(raw []byte, metadat Label: ki.Label, BucketingKey: ki.BucketingKey, Pt: ki.Pt, + Properties: ki.Properties, }) } forListener = append(forListener, impressionlistener.ImpressionsForListener{ diff --git a/splitio/proxy/controllers/events_test.go b/splitio/proxy/controllers/events_test.go index 85a8ea29..12e016da 100644 --- a/splitio/proxy/controllers/events_test.go +++ b/splitio/proxy/controllers/events_test.go @@ -6,12 +6,15 @@ import ( "net/http" "net/http/httptest" "testing" + "time" "github.com/splitio/split-synchronizer/v5/splitio/common/impressionlistener" ilMock "github.com/splitio/split-synchronizer/v5/splitio/common/impressionlistener/mocks" mw "github.com/splitio/split-synchronizer/v5/splitio/proxy/controllers/middleware" "github.com/splitio/split-synchronizer/v5/splitio/proxy/internal" "github.com/splitio/split-synchronizer/v5/splitio/proxy/tasks/mocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/splitio/go-split-commons/v9/dtos" "github.com/splitio/go-toolkit/v5/logging" @@ -121,6 +124,100 @@ func TestPostImpressionsbulk(t *testing.T) { } } +func TestPostImpressionsbulkWithProperties(t *testing.T) { + gin.SetMode(gin.TestMode) + resp := httptest.NewRecorder() + ctx, router := gin.CreateTestContext(resp) + + logger := logging.NewLogger(nil) + apikeyValidator := mw.NewAPIKeyValidator([]string{"someApiKey"}) + + impressionsSink := mocks.DeferredRecordingTaskMock{} + listener := ilMock.MockImpressionBulkListener{} + group := router.Group("/api") + controller := NewEventsServerController( + logger, + &impressionsSink, // impssions + &mocks.DeferredRecordingTaskMock{}, // imp counts + &mocks.DeferredRecordingTaskMock{}, // events + &listener, + apikeyValidator.IsValid, + ) + controller.Register(group, group) + + // Set up expectations for impressionsSink + impressionsSink.On("Stage", mock.Anything).Return(nil, nil) + + // Set up expectations for listener + listener.On("Submit", mock.MatchedBy(func(imps []impressionlistener.ImpressionsForListener) bool { + // Verify the structure and content of impressions + assert.Equal(t, 2, len(imps), "Should have 2 impression groups") + + // Check test1 impressions + assert.Equal(t, "test1", imps[0].TestName, "First group should be test1") + assert.Equal(t, 3, len(imps[0].KeyImpressions), "test1 should have 3 impressions") + + // Verify properties for test1 + assert.Equal(t, "{'prop':'val'}", imps[0].KeyImpressions[0].Properties, "First impression of test1 should have properties") + assert.Equal(t, "{'prop':'val'}", imps[0].KeyImpressions[1].Properties, "Second impression of test1 should have properties") + + // Check test2 impressions + assert.Equal(t, "test2", imps[1].TestName, "Second group should be test2") + assert.Equal(t, 4, len(imps[1].KeyImpressions), "test2 should have 4 impressions") + + // Verify properties for test2 + assert.Equal(t, "{'prop':'val'}", imps[1].KeyImpressions[0].Properties, "First impression of test2 should have properties") + assert.Equal(t, "{'prop':'val'}", imps[1].KeyImpressions[1].Properties, "Second impression of test2 should have properties") + + return true + }), mock.MatchedBy(func(metadata *dtos.Metadata) bool { + expected := dtos.Metadata{SDKVersion: "go-1.1.1", MachineIP: "1.2.3.4", MachineName: "ip-1-2-3-4"} + assert.Equal(t, expected, *metadata, "Metadata should match expected values") + return true + })).Return(nil, nil) + + serialized, err := json.Marshal([]dtos.ImpressionsDTO{ + { + TestName: "test1", + KeyImpressions: []dtos.ImpressionDTO{ + {KeyName: "k1", Treatment: "on", Time: 1, ChangeNumber: 2, Label: "l1", BucketingKey: "b1", Pt: 0, Properties: "{'prop':'val'}"}, + {KeyName: "k2", Treatment: "on", Time: 2, ChangeNumber: 3, Label: "l2", BucketingKey: "b2", Pt: 0, Properties: "{'prop':'val'}"}, + {KeyName: "k3", Treatment: "on", Time: 3, ChangeNumber: 4, Label: "l3", BucketingKey: "b3", Pt: 0}, + }, + }, + { + TestName: "test2", + KeyImpressions: []dtos.ImpressionDTO{ + {KeyName: "k1", Treatment: "off", Time: 1, ChangeNumber: 2, Label: "l1", BucketingKey: "b1", Pt: 0, Properties: "{'prop':'val'}"}, + {KeyName: "k2", Treatment: "off", Time: 2, ChangeNumber: 3, Label: "l2", BucketingKey: "b2", Pt: 0, Properties: "{'prop':'val'}"}, + {KeyName: "k3", Treatment: "off", Time: 3, ChangeNumber: 4, Label: "l3", BucketingKey: "b3", Pt: 0}, + {KeyName: "k4", Treatment: "off", Time: 4, ChangeNumber: 5, Label: "l4", BucketingKey: "b4", Pt: 0}, + }, + }, + }) + + if err != nil { + t.Error("should not have errors when serializing: ", err) + } + + ctx.Request, _ = http.NewRequest(http.MethodPost, "/api/testImpressions/bulk", bytes.NewBuffer(serialized)) + ctx.Request.Header.Set("Authorization", "Bearer someApiKey") + ctx.Request.Header.Set("SplitSDKImpressionsMode", "optimized") + ctx.Request.Header.Set("SplitSDKVersion", "go-1.1.1") + ctx.Request.Header.Set("SplitSDKMachineIp", "1.2.3.4") + ctx.Request.Header.Set("SplitSDKMachineName", "ip-1-2-3-4") + router.ServeHTTP(resp, ctx.Request) + + assert.Equal(t, 200, resp.Code, "Status code should be 200") + + // Add a small delay to allow the goroutine to complete + time.Sleep(50 * time.Millisecond) + + // Verify that all expectations were met + listener.AssertExpectations(t) + impressionsSink.AssertExpectations(t) +} + func TestPostEventsBulk(t *testing.T) { gin.SetMode(gin.TestMode) resp := httptest.NewRecorder() diff --git a/splitio/proxy/tasks/mocks/deferred.go b/splitio/proxy/tasks/mocks/deferred.go index 002395b2..afb6da00 100644 --- a/splitio/proxy/tasks/mocks/deferred.go +++ b/splitio/proxy/tasks/mocks/deferred.go @@ -1,5 +1,7 @@ package mocks +import "github.com/stretchr/testify/mock" + type MockDeferredRecordingTask struct { StageCall func(rawData interface{}) error StartCall func() @@ -22,3 +24,26 @@ func (t *MockDeferredRecordingTask) Stop(blocking bool) error { func (t *MockDeferredRecordingTask) IsRunning() bool { return t.IsRunningCall() } + +type DeferredRecordingTaskMock struct { + mock.Mock +} + +func (t *DeferredRecordingTaskMock) Stage(rawData interface{}) error { + args := t.Called(rawData) + return args.Error(1) +} + +func (t *DeferredRecordingTaskMock) Start() { + t.Called() +} + +func (t *DeferredRecordingTaskMock) Stop(blocking bool) error { + args := t.Called(blocking) + return args.Error(1) +} + +func (t *DeferredRecordingTaskMock) IsRunning() bool { + args := t.Called() + return args.Get(0).(bool) +}