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") diff --git a/sdktests/common_tests_stream_fdv2.go b/sdktests/common_tests_stream_fdv2.go index 7d907ce..a0bb4bf 100644 --- a/sdktests/common_tests_stream_fdv2.go +++ b/sdktests/common_tests_stream_fdv2.go @@ -38,6 +38,10 @@ 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 +55,70 @@ 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) { + emptyPayload := mockld.NewServerSDKDataBuilder(). + 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(emptyPayload), DataSystemOptionPollingInitializer(initialStatefulData)) + + // Force the first endpoint to fail + dataSystem.Initializers[0].Endpoint().Close() + + client := NewSDKClient(t, dataSystem) + + // 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} + 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{