From e1e8cda0d237a100ca2ff9b9b15879fc236a0735 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Thu, 21 Nov 2024 15:58:52 -0500 Subject: [PATCH 1/4] chore: Add tests for polling initialization --- sdktests/common_tests_stream_fdv2.go | 72 ++++++++++++++++++++++++++++ sdktests/testapi_sdk_data_system.go | 36 +++++++++++++- 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/sdktests/common_tests_stream_fdv2.go b/sdktests/common_tests_stream_fdv2.go index 7d907ce..ff38506 100644 --- a/sdktests/common_tests_stream_fdv2.go +++ b/sdktests/common_tests_stream_fdv2.go @@ -38,6 +38,9 @@ func (c CommonStreamingTests) FDv2(t *ldtest.T) { func (c CommonStreamingTests) StateTransitions(t *ldtest.T) { t.Run("initializes from an empty state", c.InitializeFromEmptyState) + t.Run("initializes from polling initializer", c.InitializeFromPollingInitializer) + t.Run("initializes from polling initializer + streaming updates", c.InitializeFromPollingInitializerWithStreamingUpdates) + t.Run("initializes from 2 polling initializers", c.InitializeFromTwoPollingInitializers) t.Run("saves previously known state", c.SavesPreviouslyKnownState) t.Run("replaces previously known state", c.ReplacesPreviouslyKnownState) t.Run("updates previously known state", c.UpdatesPreviouslyKnownState) @@ -51,6 +54,75 @@ func (c CommonStreamingTests) InitializeFromEmptyState(t *ldtest.T) { validatePayloadReceived(t, dataSystem.PrimarySync().endpoint, client, "", expectedEvaluations) } +func (c CommonStreamingTests) InitializeFromPollingInitializer(t *ldtest.T) { + dataBefore := mockld.NewServerSDKDataBuilder().Flag(c.makeServerSideFlag("flag-key", 1, initialValue)).Build() + dataAfter := mockld.NewServerSDKDataBuilder().IntentCode("xfer-none").IntentReason("up-to-date").Build() + dataSystem := NewSDKDataSystem(t, dataAfter, DataSystemOptionPollingInitializer(dataBefore)) + + client := NewSDKClient(t, dataSystem) + + _, err := dataSystem.Initializers[0].Endpoint().AwaitConnection(time.Second) + require.NoError(t, err) + + expectedEvaluations := map[string]ldvalue.Value{"flag-key": initialValue} + validatePayloadReceived(t, dataSystem.PrimarySync().Endpoint(), client, "initial", expectedEvaluations) +} + +func (c CommonStreamingTests) InitializeFromPollingInitializerWithStreamingUpdates(t *ldtest.T) { + dataBefore := mockld.NewServerSDKDataBuilder().Flag(c.makeServerSideFlag("flag-key", 1, initialValue)).Build() + dataAfter := mockld.NewServerSDKDataBuilder(). + IntentCode("xfer-changes"). + IntentReason("stale"). + Flag(c.makeServerSideFlag("flag-key", 2, updatedValue)). + Flag(c.makeServerSideFlag("new-flag-key", 1, newInitialValue)). + Build() + dataSystem := NewSDKDataSystem(t, dataBefore, DataSystemOptionPollingInitializer(dataBefore)) + dataSystem.PrimarySync().streaming.SetInitialData(dataAfter) + + client := NewSDKClient(t, dataSystem) + + _, err := dataSystem.Initializers[0].Endpoint().AwaitConnection(time.Second) + require.NoError(t, err) + + expectedEvaluations := map[string]ldvalue.Value{"flag-key": initialValue, "new-flag-key": newInitialValue} + validatePayloadReceived(t, dataSystem.PrimarySync().Endpoint(), client, "initial", expectedEvaluations) +} + +func (c CommonStreamingTests) InitializeFromTwoPollingInitializers(t *ldtest.T) { + statelessInitialData := mockld.NewServerSDKDataBuilder(). + IntentCode("xfer-full"). + IntentReason("payload-missing"). + // No state means the initializer chain should continue + State(""). + // Subsequent initializer should knock this out + Flag(c.makeServerSideFlag("ancient-flag-key", 1, initialValue)). + Flag(c.makeServerSideFlag("flag-key", 1, initialValue)). + Build() + initialStatefulData := mockld.NewServerSDKDataBuilder(). + IntentCode("xfer-full"). + IntentReason("payload-missing"). + State("expected-state"). + Flag(c.makeServerSideFlag("flag-key", 2, updatedValue)). + Build() + streamingData := mockld.NewServerSDKDataBuilder(). + IntentCode("none"). + IntentReason("up-to-date"). + State("expected-state"). + Build() + dataSystem := NewSDKDataSystem(t, streamingData, DataSystemOptionPollingInitializer(statelessInitialData), DataSystemOptionPollingInitializer(initialStatefulData)) + + client := NewSDKClient(t, dataSystem) + + _, err := dataSystem.Initializers[0].Endpoint().AwaitConnection(time.Second) + require.NoError(t, err) + + _, err = dataSystem.Initializers[1].Endpoint().AwaitConnection(time.Second) + require.NoError(t, err) + + expectedEvaluations := map[string]ldvalue.Value{"flag-key": updatedValue, "ancient-flag-key": defaultValue} + validatePayloadReceived(t, dataSystem.PrimarySync().Endpoint(), client, "expected-state", expectedEvaluations) +} + func (c CommonStreamingTests) SavesPreviouslyKnownState(t *ldtest.T) { dataBefore := c.makeSDKDataWithFlag(1, initialValue) dataAfter := mockld.NewServerSDKDataBuilder().IntentCode("xfer-none").IntentReason("up-to-date").Build() diff --git a/sdktests/testapi_sdk_data_system.go b/sdktests/testapi_sdk_data_system.go index 9cc2775..e6b2683 100644 --- a/sdktests/testapi_sdk_data_system.go +++ b/sdktests/testapi_sdk_data_system.go @@ -13,12 +13,21 @@ import ( ) type sdkDataSystemConfig struct { - polling o.Maybe[bool] // true, false, or "undefined, use the default" + polling o.Maybe[bool] // true, false, or "undefined, use the default" + pollingInitializers []mockld.FDv2SDKData } // SDKDataSystemOption is the interface for options to NewSDKDataSystem. type SDKDataSystemOption helpers.ConfigOption[sdkDataSystemConfig] +// DataSystemOptionPollingInitializer adds support for a polling initializer +func DataSystemOptionPollingInitializer(data mockld.FDv2SDKData) SDKDataSystemOption { + return helpers.ConfigOptionFunc[sdkDataSystemConfig](func(c *sdkDataSystemConfig) error { + c.pollingInitializers = append(c.pollingInitializers, data) + return nil + }) +} + // DataSystemOptionPolling makes an SDKDataSystem simulate the polling service. func DataSystemOptionPolling() SDKDataSystemOption { return helpers.ConfigOptionFunc[sdkDataSystemConfig](func(c *sdkDataSystemConfig) error { @@ -212,6 +221,20 @@ func NewSDKDataSystem( t *ldtest.T, data mockld.SDKData, options ...SDKDataSystemOption) *SDKDataSystem { dataSystem := NewSDKDataSystemWithoutEndpoints(t, data, options...) + if dataSystem.Initializers != nil { + for i, initializer := range dataSystem.Initializers { + if initializer.pollingService == nil { + continue + } + + initializer.endpoint = + requireContext(t).harness.NewMockEndpoint(initializer.pollingService, t.DebugLogger(), + harness.MockEndpointDescription("polling initializer")) + + dataSystem.Initializers[i] = initializer + } + } + if dataSystem.Synchronizers != nil { isPolling := dataSystem.PrimarySync().polling != nil handler := helpers.IfElse[http.Handler](isPolling, @@ -251,8 +274,17 @@ func NewSDKDataSystemWithoutEndpoints( var config sdkDataSystemConfig _ = helpers.ApplyOptions(&config, options...) - defaultIsPolling := sdkKind == mockld.JSClientSDK || sdkKind == mockld.PHPSDK d := &SDKDataSystem{} + d.t = t + + for _, initializer := range config.pollingInitializers { + d.Initializers = append(d.Initializers, DataInitializer{ + pollingService: mockld.NewPollingService(initializer, sdkKind, t.DebugLogger()). + WithGzipCompression(t.Capabilities().Has(servicedef.CapabilityPollingGzip)), + }) + } + + defaultIsPolling := sdkKind == mockld.JSClientSDK || sdkKind == mockld.PHPSDK if config.polling.Value() || (!config.polling.IsDefined() && defaultIsPolling) { d.Synchronizers = &Synchronizers{ primary: Synchronizer{ From 3e081139a807c23ab22093c0e68319382f125e72 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Fri, 22 Nov 2024 10:20:22 -0500 Subject: [PATCH 2/4] Fix linting issues --- sdktests/common_tests_stream_fdv2.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sdktests/common_tests_stream_fdv2.go b/sdktests/common_tests_stream_fdv2.go index ff38506..6f21789 100644 --- a/sdktests/common_tests_stream_fdv2.go +++ b/sdktests/common_tests_stream_fdv2.go @@ -39,7 +39,8 @@ func (c CommonStreamingTests) FDv2(t *ldtest.T) { func (c CommonStreamingTests) StateTransitions(t *ldtest.T) { t.Run("initializes from an empty state", c.InitializeFromEmptyState) t.Run("initializes from polling initializer", c.InitializeFromPollingInitializer) - t.Run("initializes from polling initializer + streaming updates", c.InitializeFromPollingInitializerWithStreamingUpdates) + t.Run("initializes from polling initializer + streaming updates", + c.InitializeFromPollingInitializerWithStreamingUpdates) t.Run("initializes from 2 polling initializers", c.InitializeFromTwoPollingInitializers) t.Run("saves previously known state", c.SavesPreviouslyKnownState) t.Run("replaces previously known state", c.ReplacesPreviouslyKnownState) @@ -109,7 +110,8 @@ func (c CommonStreamingTests) InitializeFromTwoPollingInitializers(t *ldtest.T) IntentReason("up-to-date"). State("expected-state"). Build() - dataSystem := NewSDKDataSystem(t, streamingData, DataSystemOptionPollingInitializer(statelessInitialData), DataSystemOptionPollingInitializer(initialStatefulData)) + dataSystem := NewSDKDataSystem(t, streamingData, + DataSystemOptionPollingInitializer(statelessInitialData), DataSystemOptionPollingInitializer(initialStatefulData)) client := NewSDKClient(t, dataSystem) From cf489e0b6fd56f8f11b8e1ebfe55194df3c0c9a3 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Fri, 22 Nov 2024 17:09:26 -0500 Subject: [PATCH 3/4] Update test to have failed initializer --- sdktests/common_tests_stream_fdv2.go | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/sdktests/common_tests_stream_fdv2.go b/sdktests/common_tests_stream_fdv2.go index 6f21789..a0bb4bf 100644 --- a/sdktests/common_tests_stream_fdv2.go +++ b/sdktests/common_tests_stream_fdv2.go @@ -90,14 +90,7 @@ func (c CommonStreamingTests) InitializeFromPollingInitializerWithStreamingUpdat } func (c CommonStreamingTests) InitializeFromTwoPollingInitializers(t *ldtest.T) { - statelessInitialData := mockld.NewServerSDKDataBuilder(). - IntentCode("xfer-full"). - IntentReason("payload-missing"). - // No state means the initializer chain should continue - State(""). - // Subsequent initializer should knock this out - Flag(c.makeServerSideFlag("ancient-flag-key", 1, initialValue)). - Flag(c.makeServerSideFlag("flag-key", 1, initialValue)). + emptyPayload := mockld.NewServerSDKDataBuilder(). Build() initialStatefulData := mockld.NewServerSDKDataBuilder(). IntentCode("xfer-full"). @@ -111,17 +104,18 @@ func (c CommonStreamingTests) InitializeFromTwoPollingInitializers(t *ldtest.T) State("expected-state"). Build() dataSystem := NewSDKDataSystem(t, streamingData, - DataSystemOptionPollingInitializer(statelessInitialData), DataSystemOptionPollingInitializer(initialStatefulData)) + DataSystemOptionPollingInitializer(emptyPayload), DataSystemOptionPollingInitializer(initialStatefulData)) - client := NewSDKClient(t, dataSystem) + // Force the first endpoint to fail + dataSystem.Initializers[0].Endpoint().Close() - _, err := dataSystem.Initializers[0].Endpoint().AwaitConnection(time.Second) - require.NoError(t, err) + client := NewSDKClient(t, dataSystem) - _, err = dataSystem.Initializers[1].Endpoint().AwaitConnection(time.Second) + // Verify the initializers fall over to the next initializer in line. + _, err := dataSystem.Initializers[1].Endpoint().AwaitConnection(time.Second) require.NoError(t, err) - expectedEvaluations := map[string]ldvalue.Value{"flag-key": updatedValue, "ancient-flag-key": defaultValue} + expectedEvaluations := map[string]ldvalue.Value{"flag-key": updatedValue} validatePayloadReceived(t, dataSystem.PrimarySync().Endpoint(), client, "expected-state", expectedEvaluations) } From c38d50708394cf7f02ded78b854815ab3ed63519 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 25 Nov 2024 15:39:51 -0500 Subject: [PATCH 4/4] chore: Remove unsupported non-context endpoints --- mockld/polling_service.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/mockld/polling_service.go b/mockld/polling_service.go index ee9482e..b0e5b5f 100644 --- a/mockld/polling_service.go +++ b/mockld/polling_service.go @@ -19,18 +19,6 @@ const ( PollingPathJSClientGet = "/sdk/evalx/{env}/contexts/{context}" PollingPathJSClientReport = "/sdk/evalx/{env}/context" - // The following endpoint paths were used by older SDKs based on the user model rather than - // the context model. New context-aware SDKs should always use the new paths. However, our - // mock service still supports the old paths (just as the real LD services do). We have - // specific tests to verify that the SDKs use the new paths; in all other tests, if the SDK - // uses an old path, it will still work so that we don't confusingly see every test fail. - // We do *not* support the very old "eval" (as opposed to "evalx") paths since the only SDKs - // that used them are long past EOL. - PollingPathMobileGetUser = "/msdk/evalx/users/{context}" - PollingPathMobileReportUser = "/msdk/evalx/user" - PollingPathJSClientGetUser = "/sdk/evalx/{env}/users/{context}" - PollingPathJSClientReportUser = "/sdk/evalx/{env}/user" - PollingPathPHPAllFlags = "/sdk/flags" PollingPathPHPFlag = "/sdk/flags/{key}" PollingPathPHPSegment = "/sdk/segments/{key}" @@ -70,14 +58,10 @@ func NewPollingService( case MobileSDK: router.Handle(PollingPathMobileGet, pollHandler).Methods("GET") router.Handle(PollingPathMobileReport, pollHandler).Methods("REPORT") - router.Handle(PollingPathMobileGetUser, pollHandler).Methods("GET") - router.Handle(PollingPathMobileReportUser, pollHandler).Methods("REPORT") // Note that we only support the "evalx", not the older "eval" which is used only by old unsupported SDKs case JSClientSDK: router.Handle(PollingPathJSClientGet, pollHandler).Methods("GET") router.Handle(PollingPathJSClientReport, pollHandler).Methods("REPORT") - router.Handle(PollingPathJSClientGetUser, pollHandler).Methods("GET") - router.Handle(PollingPathJSClientReportUser, pollHandler).Methods("REPORT") case PHPSDK: router.Handle(PollingPathPHPFlag, p.phpFlagHandler()).Methods("GET") router.Handle(PollingPathPHPSegment, p.phpSegmentHandler()).Methods("GET")