diff --git a/CHANGELOG.md b/CHANGELOG.md index 685cb261..44a4de5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ All notable changes to the LaunchDarkly iOS SDK will be documented in this file. ### Multiple Environment clients Version 4.0.0 does not support multiple environments. If you use version `2.14.0` or later and set `LDConfig`'s `secondaryMobileKeys` you will not be able to migrate to version `4.0.0`. Multiple Environments will be added in a future release to the Swift SDK. +## [4.7.0] - 2020-06-03 +### Added +- Added a new method signature for `startCompleteWhenFlagsReceived` that accepts an additional argument specifying a maximum time to wait for flags to be received before calling the completion closure. The completion closure on this method will be passed a `Bool` on completion indication whether the operation timed out. ## [4.6.0] - 2020-05-26 ### Added diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index 616451a9..4bd7a132 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |ld| ld.name = "LaunchDarkly" - ld.version = "4.6.0" + ld.version = "4.7.0" ld.summary = "iOS SDK for LaunchDarkly" ld.description = <<-DESC @@ -25,7 +25,7 @@ Pod::Spec.new do |ld| ld.tvos.deployment_target = "9.0" ld.osx.deployment_target = "10.10" - ld.source = { :git => "https://github.com/launchdarkly/ios-client-sdk.git", :tag => '4.6.0'} + ld.source = { :git => "https://github.com/launchdarkly/ios-client-sdk.git", :tag => '4.7.0'} ld.source_files = "LaunchDarkly/LaunchDarkly/**/*.{h,m,swift}" diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 3f4007e8..bdefc060 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -361,7 +361,44 @@ public class LDClient { completion?() } } + + /** + See [start](x-source-tag://start) for more information on starting the SDK. + + This method listens for flag updates so the completion will only return once an update has occurred. If the SDK is configured to start offline the method will ignore the timeout and call the completion with True without awaiting a flag update. + + - parameter config: The LDConfig that contains the desired configuration. (Required) + - parameter user: The LDUser set with the desired user. If omitted, LDClient retains the previously set user, or default if one was never set. (Optional) + - parameter startWaitSeconds: A TimeInterval representing how long to wait for flags before returning true in the completion to indicate that it timed out. + - parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays. Takes a Bool as a parameter that indicates whether the SDK did not come online within startWaitSeconds. (Optional) + */ + public func startCompleteWhenFlagsReceived(config: LDConfig, user: LDUser? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { + if !config.startOnline { + startCompleteWhenFlagsReceived(config: config, user: user) + completion?(timeOutCheck) + } else { + let startTime = Date().timeIntervalSince1970 + startCompleteWhenFlagsReceived(config: config, user: user) { + if startTime + startWaitSeconds > Date().timeIntervalSince1970 { + self.internalTimeOutCheckQueue.sync { + self.timeOutCheck = false + completion?(self.timeOutCheck) + } + } + } + DispatchQueue.global().asyncAfter(deadline: .now() + startWaitSeconds) { + self.internalTimeOutCheckQueue.sync { + if self.timeOutCheck { + completion?(self.timeOutCheck) + } + } + } + } + } + private var timeOutCheck = true + private let internalTimeOutCheckQueue: DispatchQueue = DispatchQueue(label: "TimeOutQueue") + private func convertCachedData(skipDuringStart skip: Bool) { guard !skip else { @@ -1009,7 +1046,7 @@ public class LDClient { private(set) var flagCache: FeatureFlagCaching private(set) var cacheConverter: CacheConverting private(set) var flagSynchronizer: LDFlagSynchronizing - private(set) var flagChangeNotifier: FlagChangeNotifying + var flagChangeNotifier: FlagChangeNotifying private(set) var eventReporter: EventReporting private(set) var environmentReporter: EnvironmentReporting private(set) var throttler: Throttling diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index c9e7c1e7..9b75191f 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -180,6 +180,20 @@ public final class ObjcLDClient: NSObject { @objc public func startCompleteWhenFlagsReceived(config configWrapper: ObjcLDConfig, user userWrapper: ObjcLDUser? = nil, completion: (() -> Void)? = nil) { LDClient.shared.startCompleteWhenFlagsReceived(config: configWrapper.config, user: userWrapper?.user, completion: completion) } + + /** + See [start](x-source-tag://start) for more information on starting the SDK. + + This method listens for flag updates so the completion will only return once an update has occurred. If the SDK is configured to start offline the method will ignore the timeout and call the completion with True without awaiting a flag update. + + - parameter configWrapper: The LDConfig that contains the desired configuration. (Required) + - parameter userWrapper: The LDUser set with the desired user. If omitted, LDClient retains the previously set user, or default if one was never set. (Optional) + - parameter startWaitSeconds: A TimeInterval representing how long to wait for flags before returning true in the completion to indicate that it timed out. + - parameter completion: Closure called when the embedded `setOnlineIdentify` call completes, subject to throttling delays. Takes a Bool as a parameter that indicates whether the SDK did not come online within startWaitSeconds. (Optional) + */ + @objc public func startCompleteWhenFlagsReceived(config configWrapper: ObjcLDConfig, user userWrapper: ObjcLDUser? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { + LDClient.shared.startCompleteWhenFlagsReceived(config: configWrapper.config, user: userWrapper?.user, startWaitSeconds: startWaitSeconds, completion: completion) + } /** Stops the LDClient. Stopping the client means the LDClient goes offline and stops recording events. LDClient will no longer provide feature flag values, only returning fallback values. diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index 16c5077a..64cb0bcd 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -200,6 +200,8 @@ final class LDClientSpec: QuickSpec { override func spec() { startSpec() + startAwaitingFlagsSpec() + startAwaitingFlagsWithTimeoutSpec() setConfigSpec() setUserSpec() setOnlineSpec() @@ -665,8 +667,10 @@ final class LDClientSpec: QuickSpec { testContext = TestContext() testContext.config.startOnline = true - waitUntil { done in + waitUntil(timeout: 10) { done in + testContext.subject.flagChangeNotifier = ClientServiceFactory().makeFlagChangeNotifier() testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, completion: done) + testContext.subject.flagChangeNotifier.notifyObservers(user: testContext.user, oldFlags: testContext.oldFlags, oldFlagSource: testContext.oldFlagSource) } } it("takes the client and service objects online") { @@ -756,8 +760,10 @@ final class LDClientSpec: QuickSpec { beforeEach { testContext = TestContext(startOnline: true, runMode: .background, operatingSystem: os) - waitUntil { done in + waitUntil(timeout: 10) { done in + testContext.subject.flagChangeNotifier = ClientServiceFactory().makeFlagChangeNotifier() testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, completion: done) + testContext.subject.flagChangeNotifier.notifyObservers(user: testContext.user, oldFlags: testContext.oldFlags, oldFlagSource: testContext.oldFlagSource) } } it("takes the client and service objects online when background enabled") { @@ -806,7 +812,7 @@ final class LDClientSpec: QuickSpec { testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, runMode: .background, operatingSystem: os) testContext.config.enableBackgroundUpdates = false - waitUntil { done in + waitUntil(timeout: 10) { done in testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, completion: done) } } @@ -856,8 +862,10 @@ final class LDClientSpec: QuickSpec { beforeEach { testContext = TestContext() testContext.config.startOnline = true - waitUntil { done in + waitUntil(timeout: 10) { done in + testContext.subject.flagChangeNotifier = ClientServiceFactory().makeFlagChangeNotifier() testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, completion: done) + testContext.subject.flagChangeNotifier.notifyObservers(user: testContext.user, oldFlags: testContext.oldFlags, oldFlagSource: testContext.oldFlagSource) } testContext.featureFlagCachingMock.reset() @@ -900,7 +908,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.recordedEvent?.key) == newUser.key } it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 2 + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == newConfig } @@ -953,7 +961,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.recordedEvent?.key) == newUser.key } it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 2 + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == newConfig } @@ -997,7 +1005,7 @@ final class LDClientSpec: QuickSpec { expect(testContext.recordedEvent?.key) == testContext.user.key } it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 2 + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config } @@ -1050,7 +1058,7 @@ final class LDClientSpec: QuickSpec { testContext.flagStoreMock.featureFlags = [:] testContext.config.startOnline = false - waitUntil { done in + waitUntil(timeout: 10) { done in testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, completion: done) } } @@ -1074,7 +1082,7 @@ final class LDClientSpec: QuickSpec { testContext.flagStoreMock.featureFlags = [:] testContext.config.startOnline = false - waitUntil { done in + waitUntil(timeout: 10) { done in testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, completion: done) } } @@ -1095,6 +1103,497 @@ final class LDClientSpec: QuickSpec { } } + private func startAwaitingFlagsWithTimeoutSpec() { + describe("startAwaitingFlagsWithTimeout") { + var testContext: TestContext! + + context("when configured to start online") { + beforeEach { + testContext = TestContext() + testContext.config.startOnline = true + + waitUntil(timeout: 10) { done in + testContext.subject.flagChangeNotifier = ClientServiceFactory().makeFlagChangeNotifier() + testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, startWaitSeconds: 10) { timedOut in + expect(timedOut) == false + done() + } + testContext.subject.flagChangeNotifier.notifyObservers(user: testContext.user, oldFlags: testContext.oldFlags, oldFlagSource: testContext.oldFlagSource) + } + } + it("takes the client and service objects online") { + expect(testContext.subject.isOnline) == true + expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline + expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline + } + it("saves the config") { + expect(testContext.subject.config) == testContext.config + expect(testContext.subject.service.config) == testContext.config + expect(testContext.makeFlagSynchronizerStreamingMode) == testContext.config.streamingMode + expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) + expect(testContext.subject.eventReporter.config) == testContext.config + } + it("saves the user") { + expect(testContext.subject.user) == testContext.user + expect(testContext.subject.service.user) == testContext.user + expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) + if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { + expect(makeFlagSynchronizerReceivedParameters.service) === testContext.subject.service + } + expect(testContext.subject.eventReporter.service.user) == testContext.user + } + it("uncaches the new users flags") { + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 2 //called on both setConfig and setUser + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + } + it("records an identify event") { + expect(testContext.eventReporterMock.recordCallCount) == 1 + expect(testContext.recordedEvent?.kind) == .identify + expect(testContext.recordedEvent?.key) == testContext.user.key + } + it("converts cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + } + } + context("when configured to start online") { + beforeEach { + testContext = TestContext() + testContext.config.startOnline = true + + waitUntil(timeout: 10) { done in + testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, startWaitSeconds: 1) { timedOut in + expect(timedOut) == true + done() + } + } + } + it("times out properly") { + expect(testContext.subject.isOnline) == true + } + } + context("when configured to start offline") { + beforeEach { + testContext = TestContext() + waitUntil(timeout: 3) { done in + testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, startWaitSeconds: 2) { timedOut in + expect(timedOut) == true + done() + } + } + } + it("leaves the client and service objects offline") { + expect(testContext.subject.isOnline) == false + expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline + expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline + } + it("saves the config") { + expect(testContext.subject.config) == testContext.config + expect(testContext.subject.service.config) == testContext.config + expect(testContext.makeFlagSynchronizerStreamingMode) == testContext.config.streamingMode + expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) + expect(testContext.subject.eventReporter.config) == testContext.config + } + it("saves the user") { + expect(testContext.subject.user) == testContext.user + expect(testContext.subject.service.user) == testContext.user + expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) + if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { + expect(makeFlagSynchronizerReceivedParameters.service) === testContext.subject.service + } + expect(testContext.subject.eventReporter.service.user) == testContext.user + } + it("uncaches the new users flags") { + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 //because config is already set by TestConfig.init, only user.didSet calls this + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + } + it("records an identify event") { + expect(testContext.eventReporterMock.recordCallCount) == 1 + expect(testContext.recordedEvent?.kind) == .identify + expect(testContext.recordedEvent?.key) == testContext.user.key + } + it("converts cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + } + } + context("when configured to allow background updates and running in background mode") { + OperatingSystem.allOperatingSystems.forEach { (os) in + context("on \(os)") { + beforeEach { + testContext = TestContext(startOnline: true, runMode: .background, operatingSystem: os) + + waitUntil(timeout: 10) { done in + testContext.subject.flagChangeNotifier = ClientServiceFactory().makeFlagChangeNotifier() + testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, startWaitSeconds: 10) { timedOut in + expect(timedOut) == false + done() + } + testContext.subject.flagChangeNotifier.notifyObservers(user: testContext.user, oldFlags: testContext.oldFlags, oldFlagSource: testContext.oldFlagSource) + } + } + it("takes the client and service objects online when background enabled") { + expect(testContext.subject.isOnline) == os.isBackgroundEnabled + expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline + expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline + } + it("saves the config") { + expect(testContext.subject.config) == testContext.config + expect(testContext.subject.service.config) == testContext.config + expect(testContext.makeFlagSynchronizerStreamingMode) == os.backgroundStreamingMode + expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: .background) + expect(testContext.subject.eventReporter.config) == testContext.config + } + it("saves the user") { + expect(testContext.subject.user) == testContext.user + expect(testContext.subject.service.user) == testContext.user + expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) + if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { + expect(makeFlagSynchronizerReceivedParameters.service) === testContext.subject.service + } + expect(testContext.subject.eventReporter.service.user) == testContext.user + } + it("uncaches the new users flags") { + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 //because config is already set by TestConfig.init, only user.didSet calls this + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + } + it("records an identify event") { + expect(testContext.eventReporterMock.recordCallCount) == 1 + expect(testContext.recordedEvent?.kind) == .identify + expect(testContext.recordedEvent?.key) == testContext.user.key + } + it("converts cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + } + } + } + } + context("when configured to not allow background updates and running in background mode") { + OperatingSystem.allOperatingSystems.forEach { (os) in + context("on \(os)") { + beforeEach { + testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, runMode: .background, operatingSystem: os) + testContext.config.enableBackgroundUpdates = false + + waitUntil { done in + testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, startWaitSeconds: 10) { timedOut in + expect(timedOut) == false + done() + } + } + } + it("leaves the client and service objects offline") { + expect(testContext.subject.isOnline) == false + expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline + expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline + } + it("saves the config") { + expect(testContext.subject.config) == testContext.config + expect(testContext.subject.service.config) == testContext.config + expect(testContext.makeFlagSynchronizerStreamingMode) == LDStreamingMode.polling + expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: .background) + expect(testContext.subject.eventReporter.config) == testContext.config + } + it("saves the user") { + expect(testContext.subject.user) == testContext.user + expect(testContext.subject.service.user) == testContext.user + expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) + if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { + expect(makeFlagSynchronizerReceivedParameters.service.user) == testContext.user + } + expect(testContext.subject.eventReporter.service.user) == testContext.user + } + it("uncaches the new users flags") { + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 //because config is already set by TestConfig.init, only user.didSet calls this + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + } + it("records an identify event") { + expect(testContext.eventReporterMock.recordCallCount) == 1 + expect(testContext.recordedEvent?.kind) == .identify + expect(testContext.recordedEvent?.key) == testContext.user.key + } + it("converts cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + } + } + } + } + context("when called more than once") { + var newConfig: LDConfig! + var newUser: LDUser! + context("while online") { + beforeEach { + testContext = TestContext() + testContext.config.startOnline = true + waitUntil(timeout: 10) { done in + testContext.subject.flagChangeNotifier = ClientServiceFactory().makeFlagChangeNotifier() + testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, startWaitSeconds: 10) { timedOut in + expect(timedOut) == false + done() + } + testContext.subject.flagChangeNotifier.notifyObservers(user: testContext.user, oldFlags: testContext.oldFlags, oldFlagSource: testContext.oldFlagSource) + } + testContext.featureFlagCachingMock.reset() + + newConfig = testContext.subject.config.copyReplacingMobileKey(Constants.alternateMockMobileKey) + newConfig.baseUrl = Constants.alternateMockUrl + + newUser = LDUser.stub() + + testContext.subject.startCompleteWhenFlagsReceived(config: newConfig, user: newUser) + } + it("takes the client and service objects online") { + expect(testContext.subject.isOnline) == true + expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline + expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline + } + it("saves the config") { + expect(testContext.subject.config) == newConfig + expect(testContext.subject.service.config) == newConfig + expect(testContext.makeFlagSynchronizerStreamingMode) == newConfig.streamingMode + expect(testContext.makeFlagSynchronizerPollingInterval) == newConfig.flagPollingInterval(runMode: testContext.subject.runMode) + expect(testContext.subject.eventReporter.config) == newConfig + } + it("saves the user") { + expect(testContext.subject.user) == newUser + expect(testContext.subject.service.user) == newUser + expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) + if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { + expect(makeFlagSynchronizerReceivedParameters.service.user) == newUser + } + expect(testContext.subject.eventReporter.service.user) == newUser + } + it("uncaches the new users flags") { + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 2 //called on both setConfig and setUser + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == newUser.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == newConfig.mobileKey + } + it("records an identify event") { + expect(testContext.eventReporterMock.recordCallCount) == 2 + expect(testContext.recordedEvent?.kind) == .identify + expect(testContext.recordedEvent?.key) == newUser.key + } + it("converts cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == newConfig + } + } + context("while offline") { + beforeEach { + testContext = TestContext() + testContext.config.startOnline = false + waitUntil { done in + testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, startWaitSeconds: 10) { timedOut in + expect(timedOut) == true + done() + } + } + testContext.featureFlagCachingMock.reset() + + newConfig = testContext.subject.config.copyReplacingMobileKey(Constants.alternateMockMobileKey) + newConfig.baseUrl = Constants.alternateMockUrl + + newUser = LDUser.stub() + + testContext.subject.startCompleteWhenFlagsReceived(config: newConfig, user: newUser) + } + it("leaves the client and service objects offline") { + expect(testContext.subject.isOnline) == false + expect(testContext.subject.flagSynchronizer.isOnline) == testContext.subject.isOnline + expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline + } + it("saves the config") { + expect(testContext.subject.config) == newConfig + expect(testContext.subject.service.config) == newConfig + expect(testContext.makeFlagSynchronizerStreamingMode) == newConfig.streamingMode + expect(testContext.makeFlagSynchronizerPollingInterval) == newConfig.flagPollingInterval(runMode: testContext.subject.runMode) + expect(testContext.subject.eventReporter.config) == newConfig + } + it("saves the user") { + expect(testContext.subject.user) == newUser + expect(testContext.subject.service.user) == newUser + expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) + if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { + expect(makeFlagSynchronizerReceivedParameters.service.user) == newUser + } + expect(testContext.subject.eventReporter.service.user) == newUser + } + it("uncaches the new users flags") { + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 2 //called on both setConfig and setUser + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == newUser.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == newConfig.mobileKey + } + it("records an identify event") { + expect(testContext.eventReporterMock.recordCallCount) == 2 + expect(testContext.recordedEvent?.kind) == .identify + expect(testContext.recordedEvent?.key) == newUser.key + } + it("converts cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == newConfig + } + } + } + context("when called without user") { + context("after setting user") { + beforeEach { + testContext = TestContext() + testContext.subject.user = testContext.user + testContext.featureFlagCachingMock.reset() + + waitUntil { done in + testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, startWaitSeconds: 3) { timedOut in + expect(timedOut) == true + done() + } + } + } + it("saves the config") { + expect(testContext.subject.config) == testContext.config + expect(testContext.subject.service.config) == testContext.config + expect(testContext.makeFlagSynchronizerStreamingMode) == testContext.config.streamingMode + expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) + expect(testContext.subject.eventReporter.config) == testContext.config + } + it("saves the user") { + expect(testContext.subject.user) == testContext.user + expect(testContext.subject.service.user) == testContext.user + expect(testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters).toNot(beNil()) + if let makeFlagSynchronizerReceivedParameters = testContext.serviceFactoryMock.makeFlagSynchronizerReceivedParameters { + expect(makeFlagSynchronizerReceivedParameters.service.user) == testContext.user + } + expect(testContext.subject.eventReporter.service.user) == testContext.user + } + it("uncaches the new users flags") { + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + } + it("records an identify event") { + expect(testContext.eventReporterMock.recordCallCount) == 1 + expect(testContext.recordedEvent?.kind) == .identify + expect(testContext.recordedEvent?.key) == testContext.user.key + } + it("converts cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + } + } + context("without setting user") { + beforeEach { + testContext = TestContext() + waitUntil { done in + testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, startWaitSeconds: 3) { timedOut in + expect(timedOut) == true + done() + } + } + testContext.config = testContext.subject.config + testContext.user = testContext.subject.user + } + it("saves the config") { + expect(testContext.subject.config) == testContext.config + expect(testContext.subject.service.config) == testContext.config + expect(testContext.makeFlagSynchronizerStreamingMode) == testContext.config.streamingMode + expect(testContext.makeFlagSynchronizerPollingInterval) == testContext.config.flagPollingInterval(runMode: testContext.subject.runMode) + expect(testContext.subject.eventReporter.config) == testContext.config + } + it("saves the user") { + expect(testContext.subject.user) == testContext.user + expect(testContext.subject.service.user) == testContext.user + expect(testContext.makeFlagSynchronizerService?.user) == testContext.user + expect(testContext.subject.eventReporter.service.user) == testContext.user + } + it("uncaches the new users flags") { + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + } + it("records an identify event") { + expect(testContext.eventReporterMock.recordCallCount) == 1 + expect(testContext.recordedEvent?.kind) == .identify + expect(testContext.recordedEvent?.key) == testContext.user.key + } + it("converts cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + } + } + } + context("when called with cached flags for the user and environment") { + var retrievedFlags: [LDFlagKey: FeatureFlag]! + beforeEach { + testContext = TestContext() + testContext.featureFlagCachingMock.retrieveFeatureFlagsReturnValue = testContext.user.flagStore.featureFlags + retrievedFlags = testContext.user.flagStore.featureFlags + testContext.flagStoreMock.featureFlags = [:] + + testContext.config.startOnline = false + waitUntil { done in + testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, startWaitSeconds: 10) { timedOut in + expect(timedOut) == true + done() + } + } + } + it("checks the flag cache for the user and environment") { + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + } + it("restores user flags from cache") { + expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags?.flagCollection) == retrievedFlags + } + it("converts cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + } + } + context("when called without cached flags for the user") { + beforeEach { + testContext = TestContext() + testContext.flagStoreMock.featureFlags = [:] + + testContext.config.startOnline = false + waitUntil { done in + testContext.subject.startCompleteWhenFlagsReceived(config: testContext.config, user: testContext.user, startWaitSeconds: 10) { timedOut in + expect(timedOut) == true + done() + } + } + } + it("checks the flag cache for the user and environment") { + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + } + it("does not restore user flags from cache") { + expect(testContext.flagStoreMock.replaceStoreCallCount) == 0 + } + it("converts cached data") { + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config + } + } + } + } + private func setConfigSpec() { var testContext: TestContext! diff --git a/README.md b/README.md index 3b3206f0..ff6248fc 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ $ gem install cocoapods ```ruby use_frameworks! target 'YourTargetName' do - pod 'LaunchDarkly', '4.6.0' + pod 'LaunchDarkly', '4.7.0' end ``` @@ -70,7 +70,7 @@ $ brew install carthage To integrate LaunchDarkly into your Xcode project using Carthage, specify it in your `Cartfile`: ```ogdl -github "launchdarkly/ios-client-sdk" "4.6.0" +github "launchdarkly/ios-client-sdk" "4.7.0" ``` Run `carthage update` to build the framework. Optionally, specify the `--platform` to build only the frameworks that support your platform(s).