diff --git a/CHANGELOG.md b/CHANGELOG.md index db052359..3d320693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to the LaunchDarkly iOS SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [5.4.0] - 2021-02-26 +### Added +- Added the `alias` method to `LDClient`. This can be used to associate two user objects for analytics purposes with an alias event. +- Added the `autoAliasingOptOut` configuration option. This can be used to control the new automatic aliasing behavior of the `identify` method; by setting `autoAliasingOptOut` to true, `identify` will not automatically generate alias events. +- Added the `isInitialized` property to `LDClient`. Unless the client has been set offline, this property's value is `false` until the client receives an initial set of flag values from the LaunchDarkly service. If the client is offline, the value will be `true` after initialization. + +### Changed +- The `identify` method will now automatically generate an alias event when switching from an anonymous to a known user. This event associates the two users for analytics purposes as they most likely represent a single person. + +### Fixed +- Some users reported synchronization issues with the internal `DiagnosticReporter` implementation, which has been reworked to address these issues. Thanks to @provanandparanjape for one such report ([#238](https://github.com/launchdarkly/ios-client-sdk/issues/238)). + ## [5.3.2] - 2021-02-11 ### Fixed - Updated to prevent a crash in `dispatch_group_leave.cold.1` that would rarely occur as the SDK transitioned to an online state for a given configuration or user. This issue may have been exacerbated for a short period due to a temporary change in the behavior of the LaunchDarkly service streaming endpoint. Thanks to all the users who reported ([#235](https://github.com/launchdarkly/ios-client-sdk/issues/235)). diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index e2078244..00b48854 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -2,7 +2,7 @@ Pod::Spec.new do |ld| ld.name = "LaunchDarkly" - ld.version = "5.3.2" + ld.version = "5.4.0" ld.summary = "iOS SDK for LaunchDarkly" ld.description = <<-DESC diff --git a/LaunchDarkly.xcodeproj/project.pbxproj b/LaunchDarkly.xcodeproj/project.pbxproj index bad58e1b..137d26a6 100644 --- a/LaunchDarkly.xcodeproj/project.pbxproj +++ b/LaunchDarkly.xcodeproj/project.pbxproj @@ -188,7 +188,6 @@ 83B6E3F1222EFA3800FF2A6A /* ThreadSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B6E3F0222EFA3800FF2A6A /* ThreadSpec.swift */; }; 83B8C2451FE360CF0082B8A9 /* FlagChangeNotifierSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B8C2441FE360CF0082B8A9 /* FlagChangeNotifierSpec.swift */; }; 83B8C2471FE4071F0082B8A9 /* HTTPURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B8C2461FE4071F0082B8A9 /* HTTPURLResponse.swift */; }; - 83B8C2491FE84D4F0082B8A9 /* FlagChangeNotifyingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B8C2481FE84D4F0082B8A9 /* FlagChangeNotifyingMock.swift */; }; 83B9A080204F56F4000C3F17 /* FlagChangeObserverSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B9A07F204F56F4000C3F17 /* FlagChangeObserverSpec.swift */; }; 83B9A082204F6022000C3F17 /* FlagsUnchangedObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83B9A081204F6022000C3F17 /* FlagsUnchangedObserver.swift */; }; 83CFE7CE1F7AD81D0010544E /* EventReporterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */; }; @@ -268,6 +267,7 @@ B468E71124B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */; }; B468E71224B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */; }; B468E71324B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */; }; + B46F344125E6DB7D0078D45F /* DiagnosticReporterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B46F344025E6DB7D0078D45F /* DiagnosticReporterSpec.swift */; }; B4903D9824BD61B200F087C4 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B4903D9724BD61B200F087C4 /* OHHTTPStubsSwift */; }; B4903D9B24BD61D000F087C4 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = B4903D9A24BD61D000F087C4 /* Nimble */; }; B4903D9E24BD61EF00F087C4 /* Quick in Frameworks */ = {isa = PBXBuildFile; productRef = B4903D9D24BD61EF00F087C4 /* Quick */; }; @@ -445,7 +445,6 @@ 83B6E3F0222EFA3800FF2A6A /* ThreadSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadSpec.swift; sourceTree = ""; }; 83B8C2441FE360CF0082B8A9 /* FlagChangeNotifierSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagChangeNotifierSpec.swift; sourceTree = ""; }; 83B8C2461FE4071F0082B8A9 /* HTTPURLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLResponse.swift; sourceTree = ""; }; - 83B8C2481FE84D4F0082B8A9 /* FlagChangeNotifyingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagChangeNotifyingMock.swift; sourceTree = ""; }; 83B9A07F204F56F4000C3F17 /* FlagChangeObserverSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagChangeObserverSpec.swift; sourceTree = ""; }; 83B9A081204F6022000C3F17 /* FlagsUnchangedObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagsUnchangedObserver.swift; sourceTree = ""; }; 83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventReporterSpec.swift; sourceTree = ""; }; @@ -478,6 +477,7 @@ B40B419B249ADA6B00CD0726 /* DiagnosticCacheSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticCacheSpec.swift; sourceTree = ""; }; B4265EB024E7390C001CFD2C /* TestUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtil.swift; sourceTree = ""; }; B468E70F24B3C3AC00E0C883 /* ObjcLDEvaluationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcLDEvaluationDetail.swift; sourceTree = ""; }; + B46F344025E6DB7D0078D45F /* DiagnosticReporterSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticReporterSpec.swift; sourceTree = ""; }; B4C9D42D2489B5FF004A9B03 /* DiagnosticEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticEvent.swift; sourceTree = ""; }; B4C9D4322489C8FD004A9B03 /* DiagnosticCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticCache.swift; sourceTree = ""; }; B4C9D4372489E20A004A9B03 /* DiagnosticReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticReporter.swift; sourceTree = ""; }; @@ -560,15 +560,16 @@ 831D8B751F72A48900ED65E8 /* ServiceObjects */ = { isa = PBXGroup; children = ( - 831D8B761F72A4B400ED65E8 /* FlagSynchronizerSpec.swift */, + B46F344025E6DB7D0078D45F /* DiagnosticReporterSpec.swift */, + 837E38C821E804ED0008A50C /* EnvironmentReporterSpec.swift */, + 833631CA221B5DFA00BA53EE /* ErrorNotifierSpec.swift */, 83CFE7CD1F7AD81D0010544E /* EventReporterSpec.swift */, - 83DDBEFF1FA2589900E428B6 /* FlagStoreSpec.swift */, 83B8C2441FE360CF0082B8A9 /* FlagChangeNotifierSpec.swift */, + 83DDBEFF1FA2589900E428B6 /* FlagStoreSpec.swift */, + 831D8B761F72A4B400ED65E8 /* FlagSynchronizerSpec.swift */, 83383A5020460DD30024D975 /* SynchronizingErrorSpec.swift */, - 831AAE2F20A9E75D00B46DBA /* ThrottlerSpec.swift */, - 837E38C821E804ED0008A50C /* EnvironmentReporterSpec.swift */, 837406D321F760640087B22B /* LDTimerSpec.swift */, - 833631CA221B5DFA00BA53EE /* ErrorNotifierSpec.swift */, + 831AAE2F20A9E75D00B46DBA /* ThrottlerSpec.swift */, 8354AC75224316C700CDE602 /* Cache */, ); path = ServiceObjects; @@ -746,7 +747,6 @@ 83EF67941F994BAD00403126 /* LDUserStub.swift */, 838F96791FBA551A009CFC45 /* ClientServiceMockFactory.swift */, 8335299D1FC37727001166F8 /* FlagMaintainingMock.swift */, - 83B8C2481FE84D4F0082B8A9 /* FlagChangeNotifyingMock.swift */, 831425AE206ABB5300F2EF36 /* EnvironmentReportingMock.swift */, C48ED690242D27E200464F5F /* DeprecatedCacheMock.swift */, ); @@ -1436,7 +1436,6 @@ 83CFE7CE1F7AD81D0010544E /* EventReporterSpec.swift in Sources */, 833631CB221B5DFA00BA53EE /* ErrorNotifierSpec.swift in Sources */, 8392FFA32033565700320914 /* HTTPURLResponse.swift in Sources */, - 83B8C2491FE84D4F0082B8A9 /* FlagChangeNotifyingMock.swift in Sources */, 83411A5F1FABDA8700E5CF39 /* mocks.generated.swift in Sources */, 83D5597E1FDA01F9002D10C8 /* KeyedValueCacheSpec.swift in Sources */, 831CE0661F853A1700A13A3A /* Match.swift in Sources */, @@ -1457,6 +1456,7 @@ 83EBCBB720DABE93003A7142 /* FlagRequestTrackerSpec.swift in Sources */, 8354AC662241586100CDE602 /* CacheableEnvironmentFlagsSpec.swift in Sources */, B4265EB124E7390C001CFD2C /* TestUtil.swift in Sources */, + B46F344125E6DB7D0078D45F /* DiagnosticReporterSpec.swift in Sources */, 83D1523B22545BB20054B6D4 /* DeprecatedCacheModelV5Spec.swift in Sources */, 83EF67951F994BAD00403126 /* LDUserStub.swift in Sources */, B40B419C249ADA6B00CD0726 /* DiagnosticCacheSpec.swift in Sources */, @@ -1591,7 +1591,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.3.2; + MARKETING_VERSION = 5.4.0; MODULEMAP_FILE = ""; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-tvOS"; PRODUCT_NAME = LaunchDarkly_tvOS; @@ -1614,7 +1614,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.3.2; + MARKETING_VERSION = 5.4.0; MODULEMAP_FILE = ""; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-tvOS"; PRODUCT_NAME = LaunchDarkly_tvOS; @@ -1637,7 +1637,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.3.2; + MARKETING_VERSION = 5.4.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-macOS"; PRODUCT_NAME = LaunchDarkly_macOS; SDKROOT = macosx; @@ -1658,7 +1658,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.3.2; + MARKETING_VERSION = 5.4.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-macOS"; PRODUCT_NAME = LaunchDarkly_macOS; SDKROOT = macosx; @@ -1701,8 +1701,8 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; - DYLIB_COMPATIBILITY_VERSION = 5.3.0; - DYLIB_CURRENT_VERSION = 5.3.2; + DYLIB_COMPATIBILITY_VERSION = 5.4.0; + DYLIB_CURRENT_VERSION = 5.4.0; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; FRAMEWORK_VERSION = B; @@ -1772,8 +1772,8 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DYLIB_COMPATIBILITY_VERSION = 5.3.0; - DYLIB_CURRENT_VERSION = 5.3.2; + DYLIB_COMPATIBILITY_VERSION = 5.4.0; + DYLIB_CURRENT_VERSION = 5.4.0; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_VERSION = B; @@ -1812,7 +1812,7 @@ INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; - MARKETING_VERSION = 5.3.2; + MARKETING_VERSION = 5.4.0; MODULEMAP_FILE = "$(PROJECT_DIR)/Framework/module.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.Darkly; PRODUCT_NAME = LaunchDarkly; @@ -1832,7 +1832,7 @@ INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_DYLIB_INSTALL_NAME = "$(DYLIB_INSTALL_NAME_BASE:standardizepath)/$(EXECUTABLE_PATH)"; - MARKETING_VERSION = 5.3.2; + MARKETING_VERSION = 5.4.0; MODULEMAP_FILE = "$(PROJECT_DIR)/Framework/module.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.launchdarkly.Darkly; PRODUCT_NAME = LaunchDarkly; @@ -1874,7 +1874,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.3.2; + MARKETING_VERSION = 5.4.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-watchOS"; PRODUCT_NAME = LaunchDarkly_watchOS; SDKROOT = watchos; @@ -1896,7 +1896,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = "$(PROJECT_DIR)/LaunchDarkly/LaunchDarkly/Support/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - MARKETING_VERSION = 5.3.2; + MARKETING_VERSION = 5.4.0; PRODUCT_BUNDLE_IDENTIFIER = "com.launchdarkly.Darkly-watchOS"; PRODUCT_NAME = LaunchDarkly_watchOS; SDKROOT = watchos; diff --git a/LaunchDarkly/GeneratedCode/mocks.generated.swift b/LaunchDarkly/GeneratedCode/mocks.generated.swift index 9acfc7cc..0418379c 100644 --- a/LaunchDarkly/GeneratedCode/mocks.generated.swift +++ b/LaunchDarkly/GeneratedCode/mocks.generated.swift @@ -6,11 +6,11 @@ import Foundation import LDSwiftEventSource @testable import LaunchDarkly +// swiftlint:disable large_tuple // MARK: - CacheConvertingMock final class CacheConvertingMock: CacheConverting { - // MARK: convertCacheData var convertCacheDataCallCount = 0 var convertCacheDataCallback: (() -> Void)? var convertCacheDataReceivedArguments: (user: LDUser, config: LDConfig)? @@ -24,7 +24,6 @@ final class CacheConvertingMock: CacheConverting { // MARK: - DarklyStreamingProviderMock final class DarklyStreamingProviderMock: DarklyStreamingProvider { - // MARK: start var startCallCount = 0 var startCallback: (() -> Void)? func start() { @@ -32,7 +31,6 @@ final class DarklyStreamingProviderMock: DarklyStreamingProvider { startCallback?() } - // MARK: stop var stopCallCount = 0 var stopCallback: (() -> Void)? func stop() { @@ -44,7 +42,6 @@ final class DarklyStreamingProviderMock: DarklyStreamingProvider { // MARK: - DiagnosticCachingMock final class DiagnosticCachingMock: DiagnosticCaching { - // MARK: lastStats var lastStatsSetCount = 0 var setLastStatsCallback: (() -> Void)? var lastStats: DiagnosticStats? = nil { @@ -54,7 +51,6 @@ final class DiagnosticCachingMock: DiagnosticCaching { } } - // MARK: getDiagnosticId var getDiagnosticIdCallCount = 0 var getDiagnosticIdCallback: (() -> Void)? var getDiagnosticIdReturnValue: DiagnosticId! @@ -64,7 +60,6 @@ final class DiagnosticCachingMock: DiagnosticCaching { return getDiagnosticIdReturnValue } - // MARK: getCurrentStatsAndReset var getCurrentStatsAndResetCallCount = 0 var getCurrentStatsAndResetCallback: (() -> Void)? var getCurrentStatsAndResetReturnValue: DiagnosticStats! @@ -74,7 +69,6 @@ final class DiagnosticCachingMock: DiagnosticCaching { return getCurrentStatsAndResetReturnValue } - // MARK: incrementDroppedEventCount var incrementDroppedEventCountCallCount = 0 var incrementDroppedEventCountCallback: (() -> Void)? func incrementDroppedEventCount() { @@ -82,7 +76,6 @@ final class DiagnosticCachingMock: DiagnosticCaching { incrementDroppedEventCountCallback?() } - // MARK: recordEventsInLastBatch var recordEventsInLastBatchCallCount = 0 var recordEventsInLastBatchCallback: (() -> Void)? var recordEventsInLastBatchReceivedEventsInLastBatch: Int? @@ -92,7 +85,6 @@ final class DiagnosticCachingMock: DiagnosticCaching { recordEventsInLastBatchCallback?() } - // MARK: addStreamInit var addStreamInitCallCount = 0 var addStreamInitCallback: (() -> Void)? var addStreamInitReceivedStreamInit: DiagnosticStreamInit? @@ -106,41 +98,19 @@ final class DiagnosticCachingMock: DiagnosticCaching { // MARK: - DiagnosticReportingMock final class DiagnosticReportingMock: DiagnosticReporting { - // MARK: service - var serviceSetCount = 0 - var setServiceCallback: (() -> Void)? - var service: DarklyServiceProvider = DarklyServiceMock() { - didSet { - serviceSetCount += 1 - setServiceCallback?() - } - } - - // MARK: runMode - var runModeSetCount = 0 - var setRunModeCallback: (() -> Void)? - var runMode: LDClientRunMode = .foreground { - didSet { - runModeSetCount += 1 - setRunModeCallback?() - } - } - - // MARK: isOnline - var isOnlineSetCount = 0 - var setIsOnlineCallback: (() -> Void)? - var isOnline: Bool = false { - didSet { - isOnlineSetCount += 1 - setIsOnlineCallback?() - } + var setModeCallCount = 0 + var setModeCallback: (() -> Void)? + var setModeReceivedArguments: (runMode: LDClientRunMode, online: Bool)? + func setMode(_ runMode: LDClientRunMode, online: Bool) { + setModeCallCount += 1 + setModeReceivedArguments = (runMode: runMode, online: online) + setModeCallback?() } } // MARK: - EnvironmentReportingMock final class EnvironmentReportingMock: EnvironmentReporting { - // MARK: isDebugBuild var isDebugBuildSetCount = 0 var setIsDebugBuildCallback: (() -> Void)? var isDebugBuild: Bool = true { @@ -150,7 +120,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - // MARK: deviceType var deviceTypeSetCount = 0 var setDeviceTypeCallback: (() -> Void)? var deviceType: String = Constants.deviceType { @@ -160,7 +129,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - // MARK: deviceModel var deviceModelSetCount = 0 var setDeviceModelCallback: (() -> Void)? var deviceModel: String = Constants.deviceModel { @@ -170,7 +138,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - // MARK: systemVersion var systemVersionSetCount = 0 var setSystemVersionCallback: (() -> Void)? var systemVersion: String = Constants.systemVersion { @@ -180,7 +147,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - // MARK: systemName var systemNameSetCount = 0 var setSystemNameCallback: (() -> Void)? var systemName: String = Constants.systemName { @@ -190,7 +156,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - // MARK: operatingSystem var operatingSystemSetCount = 0 var setOperatingSystemCallback: (() -> Void)? var operatingSystem: OperatingSystem = .iOS { @@ -200,7 +165,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - // MARK: backgroundNotification var backgroundNotificationSetCount = 0 var setBackgroundNotificationCallback: (() -> Void)? var backgroundNotification: Notification.Name? = EnvironmentReporter().backgroundNotification { @@ -210,7 +174,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - // MARK: foregroundNotification var foregroundNotificationSetCount = 0 var setForegroundNotificationCallback: (() -> Void)? var foregroundNotification: Notification.Name? = EnvironmentReporter().foregroundNotification { @@ -220,7 +183,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - // MARK: vendorUUID var vendorUUIDSetCount = 0 var setVendorUUIDCallback: (() -> Void)? var vendorUUID: String? = Constants.vendorUUID { @@ -230,7 +192,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - // MARK: sdkVersion var sdkVersionSetCount = 0 var setSdkVersionCallback: (() -> Void)? var sdkVersion: String = Constants.sdkVersion { @@ -240,7 +201,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { } } - // MARK: shouldThrottleOnlineCalls var shouldThrottleOnlineCallsSetCount = 0 var setShouldThrottleOnlineCallsCallback: (() -> Void)? var shouldThrottleOnlineCalls: Bool = true { @@ -254,7 +214,6 @@ final class EnvironmentReportingMock: EnvironmentReporting { // MARK: - ErrorNotifyingMock final class ErrorNotifyingMock: ErrorNotifying { - // MARK: addErrorObserver var addErrorObserverCallCount = 0 var addErrorObserverCallback: (() -> Void)? var addErrorObserverReceivedObserver: ErrorObserver? @@ -264,7 +223,6 @@ final class ErrorNotifyingMock: ErrorNotifying { addErrorObserverCallback?() } - // MARK: removeObservers var removeObserversCallCount = 0 var removeObserversCallback: (() -> Void)? var removeObserversReceivedOwner: LDObserverOwner? @@ -274,7 +232,6 @@ final class ErrorNotifyingMock: ErrorNotifying { removeObserversCallback?() } - // MARK: notifyObservers var notifyObserversCallCount = 0 var notifyObserversCallback: (() -> Void)? var notifyObserversReceivedError: Error? @@ -288,7 +245,6 @@ final class ErrorNotifyingMock: ErrorNotifying { // MARK: - EventReportingMock final class EventReportingMock: EventReporting { - // MARK: config var configSetCount = 0 var setConfigCallback: (() -> Void)? var config: LDConfig = LDConfig.stub { @@ -298,7 +254,6 @@ final class EventReportingMock: EventReporting { } } - // MARK: isOnline var isOnlineSetCount = 0 var setIsOnlineCallback: (() -> Void)? var isOnline: Bool = false { @@ -308,7 +263,6 @@ final class EventReportingMock: EventReporting { } } - // MARK: service var serviceSetCount = 0 var setServiceCallback: (() -> Void)? var service: DarklyServiceProvider = DarklyServiceMock() { @@ -318,7 +272,6 @@ final class EventReportingMock: EventReporting { } } - // MARK: lastEventResponseDate var lastEventResponseDateSetCount = 0 var setLastEventResponseDateCallback: (() -> Void)? var lastEventResponseDate: Date? = nil { @@ -328,7 +281,6 @@ final class EventReportingMock: EventReporting { } } - // MARK: record var recordCallCount = 0 var recordCallback: (() -> Void)? var recordReceivedEvent: Event? @@ -338,10 +290,8 @@ final class EventReportingMock: EventReporting { recordCallback?() } - // MARK: recordFlagEvaluationEvents var recordFlagEvaluationEventsCallCount = 0 var recordFlagEvaluationEventsCallback: (() -> Void)? - //swiftlint:disable:next large_tuple var recordFlagEvaluationEventsReceivedArguments: (flagKey: LDFlagKey, value: Any?, defaultValue: Any?, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool)? func recordFlagEvaluationEvents(flagKey: LDFlagKey, value: Any?, defaultValue: Any?, featureFlag: FeatureFlag?, user: LDUser, includeReason: Bool) { recordFlagEvaluationEventsCallCount += 1 @@ -349,7 +299,6 @@ final class EventReportingMock: EventReporting { recordFlagEvaluationEventsCallback?() } - // MARK: flush var flushCallCount = 0 var flushCallback: (() -> Void)? var flushReceivedCompletion: CompletionClosure? @@ -363,7 +312,6 @@ final class EventReportingMock: EventReporting { // MARK: - FeatureFlagCachingMock final class FeatureFlagCachingMock: FeatureFlagCaching { - // MARK: maxCachedUsers var maxCachedUsersSetCount = 0 var setMaxCachedUsersCallback: (() -> Void)? var maxCachedUsers: Int = 5 { @@ -373,7 +321,6 @@ final class FeatureFlagCachingMock: FeatureFlagCaching { } } - // MARK: retrieveFeatureFlags var retrieveFeatureFlagsCallCount = 0 var retrieveFeatureFlagsCallback: (() -> Void)? var retrieveFeatureFlagsReceivedArguments: (userKey: String, mobileKey: String)? @@ -385,10 +332,8 @@ final class FeatureFlagCachingMock: FeatureFlagCaching { return retrieveFeatureFlagsReturnValue } - // MARK: storeFeatureFlags var storeFeatureFlagsCallCount = 0 var storeFeatureFlagsCallback: (() -> Void)? - //swiftlint:disable:next large_tuple var storeFeatureFlagsReceivedArguments: (featureFlags: [LDFlagKey: FeatureFlag], user: LDUser, mobileKey: String, lastUpdated: Date, storeMode: FlagCachingStoreMode)? func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], forUser user: LDUser, andMobileKey mobileKey: String, lastUpdated: Date, storeMode: FlagCachingStoreMode) { storeFeatureFlagsCallCount += 1 @@ -400,7 +345,6 @@ final class FeatureFlagCachingMock: FeatureFlagCaching { // MARK: - FlagChangeNotifyingMock final class FlagChangeNotifyingMock: FlagChangeNotifying { - // MARK: addFlagChangeObserver var addFlagChangeObserverCallCount = 0 var addFlagChangeObserverCallback: (() -> Void)? var addFlagChangeObserverReceivedObserver: FlagChangeObserver? @@ -410,7 +354,6 @@ final class FlagChangeNotifyingMock: FlagChangeNotifying { addFlagChangeObserverCallback?() } - // MARK: addFlagsUnchangedObserver var addFlagsUnchangedObserverCallCount = 0 var addFlagsUnchangedObserverCallback: (() -> Void)? var addFlagsUnchangedObserverReceivedObserver: FlagsUnchangedObserver? @@ -420,7 +363,6 @@ final class FlagChangeNotifyingMock: FlagChangeNotifying { addFlagsUnchangedObserverCallback?() } - // MARK: addConnectionModeChangedObserver var addConnectionModeChangedObserverCallCount = 0 var addConnectionModeChangedObserverCallback: (() -> Void)? var addConnectionModeChangedObserverReceivedObserver: ConnectionModeChangedObserver? @@ -430,17 +372,15 @@ final class FlagChangeNotifyingMock: FlagChangeNotifying { addConnectionModeChangedObserverCallback?() } - // MARK: removeObserver var removeObserverCallCount = 0 var removeObserverCallback: (() -> Void)? - var removeObserverReceivedArguments: (keys: [LDFlagKey], owner: LDObserverOwner)? - func removeObserver(_ keys: [LDFlagKey], owner: LDObserverOwner) { + var removeObserverReceivedOwner: LDObserverOwner? + func removeObserver(owner: LDObserverOwner) { removeObserverCallCount += 1 - removeObserverReceivedArguments = (keys: keys, owner: owner) + removeObserverReceivedOwner = owner removeObserverCallback?() } - // MARK: notifyConnectionModeChangedObservers var notifyConnectionModeChangedObserversCallCount = 0 var notifyConnectionModeChangedObserversCallback: (() -> Void)? var notifyConnectionModeChangedObserversReceivedConnectionMode: ConnectionInformation.ConnectionMode? @@ -450,7 +390,6 @@ final class FlagChangeNotifyingMock: FlagChangeNotifying { notifyConnectionModeChangedObserversCallback?() } - // MARK: notifyObservers var notifyObserversCallCount = 0 var notifyObserversCallback: (() -> Void)? var notifyObserversReceivedArguments: (flagStore: FlagMaintaining, oldFlags: [LDFlagKey: FeatureFlag])? @@ -461,54 +400,9 @@ final class FlagChangeNotifyingMock: FlagChangeNotifying { } } -// MARK: - FlagMaintainingMock -final class FlagMaintainingMock: FlagMaintaining { - - // MARK: featureFlags - var featureFlagsSetCount = 0 - var setFeatureFlagsCallback: (() -> Void)? - var featureFlags: [LDFlagKey: FeatureFlag] = [:] { - didSet { - featureFlagsSetCount += 1 - setFeatureFlagsCallback?() - } - } - - // MARK: replaceStore - var replaceStoreCallCount = 0 - var replaceStoreCallback: (() -> Void)? - var replaceStoreReceivedArguments: (newFlags: [LDFlagKey: Any]?, completion: CompletionClosure?)? - func replaceStore(newFlags: [LDFlagKey: Any]?, completion: CompletionClosure?) { - replaceStoreCallCount += 1 - replaceStoreReceivedArguments = (newFlags: newFlags, completion: completion) - replaceStoreCallback?() - } - - // MARK: updateStore - var updateStoreCallCount = 0 - var updateStoreCallback: (() -> Void)? - var updateStoreReceivedArguments: (updateDictionary: [String: Any], completion: CompletionClosure?)? - func updateStore(updateDictionary: [String: Any], completion: CompletionClosure?) { - updateStoreCallCount += 1 - updateStoreReceivedArguments = (updateDictionary: updateDictionary, completion: completion) - updateStoreCallback?() - } - - // MARK: deleteFlag - var deleteFlagCallCount = 0 - var deleteFlagCallback: (() -> Void)? - var deleteFlagReceivedArguments: (deleteDictionary: [String: Any], completion: CompletionClosure?)? - func deleteFlag(deleteDictionary: [String: Any], completion: CompletionClosure?) { - deleteFlagCallCount += 1 - deleteFlagReceivedArguments = (deleteDictionary: deleteDictionary, completion: completion) - deleteFlagCallback?() - } -} - // MARK: - KeyedValueCachingMock final class KeyedValueCachingMock: KeyedValueCaching { - // MARK: set var setCallCount = 0 var setCallback: (() -> Void)? var setReceivedArguments: (value: Any?, forKey: String)? @@ -518,7 +412,6 @@ final class KeyedValueCachingMock: KeyedValueCaching { setCallback?() } - // MARK: dictionary var dictionaryCallCount = 0 var dictionaryCallback: (() -> Void)? var dictionaryReceivedForKey: String? @@ -530,7 +423,6 @@ final class KeyedValueCachingMock: KeyedValueCaching { return dictionaryReturnValue } - // MARK: removeObject var removeObjectCallCount = 0 var removeObjectCallback: (() -> Void)? var removeObjectReceivedForKey: String? @@ -544,7 +436,6 @@ final class KeyedValueCachingMock: KeyedValueCaching { // MARK: - LDFlagSynchronizingMock final class LDFlagSynchronizingMock: LDFlagSynchronizing { - // MARK: isOnline var isOnlineSetCount = 0 var setIsOnlineCallback: (() -> Void)? var isOnline: Bool = false { @@ -554,7 +445,6 @@ final class LDFlagSynchronizingMock: LDFlagSynchronizing { } } - // MARK: streamingMode var streamingModeSetCount = 0 var setStreamingModeCallback: (() -> Void)? var streamingMode: LDStreamingMode = .streaming { @@ -564,7 +454,6 @@ final class LDFlagSynchronizingMock: LDFlagSynchronizing { } } - // MARK: pollingInterval var pollingIntervalSetCount = 0 var setPollingIntervalCallback: (() -> Void)? var pollingInterval: TimeInterval = 60_000 { @@ -578,7 +467,6 @@ final class LDFlagSynchronizingMock: LDFlagSynchronizing { // MARK: - ThrottlingMock final class ThrottlingMock: Throttling { - // MARK: runThrottled var runThrottledCallCount = 0 var runThrottledCallback: (() -> Void)? var runThrottledReceivedRunClosure: RunClosure? @@ -588,7 +476,6 @@ final class ThrottlingMock: Throttling { runThrottledCallback?() } - // MARK: cancelThrottledRun var cancelThrottledRunCallCount = 0 var cancelThrottledRunCallback: (() -> Void)? func cancelThrottledRun() { diff --git a/LaunchDarkly/LaunchDarkly/LDClient.swift b/LaunchDarkly/LaunchDarkly/LDClient.swift index 61e4a248..27ec89eb 100644 --- a/LaunchDarkly/LaunchDarkly/LDClient.swift +++ b/LaunchDarkly/LaunchDarkly/LDClient.swift @@ -12,7 +12,6 @@ enum LDClientRunMode { } // swiftlint:disable type_body_length -// swiftlint:disable file_length /** The LDClient is the heart of the SDK, providing client apps running iOS, watchOS, macOS, or tvOS access to LaunchDarkly services. This singleton provides the ability to set a configuration (LDConfig) that controls how the LDClient talks to LaunchDarkly servers, and a user (LDUser) that provides finer control on the feature flag values delivered to LDClient. Once the LDClient has started, it connects to LaunchDarkly's servers to get the feature flag values you set in the Dashboard. @@ -72,7 +71,7 @@ public class LDClient { _isOnline = newValue flagSynchronizer.isOnline = _isOnline eventReporter.isOnline = _isOnline - diagnosticReporter.isOnline = _isOnline + diagnosticReporter.setMode(runMode, online: _isOnline) if _isOnline != oldValue { connectionInformation = ConnectionInformation.onlineSetCheck(connectionInformation: connectionInformation, ldClient: self, config: config, online: _isOnline) } @@ -83,6 +82,17 @@ public class LDClient { private var _isOnline = false private var isOnlineQueue = DispatchQueue(label: "com.launchdarkly.LDClient.isOnlineQueue") + /** + Reports the initialization state of the LDClient. + + When true, the SDK has either communicated with LaunchDarkly servers for feature flag values or the SDK has been set offline. + + When false, the SDK has not been able to communicate with LaunchDarkly servers. Client apps can request feature flag values and set/change feature flag observers but flags might not exist or be stale. + */ + public var isInitialized: Bool { + hasStarted && (!isOnline || initialized) + } + /** Set the LDClient online/offline. @@ -135,6 +145,9 @@ public class LDClient { var completed = false let internalCompletedQueue = DispatchQueue(label: "com.launchdarkly.LDClient.goCompletedQueue") + if !goOnline { + initialized = true + } let completionCheck = { (completion: (() -> Void)?) in internalCompletedQueue.sync { if completed == false { @@ -148,10 +161,12 @@ public class LDClient { completion?() } else if completion != nil { observeAll(owner: owner) { _ in + self.initialized = true completionCheck(completion) self.stopObserving(owner: owner) } observeFlagsUnchanged(owner: owner) { + self.initialized = true completionCheck(completion) self.stopObserving(owner: owner) } @@ -203,7 +218,7 @@ public class LDClient { service: service, onSyncComplete: onFlagSyncComplete) flagSynchronizer.isOnline = willSetSynchronizerOnline - diagnosticReporter.runMode = runMode + diagnosticReporter.setMode(runMode, online: _isOnline) } } @@ -281,6 +296,7 @@ public class LDClient { func internalIdentify(newUser: LDUser, completion: (() -> Void)? = nil) { internalIdentifyQueue.sync { + let previousUser = self.user self.user = newUser Log.debug(self.typeName(and: #function) + "new user set with key: " + self.user.key ) let wasOnline = self.isOnline @@ -296,40 +312,33 @@ public class LDClient { flagStore.replaceStore(newFlags: [:], completion: nil) } } - self.service = self.serviceFactory.makeDarklyServiceProvider(config: self.config, user: self.user) - self.service.clearFlagResponseCache() + self.service.user = self.user + flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self), + pollingInterval: config.flagPollingInterval(runMode: runMode), + useReport: config.useReport, + service: self.service, + onSyncComplete: self.onFlagSyncComplete) if self.hasStarted { self.eventReporter.record(Event.identifyEvent(user: self.user)) } self.internalSetOnline(wasOnline, completion: completion) + + if !config.autoAliasingOptOut && previousUser.isAnonymous && !newUser.isAnonymous { + self.internalAlias(context: newUser, previousContext: previousUser) + } + + self.service.clearFlagResponseCache() } } private let internalIdentifyQueue: DispatchQueue = DispatchQueue(label: "InternalIdentifyQueue") - private(set) var service: DarklyServiceProvider { - didSet { - Log.debug(typeName(and: #function) + "new service set") - eventReporter.service = service - diagnosticReporter.service = service - flagSynchronizer = serviceFactory.makeFlagSynchronizer(streamingMode: ConnectionInformation.effectiveStreamingMode(config: config, ldClient: self), - pollingInterval: config.flagPollingInterval(runMode: runMode), - useReport: config.useReport, - service: service, - onSyncComplete: onFlagSyncComplete) - } - } + let service: DarklyServiceProvider // MARK: Retrieving Flag Values - - /* FF Value Requests - Conceptual Model - The LDClient is the focal point for flag value requests. It should appear to the app that the client contains a store of [key: value] pairs where the keys are all strings and the values any of the supported LD flag types (Bool, Int, Double, String, Array, Dictionary). - When asked for a variation value, the LDClient provides either the value, or the value along with an explanation. - */ - + /** Returns the variation for the given feature flag. If the flag does not exist, cannot be cast to the correct return type, or the LDClient is not started, returns the default value. Use this method when the default value is a non-Optional type. See `variation` with the Optional return value when the default value can be nil. @@ -509,17 +518,7 @@ public class LDClient { } // MARK: Observing Updates - - /* FF Change Notification - Conceptual Model - LDClient keeps a list of two types of closure observers, either Flag Change Observers or Flags Unchanged Observers. - There are 3 types of Flag Change Observers, Individual Flag Change Observers, Flag Collection Change Observers, and All Flags Change Observers. LDClient executes Individual Flag observers when it detects a change to a single flag being observed. LDClient executes Flag Collection Change Observers one time when it detects a change to any flag in the observed flag collection. LDClient executes All Flags observers one time when it detects a change to any flag. The Individual Flag Change Observer has closure that takes a LDChangedFlag input parameter which communicates the flag's old & new value. Flag Collection and All Flags Observers will have a closure that takes a dictionary of [LDFlagKey: LDChangeFlag] that communicates all of the changed flags. - An app registers an Individual Flag observer using observe(key:, owner:, handler:). An app registers a Flag Collection Observer using observe(keys: owner: handler), An app registers an All Flags observer using observeAll(owner:, handler:). An app can register multiple closures for each type by calling these methods multiple times. When the value of a flag changes, LDClient calls each registered closure 1 time. - Flags Unchanged Observers allow the LDClient to communicate to the app when it receives flags from the LD server that doesn't change any values from what the LDClient had already. For example, at launch the LDClient restores cached flag values before requesting flags from the LD server. If there has been no change to the flag values, the LDClient will execute the Flags Unchanged Observers that the app has registered. An app registers a Flags Unchanged Observer using observeFlagsUnchanged(owner: handler:). - LDClient will automatically remove observers when the owner is nil. This means an app does not need to stop observing flags, the LDClient will remove the observer after it has gone out of scope. An app can stop observers explicitly using stopObserver(owner:). - LDClient executes observers on the main thread. - */ - + /** Sets a handler for the specified flag key executed on the specified owner. If the flag's value changes, executes the handler, passing in the `changedFlag` containing the old and new flag values. See `LDChangedFlag` for details. @@ -722,11 +721,6 @@ public class LDClient { // MARK: Events - /* Event tracking - Conceptual model - The LDClient keeps an event store that it transmits periodically to LD. An app sends an event and optional data by calling track(key: data:) supplying at least the key. - */ - /** Adds a custom event to the LDClient event store. A client app can set a tracking event to allow client customized data analysis. Once an app has called `track`, the app cannot remove the event from the event store. @@ -757,6 +751,33 @@ public class LDClient { eventReporter.record(event) } + /** + Tells the SDK to generate an alias event. + + Associates two users for analytics purposes. + + This can be helpful in the situation where a person is represented by multiple + LaunchDarkly users. This may happen, for example, when a person initially logs into + an application-- the person might be represented by an anonymous user prior to logging + in and a different user after logging in, as denoted by a different user key. + + - parameter context: the user that will be aliased to + - parameter previousContext: the user that will be bound to the new context + */ + public func alias(context new: LDUser, previousContext old: LDUser) { + guard hasStarted + else { + Log.debug(typeName(and: #function) + "aborted. LDClient not started") + return + } + + internalAlias(context: new, previousContext: old) + } + + private func internalAlias(context new: LDUser, previousContext old: LDUser) { + self.eventReporter.record(Event.aliasEvent(newUser: new, oldUser: old)) + } + /** Tells the SDK to immediately send any currently queued events to LaunchDarkly. @@ -801,19 +822,24 @@ public class LDClient { */ /// - Tag: start public static func start(config: LDConfig, user: LDUser? = nil, completion: (() -> Void)? = nil) { + start(serviceFactory: nil, config: config, user: user, completion: completion) + } + + static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, user: LDUser? = nil, completion: (() -> Void)? = nil) { Log.debug("LDClient starting") + if serviceFactory != nil { + get()?.close() + } if instances != nil { Log.debug("LDClient.start() was called more than once!") return } HTTPHeaders.removeFlagRequestEtags() - - let anonymousUser = LDUser(environmentReporter: EnvironmentReporter()) - let internalUser = user ?? anonymousUser - + + let internalUser = user + LDClient.instances = [:] - let cache = UserEnvironmentFlagCache(withKeyedValueCache: ClientServiceFactory().makeKeyedValueCache(), maxCachedUsers: config.maxCachedUsers) var mobileKeys = config.getSecondaryMobileKeys() var internalCount = 0 let completionCheck = { @@ -827,8 +853,7 @@ public class LDClient { for (name, mobileKey) in mobileKeys { var internalConfig = config internalConfig.mobileKey = mobileKey - let flagChangeNotifier = FlagChangeNotifier() - let instance = LDClient(configuration: internalConfig, startUser: internalUser, newCache: cache, flagNotifier: flagChangeNotifier, completion: completionCheck) + let instance = LDClient(serviceFactory: serviceFactory ?? ClientServiceFactory(), configuration: internalConfig, startUser: internalUser, completion: completionCheck) LDClient.instances?[name] = instance } completionCheck() @@ -843,14 +868,18 @@ public class LDClient { - parameter completion: Closure called when the embedded `setOnline` call completes. Takes a Bool that indicates whether the completion timedout as a parameter. (Optional) */ public static func start(config: LDConfig, user: LDUser? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { + start(serviceFactory: nil, config: config, user: user, startWaitSeconds: startWaitSeconds, completion: completion) + } + + static func start(serviceFactory: ClientServiceCreating?, config: LDConfig, user: LDUser? = nil, startWaitSeconds: TimeInterval, completion: ((_ timedOut: Bool) -> Void)? = nil) { var completed = true let internalCompletedQueue: DispatchQueue = DispatchQueue(label: "TimeOutQueue") if !config.startOnline { - start(config: config, user: user) + start(serviceFactory: serviceFactory, config: config, user: user) completion?(completed) } else { let startTime = Date().timeIntervalSince1970 - start(config: config, user: user) { + start(serviceFactory: serviceFactory, config: config, user: user) { internalCompletedQueue.async { if startTime + startWaitSeconds > Date().timeIntervalSince1970 && completed { completed = false @@ -884,7 +913,7 @@ public class LDClient { } // MARK: - Private - private(set) var serviceFactory: ClientServiceCreating = ClientServiceFactory() + let serviceFactory: ClientServiceCreating private(set) var flagCache: FeatureFlagCaching private(set) var cacheConverter: CacheConverting @@ -902,27 +931,31 @@ public class LDClient { } private var _hasStarted = true private var hasStartedQueue = DispatchQueue(label: "com.launchdarkly.LDClient.hasStartedQueue") + private(set) var initialized: Bool { + get { initializedQueue.sync { _initialized } } + set { initializedQueue.sync { _initialized = newValue } } + } + private var _initialized = false + private var initializedQueue = DispatchQueue(label: "com.launchdarkly.LDClient.initializedQueue") - private init(serviceFactory: ClientServiceCreating? = nil, configuration: LDConfig, startUser: LDUser?, newCache: FeatureFlagCaching, flagNotifier: FlagChangeNotifying, testing: Bool = false, completion: (() -> Void)? = nil) { - if let serviceFactory = serviceFactory { - self.serviceFactory = serviceFactory - } + private init(serviceFactory: ClientServiceCreating, configuration: LDConfig, startUser: LDUser?, completion: (() -> Void)? = nil) { + self.serviceFactory = serviceFactory environmentReporter = self.serviceFactory.makeEnvironmentReporter() - flagCache = newCache + flagCache = self.serviceFactory.makeFeatureFlagCache(maxCachedUsers: configuration.maxCachedUsers) flagStore = self.serviceFactory.makeFlagStore() if let userFlagStore = startUser?.flagStore { flagStore.replaceStore(newFlags: userFlagStore.featureFlags, completion: nil) } LDUserWrapper.configureKeyedArchiversToHandleVersion2_3_0AndOlderUserCacheFormat() cacheConverter = self.serviceFactory.makeCacheConverter(maxCachedUsers: configuration.maxCachedUsers) - flagChangeNotifier = flagNotifier + flagChangeNotifier = self.serviceFactory.makeFlagChangeNotifier() throttler = self.serviceFactory.makeThrottler(maxDelay: Throttler.Constants.defaultDelay, environmentReporter: environmentReporter) config = configuration let anonymousUser = LDUser(environmentReporter: environmentReporter) user = startUser ?? anonymousUser service = self.serviceFactory.makeDarklyServiceProvider(config: config, user: user) - diagnosticReporter = self.serviceFactory.makeDiagnosticReporter(service: service, runMode: runMode) + diagnosticReporter = self.serviceFactory.makeDiagnosticReporter(service: service) eventReporter = self.serviceFactory.makeEventReporter(config: config, service: service) errorNotifier = self.serviceFactory.makeErrorNotifier() connectionInformation = self.serviceFactory.makeConnectionInformation() @@ -976,66 +1009,9 @@ private extension Optional { } #if DEBUG - extension LDClient { - static func start(serviceFactory: ClientServiceCreating, config: LDConfig, user: LDUser? = nil, flagCache: FeatureFlagCaching, flagNotifier: FlagChangeNotifier, completion: (() -> Void)? = nil) { - Log.debug("LDClient starting for tests") - get()?.close() - - let anonymousUser = LDUser(environmentReporter: EnvironmentReporter()) - let internalUser = user ?? anonymousUser - - LDClient.instances = [:] - var mobileKeys = config.getSecondaryMobileKeys() - var internalCount = 0 - let completionCheck = { - internalCount += 1 - if internalCount > mobileKeys.count { - Log.debug("All LDClients finished starting for tests") - completion?() - } - } - mobileKeys[LDConfig.Constants.primaryEnvironmentName] = config.mobileKey - for (name, mobileKey) in mobileKeys { - var internalConfig = config - internalConfig.mobileKey = mobileKey - let instance = LDClient(serviceFactory: serviceFactory, configuration: internalConfig, startUser: internalUser, newCache: flagCache, flagNotifier: flagNotifier, completion: completionCheck) - LDClient.instances?[name] = instance - } - completionCheck() - } - - static func start(serviceFactory: ClientServiceCreating, config: LDConfig, user: LDUser? = nil, startWaitSeconds: TimeInterval, flagCache: FeatureFlagCaching, flagNotifier: FlagChangeNotifier, completion: ((_ timedOut: Bool) -> Void)? = nil) { - var completed = true - let internalCompletedQueue: DispatchQueue = DispatchQueue(label: "TimeOutQueue") - if !config.startOnline { - start(serviceFactory: serviceFactory, config: config, user: user, flagCache: flagCache, flagNotifier: flagNotifier) - completion?(completed) - } else { - let startTime = Date().timeIntervalSince1970 - start(serviceFactory: serviceFactory, config: config, user: user, flagCache: flagCache, flagNotifier: flagNotifier) { - internalCompletedQueue.async { - if startTime + startWaitSeconds > Date().timeIntervalSince1970 && completed { - completed = false - completion?(completed) - } - } - } - DispatchQueue.global().asyncAfter(deadline: .now() + startWaitSeconds) { - internalCompletedQueue.async { - if completed { - completion?(completed) - } - } - } - } - } - - func setRunMode(_ runMode: LDClientRunMode) { - self.runMode = runMode - } - - func setService(_ service: DarklyServiceProvider) { - self.service = service - } +extension LDClient { + func setRunMode(_ runMode: LDClientRunMode) { + self.runMode = runMode } +} #endif diff --git a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift index 4f2e0b35..5023c7d0 100644 --- a/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift +++ b/LaunchDarkly/LaunchDarkly/Models/DiagnosticEvent.swift @@ -98,6 +98,7 @@ struct DiagnosticSdk: Encodable { } struct DiagnosticConfig: Codable { + let autoAliasingOptOut: Bool let customBaseURI: Bool let customEventsURI: Bool let customStreamURI: Bool @@ -118,6 +119,7 @@ struct DiagnosticConfig: Codable { let customHeaders: Bool init(config: LDConfig) { + autoAliasingOptOut = config.autoAliasingOptOut customBaseURI = config.baseUrl != LDConfig.Defaults.baseUrl customEventsURI = config.eventsUrl != LDConfig.Defaults.eventsUrl customStreamURI = config.streamUrl != LDConfig.Defaults.streamUrl diff --git a/LaunchDarkly/LaunchDarkly/Models/Event.swift b/LaunchDarkly/LaunchDarkly/Models/Event.swift index 8de4722d..7ce7ff83 100644 --- a/LaunchDarkly/LaunchDarkly/Models/Event.swift +++ b/LaunchDarkly/LaunchDarkly/Models/Event.swift @@ -7,33 +7,42 @@ import Foundation +func userType(_ user: LDUser) -> String { + return user.isAnonymous ? "anonymousUser" : "user" +} + struct Event { enum CodingKeys: String, CodingKey { - case key, kind, creationDate, user, userKey, value, defaultValue = "default", variation, version, data, endDate, reason, metricValue + case key, previousKey, kind, creationDate, user, userKey, + value, defaultValue = "default", variation, version, + data, endDate, reason, metricValue, + // for aliasing + contextKind, previousContextKind } enum Kind: String { - case feature, debug, identify, custom, summary + case feature, debug, identify, custom, summary, alias static var allKinds: [Kind] { - [feature, debug, identify, custom, summary] - } - static var alwaysInlineUserKinds: [Kind] { - [identify, debug] + [feature, debug, identify, custom, summary, alias] } + var isAlwaysInlineUserKind: Bool { - Kind.alwaysInlineUserKinds.contains(self) - } - static var alwaysIncludeValueKinds: [Kind] { - [feature, debug] + [.identify, .debug].contains(self) } + var isAlwaysIncludeValueKinds: Bool { - Kind.alwaysIncludeValueKinds.contains(self) + [.feature, .debug].contains(self) + } + + var needsContextKind: Bool { + [.feature, .custom].contains(self) } } let kind: Kind let key: String? + let previousKey: String? let creationDate: Date? let user: LDUser? let value: Any? @@ -44,9 +53,14 @@ struct Event { let endDate: Date? let includeReason: Bool let metricValue: Double? + let contextKind: String? + let previousContextKind: String? init(kind: Kind = .custom, key: String? = nil, + previousKey: String? = nil, + contextKind: String? = nil, + previousContextKind: String? = nil, user: LDUser? = nil, value: Any? = nil, defaultValue: Any? = nil, @@ -58,6 +72,7 @@ struct Event { metricValue: Double? = nil) { self.kind = kind self.key = key + self.previousKey = previousKey self.creationDate = kind == .summary ? nil : Date() self.user = user self.value = value @@ -68,6 +83,8 @@ struct Event { self.endDate = endDate self.includeReason = includeReason self.metricValue = metricValue + self.contextKind = contextKind + self.previousContextKind = previousContextKind } // swiftlint:disable:next function_parameter_count @@ -107,10 +124,16 @@ struct Event { return Event(kind: .summary, flagRequestTracker: flagRequestTracker, endDate: endDate) } + static func aliasEvent(newUser new: LDUser, oldUser old: LDUser) -> Event { + Log.debug("\(typeName(and: #function)) key: \(new.key), previousKey: \(old.key)") + return Event(kind: .alias, key: new.key, previousKey: old.key, contextKind: userType(new), previousContextKind: userType(old)) + } + func dictionaryValue(config: LDConfig) -> [String: Any] { var eventDictionary = [String: Any]() eventDictionary[CodingKeys.kind.rawValue] = kind.rawValue eventDictionary[CodingKeys.key.rawValue] = key + eventDictionary[CodingKeys.previousKey.rawValue] = previousKey eventDictionary[CodingKeys.creationDate.rawValue] = creationDate?.millisSince1970 if kind.isAlwaysInlineUserKind || config.inlineUserInEvents { eventDictionary[CodingKeys.user.rawValue] = user?.dictionaryValue(includePrivateAttributes: false, config: config) @@ -134,6 +157,15 @@ struct Event { eventDictionary[CodingKeys.reason.rawValue] = includeReason || featureFlag?.trackReason ?? false ? featureFlag?.reason : nil eventDictionary[CodingKeys.metricValue.rawValue] = metricValue + if kind.needsContextKind && (user?.isAnonymous == true) { + eventDictionary[CodingKeys.contextKind.rawValue] = "anonymousUser" + } + + if kind == .alias { + eventDictionary[CodingKeys.contextKind.rawValue] = self.contextKind + eventDictionary[CodingKeys.previousContextKind.rawValue] = self.previousContextKind + } + return eventDictionary } } diff --git a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/FlagsUnchangedObserver.swift b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/FlagsUnchangedObserver.swift index 30c6ef42..f7806264 100644 --- a/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/FlagsUnchangedObserver.swift +++ b/LaunchDarkly/LaunchDarkly/Models/FeatureFlag/FlagChange/FlagsUnchangedObserver.swift @@ -9,7 +9,7 @@ import Foundation struct FlagsUnchangedObserver { private(set) weak var owner: LDObserverOwner? - let flagsUnchangedHandler: LDFlagsUnchangedHandler? + let flagsUnchangedHandler: LDFlagsUnchangedHandler init(owner: LDObserverOwner, flagsUnchangedHandler: @escaping LDFlagsUnchangedHandler) { self.owner = owner diff --git a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift index 3b748082..d86bf94c 100644 --- a/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift +++ b/LaunchDarkly/LaunchDarkly/Models/LDConfig.swift @@ -105,6 +105,9 @@ public struct LDConfig { /// a closure to allow dynamic changes of headers on connect & reconnect static let headerDelegate: RequestHeaderTransform? = nil + + /// should anonymous users automatically be aliased when identifying + static let autoAliasingOptOut: Bool = false } /// Constants relevant to setting up an `LDConfig` @@ -254,6 +257,7 @@ public struct LDConfig { /// Additional headers that should be added to all HTTP requests from SDK components to LaunchDarkly services public var additionalHeaders: [String: String] = [:] + /* TODO: find a way to make delegates equatable */ /// a closure to allow dynamic changes of headers on connect & reconnect public var headerDelegate: RequestHeaderTransform? @@ -265,6 +269,9 @@ public struct LDConfig { let environmentReporter: EnvironmentReporting + /// should anonymous users automatically be aliased when identifying + public var autoAliasingOptOut: Bool = Defaults.autoAliasingOptOut + /// A Dictionary of identifying names to unique mobile keys for all environments private var mobileKeys: [String: String] { var internalMobileKeys = getSecondaryMobileKeys() @@ -367,6 +374,7 @@ extension LDConfig: Equatable { && lhs.wrapperName == rhs.wrapperName && lhs.wrapperVersion == rhs.wrapperVersion && lhs.additionalHeaders == rhs.additionalHeaders + && lhs.autoAliasingOptOut == rhs.autoAliasingOptOut } } diff --git a/LaunchDarkly/LaunchDarkly/Models/User/LDUser.swift b/LaunchDarkly/LaunchDarkly/Models/User/LDUser.swift index b4a1a236..bb0b4ce8 100644 --- a/LaunchDarkly/LaunchDarkly/Models/User/LDUser.swift +++ b/LaunchDarkly/LaunchDarkly/Models/User/LDUser.swift @@ -36,6 +36,8 @@ public struct LDUser { [CodingKeys.device.rawValue, CodingKeys.operatingSystem.rawValue] } + static let storedIdKey: String = "ldDeviceIdentifier" + ///Client app defined string that uniquely identifies the user. If the client app does not define a key, the SDK will assign an identifier associated with the anonymous user. The key cannot be made private. public var key: String ///The secondary key for the user. See the [documentation](https://docs.launchdarkly.com/home/managing-flags/targeting-users#percentage-rollout-logic) for more information on it's use for percentage rollout bucketing. @@ -62,6 +64,7 @@ public struct LDUser { public var device: String? ///Client app defined operatingSystem for the user. The SDK will determine the operatingSystem automatically, however the client app can override the value. The SDK will insert the operatingSystem into the `custom` dictionary. The operatingSystem cannot be made private. (Default: the system identified operating system) public var operatingSystem: String? + /** Client app defined privateAttributes for the user. The SDK will not include private attribute values in analytics events, but private attribute names will be sent. @@ -230,22 +233,14 @@ public struct LDUser { static func defaultKey(environmentReporter: EnvironmentReporting) -> String { //For iOS & tvOS, this should be UIDevice.current.identifierForVendor.UUIDString //For macOS & watchOS, this should be a UUID that the sdk creates and stores so that the value returned here should be always the same - return environmentReporter.vendorUUID ?? UserDefaults.standard.installationKey - } -} - -extension UserDefaults { - struct Keys { - fileprivate static let deviceIdentifier = "ldDeviceIdentifier" - } - - fileprivate var installationKey: String { - if let key = self.string(forKey: Keys.deviceIdentifier) { - return key + if let vendorUUID = environmentReporter.vendorUUID { + return vendorUUID + } + if let storedId = UserDefaults.standard.string(forKey: storedIdKey) { + return storedId } - let key = UUID().uuidString - self.set(key, forKey: Keys.deviceIdentifier) + UserDefaults.standard.set(key, forKey: storedIdKey) return key } } diff --git a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift index 74a1292d..08a849c1 100644 --- a/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift +++ b/LaunchDarkly/LaunchDarkly/Networking/DarklyService.swift @@ -21,7 +21,7 @@ extension EventSource: DarklyStreamingProvider {} protocol DarklyServiceProvider: class { var config: LDConfig { get } - var user: LDUser { get } + var user: LDUser { get set } var diagnosticCache: DiagnosticCaching? { get } func getFeatureFlags(useReport: Bool, completion: ServiceCompletionHandler?) @@ -57,7 +57,7 @@ final class DarklyService: DarklyServiceProvider { } let config: LDConfig - let user: LDUser + var user: LDUser let httpHeaders: HTTPHeaders let diagnosticCache: DiagnosticCaching? private (set) var serviceFactory: ClientServiceCreating diff --git a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift index 69acc251..f8a9a8c2 100644 --- a/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift +++ b/LaunchDarkly/LaunchDarkly/ObjectiveC/ObjcLDClient.swift @@ -99,6 +99,17 @@ public final class ObjcLDClient: NSObject { ldClient.setOnline(goOnline, completion: completion) } + /** + Reports the initialization state of the LDClient. + + When true, the SDK has either communicated with LaunchDarkly servers for feature flag values or the SDK has been set offline. + + When false, the SDK has not been able to communicate with LaunchDarkly servers. Client apps can request feature flag values and set/change feature flag observers but flags might not exist or be stale. + */ + @objc public var isInitialized: Bool { + ldClient.isInitialized + } + /** The LDUser set into the LDClient may affect the set of feature flags returned by the LaunchDarkly server, and ties event tracking to the user. See `LDUser` for details about what information can be retained. @@ -794,6 +805,23 @@ public final class ObjcLDClient: NSObject { ldClient.flush() } + /** + Tells the SDK to generate an alias event. + + Associates two users for analytics purposes. + + This can be helpful in the situation where a person is represented by multiple + LaunchDarkly users. This may happen, for example, when a person initially logs into + an application-- the person might be represented by an anonymous user prior to logging + in and a different user after logging in, as denoted by a different user key. + + - parameter context: the user that will be aliased to + - parameter previousContext: the user that will be bound to the new context + */ + @objc public func alias(context: ObjcLDUser, previousContext: ObjcLDUser) { + ldClient.alias(context: context.user, previousContext: previousContext.user) + } + /** Starts the LDClient using the passed in `config` & `user`. Call this before requesting feature flag values. The LDClient will not go online until you call this method. Starting the LDClient means setting the `config` & `user`, setting the client online if `config.startOnline` is true (the default setting), and starting event recording. The client app must start the LDClient before it will report feature flag values. If a client does not call `start`, no methods will work. diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift index c722d017..8919afa9 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/ClientServiceFactory.swift @@ -30,7 +30,7 @@ protocol ClientServiceCreating { func makeErrorNotifier() -> ErrorNotifying func makeConnectionInformation() -> ConnectionInformation func makeDiagnosticCache(sdkKey: String) -> DiagnosticCaching - func makeDiagnosticReporter(service: DarklyServiceProvider, runMode: LDClientRunMode) -> DiagnosticReporting + func makeDiagnosticReporter(service: DarklyServiceProvider) -> DiagnosticReporting func makeFlagStore() -> FlagMaintaining } @@ -140,8 +140,8 @@ final class ClientServiceFactory: ClientServiceCreating { DiagnosticCache(sdkKey: sdkKey) } - func makeDiagnosticReporter(service: DarklyServiceProvider, runMode: LDClientRunMode) -> DiagnosticReporting { - DiagnosticReporter(service: service, runMode: runMode) + func makeDiagnosticReporter(service: DarklyServiceProvider) -> DiagnosticReporting { + DiagnosticReporter(service: service) } func makeFlagStore() -> FlagMaintaining { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift index 9eaca580..643fc792 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/DiagnosticReporter.swift @@ -9,86 +9,56 @@ import Foundation //sourcery: autoMockable protocol DiagnosticReporting { - //sourcery: defaultMockValue = DarklyServiceMock() - var service: DarklyServiceProvider { get set } - //sourcery: defaultMockValue = .foreground - var runMode: LDClientRunMode { get set } - //sourcery: defaultMockValue = false - var isOnline: Bool { get set } + func setMode(_ runMode: LDClientRunMode, online: Bool) } class DiagnosticReporter: DiagnosticReporting { - var service: DarklyServiceProvider { - didSet { - guard service.config != oldValue.config - else { return } - stateQueue.async { - self.stopReporting() - self.sentInit = false - self.maybeStartReporting() - } - } - } - - var runMode: LDClientRunMode { - didSet { - guard runMode != oldValue - else { return } - stateQueue.async { - self.stopReporting() - self.maybeStartReporting() - } - } - } - - var isOnline: Bool = false { - didSet { - guard isOnline != oldValue - else { return } - stateQueue.async { - self.stopReporting() - self.maybeStartReporting() - } - } - } - + private let service: DarklyServiceProvider private var timer: TimeResponding? private var sentInit: Bool private let stateQueue = DispatchQueue(label: "com.launchdarkly.diagnosticReporter.state", qos: .background) private let workQueue = DispatchQueue(label: "com.launchdarkly.diagnosticReporter.work", qos: .background) - init(service: DarklyServiceProvider, runMode: LDClientRunMode) { + init(service: DarklyServiceProvider) { self.service = service - self.runMode = runMode self.sentInit = false - maybeStartReporting() } - private func maybeStartReporting() { - guard isOnline && runMode == .foreground - else { return } - timer?.cancel() - if let cache = self.service.diagnosticCache { - if !sentInit { - sentInit = true - if let lastStats = cache.lastStats { - sendDiagnosticEventAsync(diagnosticEvent: lastStats) + func setMode(_ runMode: LDClientRunMode, online: Bool) { + if online && runMode == .foreground { + startReporting() + } else { + stopReporting() + } + } + + private func startReporting() { + stateQueue.sync { + timer?.cancel() + if let cache = self.service.diagnosticCache { + if !sentInit { + sentInit = true + if let lastStats = cache.lastStats { + sendDiagnosticEventAsync(diagnosticEvent: lastStats) + } + let initEvent = DiagnosticInit(config: service.config, + diagnosticId: cache.getDiagnosticId(), + creationDate: Date().millisSince1970) + sendDiagnosticEventAsync(diagnosticEvent: initEvent) } - let initEvent = DiagnosticInit(config: service.config, - diagnosticId: cache.getDiagnosticId(), - creationDate: Date().millisSince1970) - sendDiagnosticEventAsync(diagnosticEvent: initEvent) - } - timer = LDTimer(withTimeInterval: service.config.diagnosticRecordingInterval, repeats: true, fireQueue: workQueue) { - self.sendDiagnosticEventSync(diagnosticEvent: cache.getCurrentStatsAndReset()) + timer = LDTimer(withTimeInterval: service.config.diagnosticRecordingInterval, repeats: true, fireQueue: workQueue) { + self.sendDiagnosticEventSync(diagnosticEvent: cache.getCurrentStatsAndReset()) + } } } } private func stopReporting() { - timer?.cancel() - timer = nil + stateQueue.sync { + timer?.cancel() + timer = nil + } } private func sendDiagnosticEventAsync(diagnosticEvent: T) { diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift index b474b7e0..45b7d9cc 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/EnvironmentReporter.swift @@ -135,7 +135,7 @@ struct EnvironmentReporter: EnvironmentReporting { var shouldThrottleOnlineCalls: Bool { true } #endif - let sdkVersion = "5.3.2" + let sdkVersion = "5.4.0" // Unfortunately, the following does not function in certain configurations, such as when included through SPM // var sdkVersion: String { // Bundle(for: LDClient.self).infoDictionary?["CFBundleShortVersionString"] as? String ?? "5.x" diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift index 9d17478a..7584538f 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagChangeNotifier.swift @@ -12,10 +12,6 @@ protocol FlagChangeNotifying { func addFlagChangeObserver(_ observer: FlagChangeObserver) func addFlagsUnchangedObserver(_ observer: FlagsUnchangedObserver) func addConnectionModeChangedObserver(_ observer: ConnectionModeChangedObserver) - //sourcery: noMock - func removeObserver(_ key: LDFlagKey, owner: LDObserverOwner) - func removeObserver(_ keys: [LDFlagKey], owner: LDObserverOwner) - //sourcery: noMock func removeObserver(owner: LDObserverOwner) func notifyConnectionModeChangedObservers(connectionMode: ConnectionInformation.ConnectionMode) func notifyObservers(flagStore: FlagMaintaining, oldFlags: [LDFlagKey: FeatureFlag]) @@ -44,18 +40,6 @@ final class FlagChangeNotifier: FlagChangeNotifying { connectionModeChangedQueue.sync { connectionModeChangedObservers.append(observer) } } - ///Removes any change handling closures for flag.key from owner - func removeObserver(_ key: LDFlagKey, owner: LDObserverOwner) { - Log.debug(typeName(and: #function) + "key: \(key), owner: \(owner)") - removeObserver([key], owner: owner) - } - - ///Removes any change handling closures for flag keys from owner - func removeObserver(_ keys: [LDFlagKey], owner: LDObserverOwner) { - Log.debug(typeName(and: #function) + "keys: \(keys), owner: \(owner)") - flagChangeQueue.sync { flagChangeObservers.removeAll { $0.flagKeys == keys && $0.owner === owner } } - } - ///Removes all change handling closures from owner func removeObserver(owner: LDObserverOwner) { Log.debug(typeName(and: #function) + "owner: \(owner)") @@ -89,10 +73,8 @@ final class FlagChangeNotifier: FlagChangeNotifying { } flagsUnchangedQueue.sync { flagsUnchangedObservers.forEach { flagsUnchangedObserver in - if let flagsUnchangedHandler = flagsUnchangedObserver.flagsUnchangedHandler { - DispatchQueue.main.async { - flagsUnchangedHandler() - } + DispatchQueue.main.async { + flagsUnchangedObserver.flagsUnchangedHandler() } } } diff --git a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift index d1ad778f..2239b474 100644 --- a/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift +++ b/LaunchDarkly/LaunchDarkly/ServiceObjects/FlagStore.swift @@ -7,19 +7,13 @@ import Foundation -//sourcery: autoMockable protocol FlagMaintaining { var featureFlags: [LDFlagKey: FeatureFlag] { get } - func replaceStore(newFlags: [LDFlagKey: Any]?, completion: CompletionClosure?) - func updateStore(updateDictionary: [String: Any], completion: CompletionClosure?) - func deleteFlag(deleteDictionary: [String: Any], completion: CompletionClosure?) - - //sourcery: noMock + func replaceStore(newFlags: [LDFlagKey: Any], completion: CompletionClosure?) + func updateStore(updateDictionary: [LDFlagKey: Any], completion: CompletionClosure?) + func deleteFlag(deleteDictionary: [LDFlagKey: Any], completion: CompletionClosure?) func featureFlag(for flagKey: LDFlagKey) -> FeatureFlag? - - //sourcery: noMock - func variation(forKey key: LDFlagKey, defaultValue: T) -> T } final class FlagStore: FlagMaintaining { @@ -49,10 +43,10 @@ final class FlagStore: FlagMaintaining { } ///Replaces all feature flags with new flags. Pass nil to reset to an empty flag store - func replaceStore(newFlags: [LDFlagKey: Any]?, completion: CompletionClosure?) { + func replaceStore(newFlags: [LDFlagKey: Any], completion: CompletionClosure?) { Log.debug(typeName(and: #function) + "newFlags: \(String(describing: newFlags))") flagQueue.async(flags: .barrier) { - self._featureFlags = newFlags?.flagCollection ?? [:] + self._featureFlags = newFlags.flagCollection ?? [:] if let completion = completion { DispatchQueue.main.async { completion() @@ -71,7 +65,7 @@ final class FlagStore: FlagMaintaining { "reason": } */ - func updateStore(updateDictionary: [String: Any], completion: CompletionClosure?) { + func updateStore(updateDictionary: [LDFlagKey: Any], completion: CompletionClosure?) { flagQueue.async(flags: .barrier) { defer { if let completion = completion { @@ -104,7 +98,7 @@ final class FlagStore: FlagMaintaining { "version": } */ - func deleteFlag(deleteDictionary: [String: Any], completion: CompletionClosure?) { + func deleteFlag(deleteDictionary: [LDFlagKey: Any], completion: CompletionClosure?) { flagQueue.async(flags: .barrier) { defer { if let completion = completion { @@ -146,12 +140,6 @@ final class FlagStore: FlagMaintaining { func featureFlag(for flagKey: LDFlagKey) -> FeatureFlag? { flagQueue.sync { _featureFlags[flagKey] } } - - func variation(forKey key: LDFlagKey, defaultValue: T) -> T { - flagQueue.sync { - (_featureFlags[key]?.value as? T) ?? defaultValue - } - } } extension FlagStore: TypeIdentifying { } diff --git a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift index f194d81d..319fa74e 100644 --- a/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift @@ -22,15 +22,6 @@ final class LDClientSpec: QuickSpec { fileprivate static let updateThreshold: TimeInterval = 0.05 } - struct BadFlagKeys { - static let bool = "bool-flag-bad" - static let int = "int-flag-bad" - static let double = "double-flag-bad" - static let string = "string-flag-bad" - static let array = "array-flag-bad" - static let dictionary = "dictionary-flag-bad" - } - struct DefaultFlagValues { static let bool = false static let int = 5 @@ -44,10 +35,8 @@ final class LDClientSpec: QuickSpec { var config: LDConfig! var user: LDUser! var subject: LDClient! + let serviceFactoryMock = ClientServiceMockFactory() // mock getters based on setting up the user & subject - var serviceFactoryMock: ClientServiceMockFactory! { - subject.serviceFactory as? ClientServiceMockFactory - } var serviceMock: DarklyServiceMock! { subject.service as? DarklyServiceMock } @@ -75,7 +64,6 @@ final class LDClientSpec: QuickSpec { var environmentReporterMock: EnvironmentReportingMock! { subject.environmentReporter as? EnvironmentReportingMock } - // makeFlagSynchronizer getters var makeFlagSynchronizerStreamingMode: LDStreamingMode? { serviceFactoryMock.makeFlagSynchronizerReceivedParameters?.streamingMode } @@ -94,113 +82,82 @@ final class LDClientSpec: QuickSpec { var recordedEvent: LaunchDarkly.Event? { eventReporterMock.recordReceivedEvent } - // user flags - var oldFlags: [LDFlagKey: FeatureFlag]! - // throttler var throttlerMock: ThrottlingMock? { subject.throttler as? ThrottlingMock } - init(newUser: LDUser? = nil, - noUser: Bool = false, - newConfig: LDConfig? = nil, + private(set) var cachedFlags: [String: [String: [LDFlagKey: FeatureFlag]]] = [:] + + init(newConfig: LDConfig? = nil, startOnline: Bool = false, streamingMode: LDStreamingMode = .streaming, enableBackgroundUpdates: Bool = true, - runMode: LDClientRunMode = .foreground, operatingSystem: OperatingSystem? = nil, - completion: (() -> Void)? = nil) { + autoAliasingOptOut: Bool = true) { - let clientServiceFactory = ClientServiceMockFactory() if let operatingSystem = operatingSystem { - clientServiceFactory.makeEnvironmentReporterReturnValue.operatingSystem = operatingSystem + serviceFactoryMock.makeEnvironmentReporterReturnValue.operatingSystem = operatingSystem } + serviceFactoryMock.makeFlagChangeNotifierReturnValue = FlagChangeNotifier() - config = newConfig ?? LDConfig.stub(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: clientServiceFactory.makeEnvironmentReporterReturnValue) + let flagCache = serviceFactoryMock.makeFeatureFlagCacheReturnValue + flagCache.retrieveFeatureFlagsCallback = { + let received = flagCache.retrieveFeatureFlagsReceivedArguments! + flagCache.retrieveFeatureFlagsReturnValue = self.cachedFlags[received.mobileKey]?[received.userKey] + } + + config = newConfig ?? LDConfig.stub(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: serviceFactoryMock.makeEnvironmentReporterReturnValue) config.startOnline = startOnline config.streamingMode = streamingMode config.enableBackgroundUpdates = enableBackgroundUpdates config.eventFlushInterval = 300.0 //5 min...don't want this to trigger - user = newUser ?? LDUser.stub() - let stubFlags = FlagMaintainingMock(flags: FlagMaintainingMock.stubFlags(includeNullValue: true, includeVersions: true)) - clientServiceFactory.makeFlagStoreReturnValue = stubFlags - oldFlags = stubFlags.featureFlags + config.autoAliasingOptOut = autoAliasingOptOut - let flagNotifier = (ClientServiceFactory().makeFlagChangeNotifier() as! FlagChangeNotifier) - - LDClient.start(serviceFactory: clientServiceFactory, config: config, user: noUser ? nil : user, flagCache: clientServiceFactory.makeFeatureFlagCache(), flagNotifier: flagNotifier) { - self.startCompletion(runMode: runMode, completion: completion) - } - flagNotifier.notifyObservers(flagStore: stubFlags, oldFlags: self.oldFlags) + user = LDUser.stub() } - - init(newUser: LDUser? = nil, - noUser: Bool = false, - newConfig: LDConfig? = nil, - startOnline: Bool = false, - streamingMode: LDStreamingMode = .streaming, - enableBackgroundUpdates: Bool = true, - runMode: LDClientRunMode = .foreground, - operatingSystem: OperatingSystem? = nil, - timeOut: TimeInterval, - forceTimeout: Bool = false, - timeOutCompletion: ((_ timedOut: Bool) -> Void)? = nil) { - let clientServiceFactory = ClientServiceMockFactory() - if let operatingSystem = operatingSystem { - clientServiceFactory.makeEnvironmentReporterReturnValue.operatingSystem = operatingSystem - } - - config = newConfig ?? LDConfig.stub(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: clientServiceFactory.makeEnvironmentReporterReturnValue) - config.startOnline = startOnline - config.streamingMode = streamingMode - config.enableBackgroundUpdates = enableBackgroundUpdates - config.eventFlushInterval = 300.0 //5 min...don't want this to trigger - user = newUser ?? LDUser.stub() - let stubFlags = FlagMaintainingMock(flags: FlagMaintainingMock.stubFlags(includeNullValue: true, includeVersions: true)) - clientServiceFactory.makeFlagStoreReturnValue = stubFlags - oldFlags = stubFlags.featureFlags + func withUser(_ user: LDUser?) -> TestContext { + self.user = user + return self + } - let flagNotifier = (ClientServiceFactory().makeFlagChangeNotifier() as! FlagChangeNotifier) - - LDClient.start(serviceFactory: clientServiceFactory, config: config, user: noUser ? nil : user, startWaitSeconds: timeOut, flagCache: clientServiceFactory.makeFeatureFlagCache(), flagNotifier: flagNotifier) { timedOut in - self.startCompletion(runMode: runMode, timedOut: timedOut, timeOutCompletion: timeOutCompletion) - } - if !forceTimeout { - flagNotifier.notifyObservers(flagStore: stubFlags, oldFlags: self.oldFlags) - } + func withCached(flags: [LDFlagKey: FeatureFlag]?) -> TestContext { + withCached(userKey: user.key, flags: flags) } - func startCompletion(runMode: LDClientRunMode, timedOut: Bool = false, completion: (() -> Void)? = nil, timeOutCompletion: ((_ timedOut: Bool) -> Void)? = nil) { - subject = LDClient.get() + func withCached(userKey: String, flags: [LDFlagKey: FeatureFlag]?) -> TestContext { + var forEnv = cachedFlags[config.mobileKey] ?? [:] + forEnv[userKey] = flags + cachedFlags[config.mobileKey] = forEnv + return self + } - if runMode == .background { - subject.setRunMode(.background) + func start(runMode: LDClientRunMode = .foreground, completion: (() -> Void)? = nil) { + LDClient.start(serviceFactory: serviceFactoryMock, config: config, user: user) { + self.subject = LDClient.get() + if runMode == .background { + self.subject.setRunMode(.background) + } + completion?() } - completion?() - timeOutCompletion?(timedOut) + subject = LDClient.get() } - ///Pass nil to leave the flags unchanged - func setFlagStoreCallbackToMimicRealFlagStore(newFlags: [LDFlagKey: FeatureFlag]? = nil) { - flagStoreMock.replaceStoreCallback = { - self.flagStoreMock!.featureFlags = newFlags ?? self.flagStoreMock!.featureFlags - self.flagStoreMock!.replaceStoreReceivedArguments?.completion?() - } - flagStoreMock.updateStoreCallback = { - self.flagStoreMock!.featureFlags = newFlags ?? self.flagStoreMock!.featureFlags - self.flagStoreMock!.updateStoreReceivedArguments?.completion?() - } - flagStoreMock.deleteFlagCallback = { - self.flagStoreMock!.featureFlags = newFlags ?? self.flagStoreMock!.featureFlags - self.flagStoreMock!.deleteFlagReceivedArguments?.completion?() + func start(runMode: LDClientRunMode = .foreground, timeOut: TimeInterval, timeOutCompletion: ((_ timedOut: Bool) -> Void)? = nil) { + LDClient.start(serviceFactory: serviceFactoryMock, config: config, user: user, startWaitSeconds: timeOut) { timedOut in + self.subject = LDClient.get() + if runMode == .background { + self.subject.setRunMode(.background) + } + timeOutCompletion?(timedOut) } + subject = LDClient.get() } } override func spec() { startSpec() - startWithTimeoutSpec() + moveToBackgroundSpec() identifySpec() setOnlineSpec() closeSpec() @@ -211,347 +168,189 @@ final class LDClientSpec: QuickSpec { runModeSpec() streamingModeSpec() flushSpec() - allFlagValuesSpec() + allFlagsSpec() connectionInformationSpec() variationDetailSpec() + aliasingSpec() + isInitializedSpec() } - private func startSpec() { - describe("start") { - var testContext: TestContext! + private func aliasingSpec() { + describe("aliasing") { + var ctx: TestContext! - context("when configured to start online") { + context("automatic aliasing from anonymous to user") { beforeEach { - waitUntil(timeout: .seconds(10)) { done in - testContext = TestContext(startOnline: true, completion: done) + waitUntil { done in + ctx = TestContext(autoAliasingOptOut: false).withUser(LDUser(isAnonymous: true)) + ctx.start(completion: done) } - } - 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 + let notAnonymous = LDUser(key: "something", isAnonymous: false) + waitUntil { done in + ctx.subject.internalIdentify(newUser: notAnonymous, completion: done) } - 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 + + it("records an alias and identify event") { + // init, identify, and alias event + expect(ctx.eventReporterMock.recordCallCount) == 3 + expect(ctx.recordedEvent?.kind) == .alias } } - context("when configured to start offline") { + + context("automatic aliasing from user to user") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: false, completion: 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 - 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 { - waitUntil { done in - testContext = TestContext(startOnline: true, runMode: .background, operatingSystem: os, completion: done) - } - } - it("takes the client and service objects online when background enabled") { - expect(testContext.subject.isOnline) == true - expect(testContext.subject.flagSynchronizer.isOnline) == os.isBackgroundEnabled - 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 - 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 - } + waitUntil { done in + ctx = TestContext().withUser(LDUser(isAnonymous: false)) + ctx.start(completion: done) } - } - } - context("when configured to not allow background updates and running in background mode") { - OperatingSystem.allOperatingSystems.forEach { os in - context("on \(os)") { - beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, runMode: .background, operatingSystem: os, completion: done) - } - } - it("leaves the client and service objects offline") { - expect(testContext.subject.isOnline) == true - expect(testContext.subject.flagSynchronizer.isOnline) == false - expect(testContext.subject.eventReporter.isOnline) == true - } - 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 - 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 - } + let notAnonymous = LDUser(key: "something", isAnonymous: false) + waitUntil { done in + ctx.subject.internalIdentify(newUser: notAnonymous, completion: done) } } - } - context("when called without user") { - context("after setting user") { - beforeEach { - waitUntil { done in - testContext = TestContext(noUser: true, startOnline: true, completion: done) - } - waitUntil { done in - testContext.subject.internalIdentify(newUser: testContext.user, completion: done) - testContext.subject.flagChangeNotifier.notifyObservers(flagStore: testContext.flagStoreMock, oldFlags: testContext.oldFlags) - } - } - 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) == 2 //called on init and subsequent identify - 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) == 2 //both start and internalIdentify - expect(testContext.recordedEvent?.kind) == .identify - expect(testContext.recordedEvent?.key) == testContext.user.key - } - it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 2 //Both start and internalIdentify - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config - } - } - context("without setting user") { - beforeEach { - waitUntil { done in - testContext = TestContext(noUser: true, startOnline: true, completion: 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 - } + it("doesnt record an alias event") { + // init and identify event + expect(ctx.eventReporterMock.recordCallCount) == 2 + expect(ctx.recordedEvent?.kind) == .identify } } - context("when called with cached flags for the user and environment") { - var retrievedFlags: [LDFlagKey: FeatureFlag]! + + context("automatic aliasing from anonymous to anonymous") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: false, completion: done) + waitUntil { done in + ctx = TestContext().withUser(LDUser(isAnonymous: false)) + ctx.start(completion: done) } - testContext.featureFlagCachingMock.retrieveFeatureFlagsReturnValue = testContext.flagStoreMock.featureFlags - retrievedFlags = testContext.flagStoreMock.featureFlags - testContext.flagStoreMock.featureFlags = [:] - waitUntil { done in - testContext.subject.internalIdentify(newUser: testContext.user, completion: done) + let notAnonymous = LDUser(key: "something", isAnonymous: false) + waitUntil { done in + ctx.subject.internalIdentify(newUser: notAnonymous, completion: done) } } - it("checks the flag cache for the user and environment") { - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 2 //called on init and subsequent identify - 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) == 2 // both start and identify - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config - } - } - context("when called without cached flags for the user") { - beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: false) { - testContext.flagStoreMock.featureFlags = [:] - 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 + + it("doesnt record an alias event") { + // init and identify event + expect(ctx.eventReporterMock.recordCallCount) == 2 + expect(ctx.recordedEvent?.kind) == .identify } } } } - private func startWithTimeoutSpec() { + private func startSpec() { + describe("start") { + startSpec(withTimeout: false) + } describe("startWithTimeout") { - var testContext: TestContext! - - context("when configured to start online") { - beforeEach { - waitUntil(timeout: .seconds(15)) { done in - testContext = TestContext(startOnline: true, timeOut: 10) { timedOut in - expect(timedOut) == false - done() - } - } + startSpec(withTimeout: true) + } + describe("startCompletions") { + startCompletionSpec() + } + } + + private func startSpec(withTimeout: Bool) { + var testContext: TestContext! + + context("when configured to start online") { + beforeEach { + testContext = TestContext(startOnline: true) + withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() + } + 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 } - 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 + 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 + } + it("starts in foreground") { + expect(testContext.subject.runMode) == .foreground + } + } + context("when configured to start offline") { + beforeEach { + testContext = TestContext() + withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() + } + 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 + 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 + } + it("starts in foreground") { + expect(testContext.subject.runMode) == .foreground + } + } + context("when called without user") { + context("after setting user") { + beforeEach { + testContext = TestContext(startOnline: true).withUser(nil) + withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() + + testContext.user = LDUser.stub() + testContext.subject.internalIdentify(newUser: testContext.user) } it("saves the config") { expect(testContext.subject.config) == testContext.config @@ -565,52 +364,30 @@ final class LDClientSpec: QuickSpec { 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(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.retrieveFeatureFlagsCallCount) == 2 //called on init and subsequent identify 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.eventReporterMock.recordCallCount) == 2 //both start and internalIdentify expect(testContext.recordedEvent?.kind) == .identify expect(testContext.recordedEvent?.key) == testContext.user.key } it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 + expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 2 //Both start and internalIdentify expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config } } - context("when configured to start online") { + context("without setting user") { beforeEach { - waitUntil(timeout: .seconds(10)) { done in - testContext = TestContext(startOnline: true, timeOut: 2.0, forceTimeout: true) { timedOut in - expect(timedOut) == true - done() - } - } - } - it("times out properly") { - expect(testContext.subject.isOnline) == true - } - } - context("when configured to start offline") { - beforeEach { - waitUntil(timeout: .seconds(15)) { done in - testContext = TestContext(startOnline: false, timeOut: 10) { 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 + testContext = TestContext(startOnline: true).withUser(nil) + withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() } it("saves the config") { expect(testContext.subject.config) == testContext.config @@ -619,50 +396,180 @@ final class LDClientSpec: QuickSpec { 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("uses anonymous user") { + expect(testContext.subject.user.key) == LDUser.defaultKey(environmentReporter: testContext.environmentReporterMock) + expect(testContext.subject.user.isAnonymous).to(beTrue()) + expect(testContext.subject.service.user) == testContext.subject.user + expect(testContext.makeFlagSynchronizerService?.user) == testContext.subject.user + expect(testContext.subject.eventReporter.service.user) == testContext.subject.user } it("uncaches the new users flags") { expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.user.key + expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsReceivedArguments?.userKey) == testContext.subject.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 + expect(testContext.recordedEvent?.key) == testContext.subject.user.key } it("converts cached data") { expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user + expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.subject.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)") { + } + context("when called with cached flags for the user and environment") { + beforeEach { + testContext = TestContext().withCached(flags: FlagMaintainingMock.stubFlags()) + withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() + } + 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) == FlagMaintainingMock.stubFlags() + } + 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() + withTimeout ? testContext.start(timeOut: 10.0) : testContext.start() + } + 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 + } + } + } + + func startCompletionSpec() { + var testContext: TestContext! + var completed = false + var didTimeOut: Bool? = nil + var startTime: Date! + var completeTime: Date! + + let startCompletion = { completed = true } + func startTimeoutCompletion(_ done: (() -> Void)? = nil) -> (Bool) -> Void { + { timedOut in + completeTime = Date() + didTimeOut = timedOut + completed = true + done?() + } + } + + beforeEach { + completed = false + didTimeOut = nil + startTime = nil + completeTime = nil + } + + context("when configured to start offline") { + beforeEach { + testContext = TestContext() + } + it("completes immediately without timeout") { + testContext.start(completion: startCompletion) + expect(completed) == true + } + it("completes immediately with timeout") { + testContext.start(timeOut: 5.0, timeOutCompletion: startTimeoutCompletion()) + expect(completed) == true + expect(didTimeOut) == true + } + } + context("when configured to start online") { + beforeEach { + testContext = TestContext(startOnline: true) + } + context("without receiving flags") { + for withCached in [false, true] { + context(withCached ? "with cached flags" : "") { beforeEach { - waitUntil(timeout: .seconds(15)) { done in - testContext = TestContext(startOnline: true, runMode: .background, operatingSystem: os, timeOut: 10) { timedOut in - expect(timedOut) == false - done() - } + if withCached { + _ = testContext.withCached(flags: FlagMaintainingMock.stubFlags()) } - waitUntil(timeout: .seconds(10)) { done in - testContext.subject.setService(ClientServiceMockFactory().makeDarklyServiceProvider(config: testContext.subject.config, user: testContext.subject.user)) - testContext.subject.setOnline(true, completion: done) - testContext.subject.flagChangeNotifier.notifyObservers(flagStore: testContext.flagStoreMock, oldFlags: testContext.oldFlags) + } + it("does not complete without timeout") { + testContext.start(completion: startCompletion) + Thread.sleep(forTimeInterval: 1.0) + expect(completed) == false + } + it("completes in timed out state with timeout") { + waitUntil(timeout: .seconds(5)) { done in + startTime = Date() + testContext.start(timeOut: 1.0, timeOutCompletion: startTimeoutCompletion(done)) } + expect(completed) == true + expect(didTimeOut) == true + // Should not have occured immediately + expect(completeTime.timeIntervalSince(startTime)) >= 1.0 + + // Test that already timed out completion is not called when sync completes + completed = false + testContext.onSyncComplete?(.success([:], nil)) + testContext.onSyncComplete?(.success([:], .ping)) + testContext.onSyncComplete?(.success([:], .put)) + Thread.sleep(forTimeInterval: 1.0) + expect(completed) == false + } + } + } + } + for eventType in [nil, FlagUpdateType.ping, FlagUpdateType.put] { + context("after receiving flags as " + (eventType?.rawValue ?? "poll")) { + it("does complete without timeout") { + testContext.start(completion: startCompletion) + testContext.onSyncComplete?(.success([:], eventType)) + expect(completed).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) + } + it("does complete with timeout") { + waitUntil(timeout: .seconds(3)) { done in + testContext.start(timeOut: 5.0, timeOutCompletion: startTimeoutCompletion(done)) + testContext.onSyncComplete?(.success([:], eventType)) + } + expect(completed).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) + expect(didTimeOut) == false + } + } + } + } + } + + func moveToBackgroundSpec() { + describe("moveToBackground") { + var testContext: TestContext! + context("when configured to allow background updates") { + OperatingSystem.allOperatingSystems.forEach { os in + context("on \(os)") { + beforeEach { + testContext = TestContext(startOnline: true, operatingSystem: os) + testContext.start() + testContext.subject.setRunMode(.background) } 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.isOnline) == true + expect(testContext.subject.flagSynchronizer.isOnline) == os.isBackgroundEnabled expect(testContext.subject.eventReporter.isOnline) == testContext.subject.isOnline } it("saves the config") { @@ -699,25 +606,18 @@ final class LDClientSpec: QuickSpec { } } } - context("when configured to not allow background updates and running in background mode") { + context("when configured to not allow background updates") { OperatingSystem.allOperatingSystems.forEach { os in context("on \(os)") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, runMode: .background, operatingSystem: os, timeOut: 10) { timedOut in - expect(timedOut) == false - done() - } - } - waitUntil(timeout: .seconds(10)) { done in - testContext.subject.setOnline(true, completion: done) - testContext.subject.flagChangeNotifier.notifyObservers(flagStore: testContext.flagStoreMock, oldFlags: testContext.oldFlags) - } + testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, operatingSystem: os) + testContext.start() + testContext.subject.setRunMode(.background) } 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 + expect(testContext.subject.isOnline) == true + expect(testContext.subject.flagSynchronizer.isOnline) == false + expect(testContext.subject.eventReporter.isOnline) == true } it("saves the config") { expect(testContext.subject.config) == testContext.config @@ -753,147 +653,6 @@ final class LDClientSpec: QuickSpec { } } } - context("when called without user") { - context("after setting user") { - beforeEach { - waitUntil { done in - testContext = TestContext(noUser: true, timeOut: 3) { timedOut in - expect(timedOut) == true - done() - } - } - - waitUntil { done in - testContext.subject.internalIdentify(newUser: testContext.user, completion: 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) == 2 //called on init and subsequent identify - 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) == 2 // both start and identify - expect(testContext.recordedEvent?.kind) == .identify - expect(testContext.recordedEvent?.key) == testContext.user.key - } - it("converts cached data") { - expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 2 - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config - } - } - context("without setting user") { - beforeEach { - waitUntil { done in - testContext = TestContext(noUser: true, startOnline: false, timeOut: 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 { - waitUntil { done in - testContext = TestContext(startOnline: false, timeOut: 10) { timedOut in - expect(timedOut) == true - done() - } - } - testContext.featureFlagCachingMock.retrieveFeatureFlagsReturnValue = testContext.flagStoreMock.featureFlags - retrievedFlags = testContext.flagStoreMock.featureFlags - testContext.flagStoreMock.featureFlags = [:] - waitUntil { done in - testContext.subject.internalIdentify(newUser: testContext.user, completion: done) - } - } - it("checks the flag cache for the user and environment") { - expect(testContext.featureFlagCachingMock.retrieveFeatureFlagsCallCount) == 2 //called on init and subsequent identify - 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) == 2 // both start and internalIdentify - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user - expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config - } - } - context("when called without cached flags for the user") { - beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: false, timeOut: 10) { timedOut in - expect(timedOut) == true - done() - } - } - testContext.flagStoreMock.featureFlags = [:] - } - 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 - } - } } } @@ -902,20 +661,15 @@ final class LDClientSpec: QuickSpec { describe("identify") { var newUser: LDUser! - var stubFlags: FlagMaintainingMock! context("when the client is online") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, completion: done) - } + testContext = TestContext(startOnline: true) + testContext.start() testContext.featureFlagCachingMock.reset() testContext.cacheConvertingMock.reset() newUser = LDUser.stub() - waitUntil(timeout: .seconds(5)) { done in - testContext.subject.internalIdentify(newUser: newUser, completion: done) - testContext.subject.flagChangeNotifier.notifyObservers(flagStore: testContext.flagStoreMock, oldFlags: testContext.oldFlags) - } + testContext.subject.internalIdentify(newUser: newUser) } it("changes to the new user") { expect(testContext.subject.user) == newUser @@ -945,16 +699,13 @@ final class LDClientSpec: QuickSpec { } context("when the client is offline") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: false, completion: done) - } + testContext = TestContext() + testContext.start() testContext.featureFlagCachingMock.reset() testContext.cacheConvertingMock.reset() newUser = LDUser.stub() - waitUntil { done in - testContext.subject.internalIdentify(newUser: newUser, completion: done) - } + testContext.subject.internalIdentify(newUser: newUser) } it("changes to the new user") { expect(testContext.subject.user) == newUser @@ -983,26 +734,20 @@ final class LDClientSpec: QuickSpec { } } context("when the new user has cached feature flags") { + let stubFlags = FlagMaintainingMock.stubFlags() beforeEach { - //offline makes no request to update flags... - waitUntil { done in - testContext = TestContext(startOnline: false, completion: done) - } - testContext.featureFlagCachingMock.reset() newUser = LDUser.stub() - stubFlags = FlagMaintainingMock(flags: FlagMaintainingMock.stubFlags(includeNullValue: true, includeVersions: true)) - - testContext.featureFlagCachingMock.retrieveFeatureFlagsReturnValue = stubFlags.featureFlags + testContext = TestContext().withCached(userKey: newUser.key, flags: stubFlags) + testContext.start() + testContext.featureFlagCachingMock.reset() testContext.cacheConvertingMock.reset() - waitUntil { done in - testContext.subject.internalIdentify(newUser: newUser, completion: done) - } + testContext.subject.internalIdentify(newUser: newUser) } it("restores the cached users feature flags") { expect(testContext.subject.user) == newUser expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags?.flagCollection) == stubFlags?.featureFlags + expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags.flagCollection) == stubFlags } it("converts cached data") { expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 @@ -1021,14 +766,11 @@ final class LDClientSpec: QuickSpec { context("setting online") { beforeEach { waitUntil { done in - testContext = TestContext(startOnline: false, completion: done) - } - - waitUntil { done in - testContext.subject.setOnline(true) { + testContext = TestContext() + testContext.start { + testContext.subject.setOnline(true) done() } - testContext.subject.flagChangeNotifier.notifyObservers(flagStore: testContext.flagStoreMock, oldFlags: testContext.oldFlags) } } it("sets the client and service objects online") { @@ -1042,9 +784,8 @@ final class LDClientSpec: QuickSpec { context("when the client is online") { context("setting offline") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, completion: done) - } + testContext = TestContext(startOnline: true) + testContext.start() testContext.throttlerMock?.runThrottledCallCount = 0 testContext.subject.setOnline(false) @@ -1065,14 +806,11 @@ final class LDClientSpec: QuickSpec { var targetRunThrottledCalls: Int! beforeEach { waitUntil { done in - testContext = TestContext(runMode: .background, operatingSystem: os, completion: done) + testContext = TestContext(operatingSystem: os) + testContext.start(runMode: .background, completion: done) } targetRunThrottledCalls = os.isBackgroundEnabled ? 1 : 0 - waitUntil(timeout: .seconds(10)) { done in - testContext.subject.setService(ClientServiceMockFactory().makeDarklyServiceProvider(config: testContext.subject.config, user: testContext.subject.user)) - testContext.subject.setOnline(true, completion: done) - testContext.subject.flagChangeNotifier.notifyObservers(flagStore: testContext.flagStoreMock, oldFlags: testContext.oldFlags) - } + testContext.subject.setOnline(true) } it("takes the client and service objects online") { expect(testContext.throttlerMock?.runThrottledCallCount) == targetRunThrottledCalls @@ -1087,7 +825,8 @@ final class LDClientSpec: QuickSpec { context("while configured to disable background updates") { beforeEach { waitUntil { done in - testContext = TestContext(enableBackgroundUpdates: false, runMode: .background, operatingSystem: os, completion: done) + testContext = TestContext(enableBackgroundUpdates: false, operatingSystem: os) + testContext.start(runMode: .background, completion: done) } } context("and setting online") { @@ -1110,7 +849,8 @@ final class LDClientSpec: QuickSpec { context("when the mobile key is empty") { beforeEach { waitUntil { done in - testContext = TestContext(newConfig: LDConfig(mobileKey: ""), completion: done) + testContext = TestContext(newConfig: LDConfig(mobileKey: "")) + testContext.start(completion: done) } testContext.throttlerMock?.runThrottledCallCount = 0 @@ -1138,9 +878,8 @@ final class LDClientSpec: QuickSpec { } context("and online") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, completion: done) - } + testContext = TestContext(startOnline: true) + testContext.start() event = Event.stub(.custom, with: testContext.user) priorRecordedEvents = testContext.eventReporterMock.recordCallCount @@ -1159,9 +898,8 @@ final class LDClientSpec: QuickSpec { } context("and offline") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: false, completion: done) - } + testContext = TestContext() + testContext.start() event = Event.stub(.custom, with: testContext.user) priorRecordedEvents = testContext.eventReporterMock.recordCallCount @@ -1181,9 +919,8 @@ final class LDClientSpec: QuickSpec { } context("when already stopped") { beforeEach { - waitUntil { done in - testContext = TestContext(completion: done) - } + testContext = TestContext() + testContext.start() event = Event.stub(.custom, with: testContext.user) testContext.subject.close() priorRecordedEvents = testContext.eventReporterMock.recordCallCount @@ -1211,6 +948,7 @@ final class LDClientSpec: QuickSpec { var event: LaunchDarkly.Event! beforeEach { testContext = TestContext() + testContext.start() event = Event.stub(.custom, with: testContext.user) } context("when client was started") { @@ -1244,10 +982,16 @@ final class LDClientSpec: QuickSpec { var testContext: TestContext! beforeEach { waitUntil { done in - testContext = TestContext(completion: done) + testContext = TestContext() + testContext.start(completion: done) } } context("flag store contains the requested value") { + beforeEach { + waitUntil { done in + testContext.flagStoreMock.replaceStore(newFlags: FlagMaintainingMock.stubFlags(), completion: done) + } + } context("non-Optional default value") { it("returns the flag value") { //The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method @@ -1318,17 +1062,17 @@ final class LDClientSpec: QuickSpec { context("non-Optional default value") { it("returns the default value") { //The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method - expect(testContext.subject.variation(forKey: BadFlagKeys.bool, defaultValue: DefaultFlagValues.bool) as Bool) == DefaultFlagValues.bool - expect(testContext.subject.variation(forKey: BadFlagKeys.int, defaultValue: DefaultFlagValues.int) as Int) == DefaultFlagValues.int - expect(testContext.subject.variation(forKey: BadFlagKeys.double, defaultValue: DefaultFlagValues.double) as Double) == DefaultFlagValues.double - expect(testContext.subject.variation(forKey: BadFlagKeys.string, defaultValue: DefaultFlagValues.string) as String) == DefaultFlagValues.string - expect(testContext.subject.variation(forKey: BadFlagKeys.array, defaultValue: DefaultFlagValues.array) == DefaultFlagValues.array).to(beTrue()) - expect(testContext.subject.variation(forKey: BadFlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary) as [String: Any] == DefaultFlagValues.dictionary).to(beTrue()) + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) as Bool) == DefaultFlagValues.bool + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int) as Int) == DefaultFlagValues.int + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double) as Double) == DefaultFlagValues.double + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string) as String) == DefaultFlagValues.string + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array) == DefaultFlagValues.array).to(beTrue()) + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary) as [String: Any] == DefaultFlagValues.dictionary).to(beTrue()) } it("records a flag evaluation event") { - _ = testContext.subject.variation(forKey: BadFlagKeys.bool, defaultValue: DefaultFlagValues.bool) + _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == BadFlagKeys.bool + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool expect(AnyComparer.isEqual(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value, to: DefaultFlagValues.bool)).to(beTrue()) expect(AnyComparer.isEqual(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue, to: DefaultFlagValues.bool)).to(beTrue()) expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.featureFlag).to(beNil()) @@ -1338,18 +1082,18 @@ final class LDClientSpec: QuickSpec { context("Optional default value") { it("returns the default value") { //The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method - expect(testContext.subject.variation(forKey: BadFlagKeys.bool, defaultValue: DefaultFlagValues.bool as Bool?)) == DefaultFlagValues.bool - expect(testContext.subject.variation(forKey: BadFlagKeys.int, defaultValue: DefaultFlagValues.int as Int?)) == DefaultFlagValues.int - expect(testContext.subject.variation(forKey: BadFlagKeys.double, defaultValue: DefaultFlagValues.double as Double?)) == DefaultFlagValues.double - expect(testContext.subject.variation(forKey: BadFlagKeys.string, defaultValue: DefaultFlagValues.string as String?)) == DefaultFlagValues.string - expect(testContext.subject.variation(forKey: BadFlagKeys.array, defaultValue: DefaultFlagValues.array as Array?) == DefaultFlagValues.array).to(beTrue()) - expect(testContext.subject.variation(forKey: BadFlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary as [String: Any]?) == DefaultFlagValues.dictionary).to(beTrue()) + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool as Bool?)) == DefaultFlagValues.bool + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultFlagValues.int as Int?)) == DefaultFlagValues.int + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultFlagValues.double as Double?)) == DefaultFlagValues.double + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultFlagValues.string as String?)) == DefaultFlagValues.string + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultFlagValues.array as Array?) == DefaultFlagValues.array).to(beTrue()) + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultFlagValues.dictionary as [String: Any]?) == DefaultFlagValues.dictionary).to(beTrue()) } it("records a flag evaluation event") { //The cast in the variation call directs the compiler to the Optional variation method - _ = testContext.subject.variation(forKey: BadFlagKeys.bool, defaultValue: DefaultFlagValues.bool as Bool?) + _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool as Bool?) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == BadFlagKeys.bool + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool expect(AnyComparer.isEqual(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value, to: DefaultFlagValues.bool)).to(beTrue()) expect(AnyComparer.isEqual(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue, to: DefaultFlagValues.bool)).to(beTrue()) expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.featureFlag).to(beNil()) @@ -1359,18 +1103,18 @@ final class LDClientSpec: QuickSpec { context("no default value") { it("returns nil") { //The casts in the expect() calls allow the compiler to determine which variation method to use. This test calls the non-Optional variation method - expect(testContext.subject.variation(forKey: BadFlagKeys.bool, defaultValue: nil as Bool?)).to(beNil()) - expect(testContext.subject.variation(forKey: BadFlagKeys.int, defaultValue: nil as Int?)).to(beNil()) - expect(testContext.subject.variation(forKey: BadFlagKeys.double, defaultValue: nil as Double?)).to(beNil()) - expect(testContext.subject.variation(forKey: BadFlagKeys.string, defaultValue: nil as String?)).to(beNil()) - expect(testContext.subject.variation(forKey: BadFlagKeys.array, defaultValue: nil as [Any]?)).to(beNil()) - expect(testContext.subject.variation(forKey: BadFlagKeys.dictionary, defaultValue: nil as [String: Any]?)).to(beNil()) + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: nil as Bool?)).to(beNil()) + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: nil as Int?)).to(beNil()) + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: nil as Double?)).to(beNil()) + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: nil as String?)).to(beNil()) + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: nil as [Any]?)).to(beNil()) + expect(testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: nil as [String: Any]?)).to(beNil()) } it("records a flag evaluation event") { //The cast in the variation call directs the compiler to the Optional variation method - _ = testContext.subject.variation(forKey: BadFlagKeys.bool, defaultValue: nil as Bool?) + _ = testContext.subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: nil as Bool?) expect(testContext.eventReporterMock.recordFlagEvaluationEventsCallCount) == 1 - expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == BadFlagKeys.bool + expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.flagKey) == DarklyServiceMock.FlagKeys.bool expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.value).to(beNil()) expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.defaultValue).to(beNil()) expect(testContext.eventReporterMock.recordFlagEvaluationEventsReceivedArguments?.featureFlag).to(beNil()) @@ -1382,162 +1126,73 @@ final class LDClientSpec: QuickSpec { } private func observeSpec() { + var testContext: TestContext! + var mockNotifier: FlagChangeNotifyingMock! + var callCount: Int = 0 describe("observe") { - let mockNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() as! FlagChangeNotifyingMock - var receivedChangedFlag: Bool = false - var testContext: TestContext! beforeEach { - waitUntil { done in - testContext = TestContext(completion: done) - } - + testContext = TestContext() + testContext.start() + mockNotifier = FlagChangeNotifyingMock() testContext.subject.flagChangeNotifier = mockNotifier - testContext.subject.observe(key: "test-key", owner: self, handler: { _ in - receivedChangedFlag = true - }) + callCount = 0 } - it("registers a single flag observer") { + it("observe") { + testContext.subject.observe(key: "test-key", owner: self) { _ in callCount += 1 } let receivedObserver = mockNotifier.addFlagChangeObserverReceivedObserver expect(mockNotifier.addFlagChangeObserverCallCount) == 1 expect(receivedObserver?.flagKeys) == ["test-key"] expect(receivedObserver?.owner) === self receivedObserver?.flagChangeHandler?(LDChangedFlag(key: "", oldValue: nil, newValue: nil)) - expect(receivedChangedFlag) == true - } - } - - describe("observeKeys") { - let mockNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() as! FlagChangeNotifyingMock - var receivedChangedFlags: Bool = false - var testContext: TestContext! - - beforeEach { - waitUntil { done in - testContext = TestContext(completion: done) - } - - testContext.subject.flagChangeNotifier = mockNotifier - testContext.subject.observe(keys: ["test-key"], owner: self, handler: { _ in - receivedChangedFlags = true - }) + expect(callCount) == 1 } - it("registers a multiple flag observer") { + it("observeKeys") { + testContext.subject.observe(keys: ["test-key"], owner: self) { _ in callCount += 1 } let receivedObserver = mockNotifier.addFlagChangeObserverReceivedObserver expect(mockNotifier.addFlagChangeObserverCallCount) == 1 expect(receivedObserver?.flagKeys) == ["test-key"] expect(receivedObserver?.owner) === self let changedFlags = ["test-key": LDChangedFlag(key: "", oldValue: nil, newValue: nil)] receivedObserver?.flagCollectionChangeHandler?(changedFlags) - expect(receivedChangedFlags) == true + expect(callCount) == 1 } - } - - describe("observeAll") { - let mockNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() as! FlagChangeNotifyingMock - var receivedChangedFlags: Bool = false - var testContext: TestContext! - - beforeEach { - waitUntil { done in - testContext = TestContext(completion: done) - } - - testContext.subject.flagChangeNotifier = mockNotifier - testContext.subject.observeAll(owner: self, handler: { _ in - receivedChangedFlags = true - }) - } - it("registers a collection flag observer") { + it("observeAll") { + testContext.subject.observeAll(owner: self) { _ in callCount += 1 } let receivedObserver = mockNotifier.addFlagChangeObserverReceivedObserver expect(mockNotifier.addFlagChangeObserverCallCount) == 1 expect(receivedObserver?.flagKeys) == LDFlagKey.anyKey expect(receivedObserver?.owner) === self let changedFlags = ["test-key": LDChangedFlag(key: "", oldValue: nil, newValue: nil)] receivedObserver?.flagCollectionChangeHandler?(changedFlags) - expect(receivedChangedFlags) == true - } - } - - describe("observeFlagsUnchanged") { - let mockNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() as! FlagChangeNotifyingMock - var receivedFlagsUnchanged: Bool = false - var testContext: TestContext! - beforeEach { - waitUntil { done in - testContext = TestContext(completion: done) - } - - testContext.subject.flagChangeNotifier = mockNotifier - testContext.subject.observeFlagsUnchanged(owner: self, handler: { - receivedFlagsUnchanged = true - }) + expect(callCount) == 1 } - it("registers a flags unchanged observer") { + it("observeFlagsUnchanged") { + testContext.subject.observeFlagsUnchanged(owner: self) { callCount += 1 } let receivedObserver = mockNotifier.addFlagsUnchangedObserverReceivedObserver expect(mockNotifier.addFlagsUnchangedObserverCallCount) == 1 expect(receivedObserver?.owner) === self - receivedObserver?.flagsUnchangedHandler?() - expect(receivedFlagsUnchanged) == true + receivedObserver?.flagsUnchangedHandler() + expect(callCount) == 1 } - } - - describe("observeConnectionModeChanged") { - var testContext: TestContext! - let mockNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() as! FlagChangeNotifyingMock - var receivedConnectionModeChanged: Bool = false - beforeEach { - waitUntil { done in - testContext = TestContext(completion: done) - } - - testContext.subject.flagChangeNotifier = mockNotifier - testContext.subject.observeCurrentConnectionMode(owner: self, handler: { _ in - receivedConnectionModeChanged = true - }) - } - it("registers a ConnectionModeChanged observer") { + it("observeConnectionModeChanged") { + testContext.subject.observeCurrentConnectionMode(owner: self) { _ in callCount += 1 } let receivedObserver = mockNotifier.addConnectionModeChangedObserverReceivedObserver expect(mockNotifier.addConnectionModeChangedObserverCallCount) == 1 expect(receivedObserver?.owner) === self receivedObserver?.connectionModeChangedHandler?(ConnectionInformation.ConnectionMode.offline) - expect(receivedConnectionModeChanged) == true - } - } - - describe("observeError") { - var testContext: TestContext! - var receivedError: Bool = false - beforeEach { - waitUntil { done in - testContext = TestContext(completion: done) - } - - testContext.subject.observeError(owner: self, handler: { _ in - receivedError = true - }) + expect(callCount) == 1 } - it("registers an error observer") { + it("observeError") { + testContext.subject.observeError(owner: self) { _ in callCount += 1 } expect(testContext.errorNotifierMock.addErrorObserverCallCount) == 1 expect(testContext.errorNotifierMock.addErrorObserverReceivedObserver?.owner) === self testContext.errorNotifierMock.addErrorObserverReceivedObserver?.errorHandler?(ErrorMock()) - expect(receivedError) == true + expect(callCount) == 1 } - } - - describe("stopObserving") { - let mockFlagNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() as! FlagChangeNotifyingMock - var testContext: TestContext! - beforeEach { - waitUntil { done in - testContext = TestContext(completion: done) - } - - testContext.subject.flagChangeNotifier = mockFlagNotifier + it("stopObserving") { testContext.subject.stopObserving(owner: self) - } - it("unregisters the owner") { - expect(mockFlagNotifier.removeObserverCallCount) == 1 - expect(mockFlagNotifier.removeObserverReceivedArguments?.owner) === self + expect(mockNotifier.removeObserverCallCount) == 1 + expect(mockNotifier.removeObserverReceivedOwner) === self expect(testContext.errorNotifierMock.removeObserversCallCount) == 1 expect(testContext.errorNotifierMock.removeObserversReceivedOwner) === self } @@ -1569,478 +1224,182 @@ final class LDClientSpec: QuickSpec { } } - /* The concept of the onSyncCompleteSuccess tests is to configure the flags & mocks to simulate the intended change, prep the callbacks to trigger done() to end the async wait, and then call onFlagSyncComplete with the parameters for the area under test. onFlagSyncComplete will call a flagStore method which has an async closure, and so the test has to trigger that closure to get the correct code to execute in onFlagSyncComplete. Once the async flagStore closure runs for the appropriate update method, the result can be measured in the mocks. While setting up each test is slightly different, measuring the result is largely the same. - */ private func onSyncCompleteSuccessReplacingFlagsSpec(streamingMode: LDStreamingMode, eventType: FlagUpdateType? = nil) { var testContext: TestContext! var newFlags: [LDFlagKey: FeatureFlag]! var updateDate: Date! beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, completion: done) - } + testContext = TestContext(startOnline: true) + testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - } - context("flags have different values") { - beforeEach { - let newBoolFeatureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.bool, useAlternateValue: true) - newFlags = testContext.flagStoreMock.featureFlags - newFlags[DarklyServiceMock.FlagKeys.bool] = newBoolFeatureFlag - testContext.setFlagStoreCallbackToMimicRealFlagStore(newFlags: newFlags) + newFlags = FlagMaintainingMock.stubFlags() + newFlags[Constants.newFlagKey] = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.string, useAlternateValue: true) - waitUntil { done in - testContext.changeNotifierMock.notifyObserversCallback = done - updateDate = Date() - testContext.onSyncComplete?(.success(newFlags, eventType)) - } - } - it("updates the flag store") { - expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags == newFlags).to(beTrue()) - } - it("caches the new flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.user) == testContext.user - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated.isWithin(Constants.updateThreshold, of: updateDate)) == true - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async - } - it("informs the flag change notifier of the changed flags") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.flagStore.featureFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == testContext.oldFlags).to(beTrue()) + waitUntil { done in + testContext.changeNotifierMock.notifyObserversCallback = done + updateDate = Date() + testContext.onSyncComplete?(.success(newFlags, eventType)) } } - context("a flag was added") { - beforeEach { - newFlags = testContext.flagStoreMock.featureFlags - newFlags[Constants.newFlagKey] = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.string, useAlternateValue: true) - testContext.setFlagStoreCallbackToMimicRealFlagStore(newFlags: newFlags) - - waitUntil { done in - testContext.changeNotifierMock.notifyObserversCallback = done - updateDate = Date() - testContext.onSyncComplete?(.success(newFlags, eventType)) - } - } - it("updates the flag store") { - expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags == newFlags).to(beTrue()) - } - it("caches the new flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.user) == testContext.user - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated.isWithin(Constants.updateThreshold, of: updateDate)) == true - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async - } - it("informs the flag change notifier of the changed flags") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.flagStore.featureFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == testContext.oldFlags).to(beTrue()) - } + it("updates the flag store") { + expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 + expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags == newFlags).to(beTrue()) } - context("a flag was removed") { - beforeEach { - newFlags = testContext.flagStoreMock.featureFlags - newFlags.removeValue(forKey: DarklyServiceMock.FlagKeys.dictionary) - testContext.setFlagStoreCallbackToMimicRealFlagStore(newFlags: newFlags) - - waitUntil { done in - testContext.changeNotifierMock.notifyObserversCallback = done - updateDate = Date() - testContext.onSyncComplete?(.success(newFlags, eventType)) - } - } - it("updates the flag store") { - expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags == newFlags).to(beTrue()) - } - it("caches the new flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.user) == testContext.user - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated.isWithin(Constants.updateThreshold, of: updateDate)) == true - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async - } - it("informs the flag change notifier of the changed flags") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.flagStore.featureFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == testContext.oldFlags).to(beTrue()) - } + it("caches the new flags") { + expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.user) == testContext.user + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated.isWithin(Constants.updateThreshold, of: updateDate)) == true + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async } - context("there were no changes to the flags") { - beforeEach { - newFlags = testContext.flagStoreMock.featureFlags - testContext.setFlagStoreCallbackToMimicRealFlagStore(newFlags: newFlags) - - testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - waitUntil { done in - testContext.changeNotifierMock.notifyObserversCallback = done - updateDate = Date() - testContext.onSyncComplete?(.success(newFlags, eventType)) - } - } - it("updates the flag store") { - expect(testContext.flagStoreMock.replaceStoreCallCount) == 1 - expect(testContext.flagStoreMock.replaceStoreReceivedArguments?.newFlags == newFlags).to(beTrue()) - } - it("caches the new flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.user) == testContext.user - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated.isWithin(Constants.updateThreshold, of: updateDate)) == true - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async - } - it("informs the flag change notifier of the unchanged flags") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.flagStore.featureFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == testContext.oldFlags).to(beTrue()) - } + it("informs the flag change notifier of the changed flags") { + expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.flagStore.featureFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == testContext.cachedFlags).to(beTrue()) } } func onSyncCompleteStreamingPatchSpec() { var testContext: TestContext! var flagUpdateDictionary: [String: Any]! - var oldFlags: [LDFlagKey: FeatureFlag]! - var newFlags: [LDFlagKey: FeatureFlag]! var updateDate: Date! - + let stubFlags = FlagMaintainingMock.stubFlags() beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, completion: done) - } + testContext = TestContext(startOnline: true).withCached(flags: stubFlags) + testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - } - - context("update changes flags") { - beforeEach { - oldFlags = testContext.flagStoreMock.featureFlags - flagUpdateDictionary = FlagMaintainingMock.stubPatchDictionary(key: DarklyServiceMock.FlagKeys.int, - value: DarklyServiceMock.FlagValues.int + 1, - variation: DarklyServiceMock.Constants.variation + 1, - version: DarklyServiceMock.Constants.version + 1) - let newIntFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.int, useAlternateValue: true) - newFlags = oldFlags - newFlags[DarklyServiceMock.FlagKeys.int] = newIntFlag - testContext.setFlagStoreCallbackToMimicRealFlagStore(newFlags: newFlags) + flagUpdateDictionary = FlagMaintainingMock.stubPatchDictionary(key: DarklyServiceMock.FlagKeys.int, + value: DarklyServiceMock.FlagValues.int + 1, + variation: DarklyServiceMock.Constants.variation + 1, + version: DarklyServiceMock.Constants.version + 1) - waitUntil { done in - testContext.changeNotifierMock.notifyObserversCallback = done - updateDate = Date() - testContext.onSyncComplete?(.success(flagUpdateDictionary, .patch)) - } - } - it("updates the flag store") { - expect(testContext.flagStoreMock.updateStoreCallCount) == 1 - expect(testContext.flagStoreMock.updateStoreReceivedArguments?.updateDictionary == flagUpdateDictionary).to(beTrue()) - } - it("caches the updated flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.user) == testContext.user - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated.isWithin(Constants.updateThreshold, of: updateDate)) == true - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async - } - it("informs the flag change notifier of the changed flag") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.flagStore.featureFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == oldFlags).to(beTrue()) + waitUntil { done in + testContext.changeNotifierMock.notifyObserversCallback = done + updateDate = Date() + testContext.onSyncComplete?(.success(flagUpdateDictionary, .patch)) } } - context("update does not change flags") { - beforeEach { - oldFlags = testContext.flagStoreMock.featureFlags - flagUpdateDictionary = FlagMaintainingMock.stubPatchDictionary(key: DarklyServiceMock.FlagKeys.int, - value: DarklyServiceMock.FlagValues.int + 1, - variation: DarklyServiceMock.Constants.variation, - version: DarklyServiceMock.Constants.version) - newFlags = oldFlags - testContext.setFlagStoreCallbackToMimicRealFlagStore(newFlags: newFlags) - - waitUntil { done in - testContext.changeNotifierMock.notifyObserversCallback = done - updateDate = Date() - testContext.onSyncComplete?(.success(flagUpdateDictionary, .patch)) - } - } - it("updates the flag store") { - expect(testContext.flagStoreMock.updateStoreCallCount) == 1 - expect(testContext.flagStoreMock.updateStoreReceivedArguments?.updateDictionary == flagUpdateDictionary).to(beTrue()) - } - it("caches the updated flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.user) == testContext.user - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated.isWithin(Constants.updateThreshold, of: updateDate)) == true - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async - } - it("informs the flag change notifier of the unchanged flag") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.flagStore.featureFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == oldFlags).to(beTrue()) - } + it("updates the flag store") { + expect(testContext.flagStoreMock.updateStoreCallCount) == 1 + expect(testContext.flagStoreMock.updateStoreReceivedArguments?.updateDictionary == flagUpdateDictionary).to(beTrue()) + } + it("caches the updated flags") { + expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.user) == testContext.user + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated.isWithin(Constants.updateThreshold, of: updateDate)) == true + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async + } + it("informs the flag change notifier of the changed flag") { + expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.flagStore.featureFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags).to(beTrue()) } } func onSyncCompleteDeleteFlagSpec() { var testContext: TestContext! var flagUpdateDictionary: [String: Any]! - var oldFlags: [LDFlagKey: FeatureFlag]! - var newFlags: [LDFlagKey: FeatureFlag]! var updateDate: Date! - + let stubFlags = FlagMaintainingMock.stubFlags() beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, completion: done) - } + testContext = TestContext(startOnline: true).withCached(flags: stubFlags) + testContext.start() testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - } - - context("delete changes flags") { - beforeEach { - oldFlags = testContext.flagStoreMock.featureFlags - flagUpdateDictionary = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) - newFlags = oldFlags - newFlags.removeValue(forKey: DarklyServiceMock.FlagKeys.int) - testContext.setFlagStoreCallbackToMimicRealFlagStore(newFlags: newFlags) + flagUpdateDictionary = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version + 1) - waitUntil { done in - testContext.changeNotifierMock.notifyObserversCallback = done - updateDate = Date() - testContext.onSyncComplete?(.success(flagUpdateDictionary, .delete)) - } - } - it("updates the flag store") { - expect(testContext.flagStoreMock.deleteFlagCallCount) == 1 - expect(testContext.flagStoreMock.deleteFlagReceivedArguments?.deleteDictionary == flagUpdateDictionary).to(beTrue()) - } - it("caches the updated flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == newFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.user) == testContext.user - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated.isWithin(Constants.updateThreshold, of: updateDate)) == true - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async - } - it("informs the flag change notifier of the changed flag") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.flagStore.featureFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == oldFlags).to(beTrue()) + waitUntil { done in + testContext.changeNotifierMock.notifyObserversCallback = done + updateDate = Date() + testContext.onSyncComplete?(.success(flagUpdateDictionary, .delete)) } } - context("delete does not change flags") { - beforeEach { - oldFlags = testContext.flagStoreMock.featureFlags - flagUpdateDictionary = FlagMaintainingMock.stubDeleteDictionary(key: DarklyServiceMock.FlagKeys.int, version: DarklyServiceMock.Constants.version) - testContext.setFlagStoreCallbackToMimicRealFlagStore(newFlags: oldFlags) - - waitUntil { done in - testContext.changeNotifierMock.notifyObserversCallback = done - updateDate = Date() - testContext.onSyncComplete?(.success(flagUpdateDictionary, .delete)) - } - } - it("updates the flag store") { - expect(testContext.flagStoreMock.deleteFlagCallCount) == 1 - expect(testContext.flagStoreMock.deleteFlagReceivedArguments?.deleteDictionary == flagUpdateDictionary).to(beTrue()) - } - it("caches the updated flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == oldFlags - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.user) == testContext.user - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated.isWithin(Constants.updateThreshold, of: updateDate)) == true - expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async - } - it("informs the flag change notifier of the unchanged flag") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.flagStore.featureFlags) == testContext.flagStoreMock.featureFlags - expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == oldFlags).to(beTrue()) - } + it("updates the flag store") { + expect(testContext.flagStoreMock.deleteFlagCallCount) == 1 + expect(testContext.flagStoreMock.deleteFlagReceivedArguments?.deleteDictionary == flagUpdateDictionary).to(beTrue()) + } + it("caches the updated flags") { + expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 1 + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.featureFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.user) == testContext.user + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.mobileKey) == testContext.config.mobileKey + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.lastUpdated.isWithin(Constants.updateThreshold, of: updateDate)) == true + expect(testContext.featureFlagCachingMock.storeFeatureFlagsReceivedArguments?.storeMode) == .async + } + it("informs the flag change notifier of the changed flag") { + expect(testContext.changeNotifierMock.notifyObserversCallCount) == 1 + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.flagStore.featureFlags) == testContext.flagStoreMock.featureFlags + expect(testContext.changeNotifierMock.notifyObserversReceivedArguments?.oldFlags == stubFlags).to(beTrue()) } } func onSyncCompleteErrorSpec() { - var testContext: TestContext! - beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, completion: done) - } - } - - context("there was an internal server error") { - beforeEach { - testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - waitUntil { done in - testContext.errorNotifierMock.notifyObserversCallback = { - done() + func runTest(_ ctx: String, _ err: SynchronizingError, testError: @escaping ((SynchronizingError) -> Void)) { + var testContext: TestContext! + context(ctx) { + beforeEach { + waitUntil { done in + testContext = TestContext(startOnline: true) + testContext.start() + testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() + testContext.errorNotifierMock.notifyObserversCallback = done + testContext.onSyncComplete?(.error(err)) } - - testContext.onSyncComplete?(.error(.response(HTTPURLResponse(url: testContext.config.baseUrl, - statusCode: HTTPURLResponse.StatusCodes.internalServerError, - httpVersion: DarklyServiceMock.Constants.httpVersion, - headerFields: nil)))) - } - } - it("does not take the client offline") { - expect(testContext.subject.isOnline) == true - } - it("does not cache the users flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 0 - } - it("does not call the flag change notifier") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 0 - } - it("calls the errorNotifier with a .response SynchronizingError") { - expect(testContext.errorNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.observedError as? SynchronizingError).toNot(beNil()) - guard case .response(let urlResponse)? = testContext.observedError as? SynchronizingError, - let httpUrlResponse = urlResponse as? HTTPURLResponse - else { - fail("unexpected error reported") - return } - expect(httpUrlResponse.statusCode) == HTTPURLResponse.StatusCodes.internalServerError - } - } - context("there was a request error") { - beforeEach { - testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - waitUntil { done in - testContext.errorNotifierMock.notifyObserversCallback = { - done() - } - - testContext.onSyncComplete?(.error(.request(DarklyServiceMock.Constants.error))) + it("takes the client offline when unauthed") { + expect(testContext.subject.isOnline) == !err.isClientUnauthorized } - } - it("does not take the client offline") { - expect(testContext.subject.isOnline) == true - } - it("does not cache the users flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 0 - } - it("does not call the flag change notifier") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 0 - } - it("calls the errorNotifier with a .request SynchronizingError") { - expect(testContext.errorNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.observedError as? SynchronizingError).toNot(beNil()) - guard case .request(let error as NSError)? = testContext.observedError as? SynchronizingError - else { - fail("unexpected error reported") - return + it("does not cache the users flags") { + expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 0 } - expect(error.code) == Int(CFNetworkErrors.cfurlErrorResourceUnavailable.rawValue) - } - } - context("there was a data error") { - beforeEach { - testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - waitUntil { done in - testContext.errorNotifierMock.notifyObserversCallback = { - done() - } - - testContext.onSyncComplete?(.error(.data(DarklyServiceMock.Constants.errorData))) + it("does not call the flag change notifier") { + expect(testContext.changeNotifierMock.notifyObserversCallCount) == 0 } - } - it("does not take the client offline") { - expect(testContext.subject.isOnline) == true - } - it("does not cache the users flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 0 - } - it("does not call the flag change notifier") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 0 - } - it("calls the errorNotifier with a .data SynchronizingError") { - expect(testContext.errorNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.observedError as? SynchronizingError).toNot(beNil()) - guard case .data(let data)? = testContext.observedError as? SynchronizingError, - let errorData = data - else { - fail("unexpected error reported") - return + it("informs the error notifier") { + expect(testContext.errorNotifierMock.notifyObserversCallCount) == 1 + expect(testContext.observedError).to(beAnInstanceOf(SynchronizingError.self)) + if let err = testContext.observedError as? SynchronizingError { testError(err) } } - expect(errorData) == DarklyServiceMock.Constants.errorData } } - context("there was a client unauthorized error") { - beforeEach { - testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - waitUntil { done in - testContext.errorNotifierMock.notifyObserversCallback = { - done() - } - testContext.onSyncComplete?(.error(.response(HTTPURLResponse(url: testContext.config.baseUrl, - statusCode: HTTPURLResponse.StatusCodes.unauthorized, - httpVersion: DarklyServiceMock.Constants.httpVersion, - headerFields: nil)))) - } - } - it("takes the client offline") { - expect(testContext.subject.isOnline) == false - } - it("does not cache the users flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 0 - } - it("does not call the flag change notifier") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 0 - } - it("calls the errorNotifier with a .response SynchronizingError") { - expect(testContext.errorNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.observedError as? SynchronizingError).toNot(beNil()) - guard case .response(let urlResponse)? = testContext.observedError as? SynchronizingError, - let httpUrlResponse = urlResponse as? HTTPURLResponse - else { - fail("unexpected error reported") - return - } - expect(httpUrlResponse.statusCode) == HTTPURLResponse.StatusCodes.unauthorized - } + let serverError = HTTPURLResponse(url: DarklyServiceMock.Constants.mockBaseUrl, + statusCode: HTTPURLResponse.StatusCodes.internalServerError, + httpVersion: DarklyServiceMock.Constants.httpVersion, + headerFields: nil) + runTest("there was an internal server error", .response(serverError)) { error in + if case .response(let urlResponse as HTTPURLResponse) = error { + expect(urlResponse).to(beIdenticalTo(serverError)) + } else { fail("Incorrect error given to error notifier") } } - context("there was a non-NSError error") { - beforeEach { - testContext.subject.flagChangeNotifier = ClientServiceMockFactory().makeFlagChangeNotifier() - waitUntil { done in - testContext.errorNotifierMock.notifyObserversCallback = { - done() - } - testContext.onSyncComplete?(.error(.streamError(DummyError()))) - } - } - it("does not take the client offline") { - expect(testContext.subject.isOnline) == true - } - it("does not cache the users flags") { - expect(testContext.featureFlagCachingMock.storeFeatureFlagsCallCount) == 0 - } - it("does not call the flag change notifier") { - expect(testContext.changeNotifierMock.notifyObserversCallCount) == 0 - } - it("calls the errorNotifier with a .streamError SynchronizingError") { - expect(testContext.errorNotifierMock.notifyObserversCallCount) == 1 - expect(testContext.observedError as? SynchronizingError).toNot(beNil()) - guard case .streamError(let error)? = testContext.observedError as? SynchronizingError - else { - fail("unexpected error reported") - return - } - expect(error is DummyError).to(beTrue()) - } + let unauthedError = HTTPURLResponse(url: DarklyServiceMock.Constants.mockBaseUrl, + statusCode: HTTPURLResponse.StatusCodes.unauthorized, + httpVersion: DarklyServiceMock.Constants.httpVersion, + headerFields: nil) + runTest("there was a client unauthorized error", .response(unauthedError)) { error in + if case .response(let urlResponse as HTTPURLResponse) = error { + expect(urlResponse).to(beIdenticalTo(unauthedError)) + } else { fail("Incorrect error given to error notifier") } + } + runTest("there was a request error", .request(DarklyServiceMock.Constants.error)) { error in + if case .request(let nsError as NSError) = error { + expect(nsError).to(beIdenticalTo(DarklyServiceMock.Constants.error)) + } else { fail("Incorrect error given to error notifier") } + } + runTest("there was a data error", .data(DarklyServiceMock.Constants.errorData)) { error in + if case .data(let data) = error { + expect(data) == DarklyServiceMock.Constants.errorData + } else { fail("Incorrect error given to error notifier") } + } + runTest("there was a non-NSError error", .streamError(DummyError())) { error in + if case .streamError(let dummy) = error { + expect(dummy is DummyError).to(beTrue()) + } else { fail("Incorrect error given to error notifier") } } } @@ -2054,9 +1413,8 @@ final class LDClientSpec: QuickSpec { context("on \(os)") { context("background updates disabled") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, runMode: .foreground, operatingSystem: os, completion: done) - } + testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, operatingSystem: os) + testContext.start() NotificationCenter.default.post(name: testContext.environmentReporterMock.backgroundNotification!, object: self) expect(testContext.subject.runMode).toEventually(equal(LDClientRunMode.background)) } @@ -2069,9 +1427,8 @@ final class LDClientSpec: QuickSpec { } context("background updates enabled") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, enableBackgroundUpdates: true, runMode: .foreground, operatingSystem: os, completion: done) - } + testContext = TestContext(startOnline: true, operatingSystem: os) + testContext.start() waitUntil { done in NotificationCenter.default.post(name: testContext.environmentReporterMock.backgroundNotification!, object: self) @@ -2091,8 +1448,8 @@ final class LDClientSpec: QuickSpec { } context("when offline") { beforeEach { - testContext = TestContext(startOnline: false, runMode: .foreground) - + testContext = TestContext() + testContext.start() NotificationCenter.default.post(name: testContext.environmentReporterMock.backgroundNotification!, object: self) } it("leaves the sdk offline") { @@ -2111,10 +1468,8 @@ final class LDClientSpec: QuickSpec { context("on \(os)") { context("when online at foreground notification") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, runMode: .background, operatingSystem: os, completion: done) - } - + testContext = TestContext(startOnline: true, operatingSystem: os) + testContext.start(runMode: .background) NotificationCenter.default.post(name: testContext.environmentReporterMock.foregroundNotification!, object: self) } it("takes the sdk online") { @@ -2126,7 +1481,8 @@ final class LDClientSpec: QuickSpec { } context("when offline at foreground notification") { beforeEach { - testContext = TestContext(startOnline: false, runMode: .background, operatingSystem: os) + testContext = TestContext(operatingSystem: os) + testContext.start(runMode: .background) NotificationCenter.default.post(name: testContext.environmentReporterMock.foregroundNotification!, object: self) } @@ -2149,9 +1505,8 @@ final class LDClientSpec: QuickSpec { context("with background updates enabled") { context("streaming mode") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, streamingMode: .streaming, enableBackgroundUpdates: true, runMode: .foreground, operatingSystem: .macOS, completion: done) - } + testContext = TestContext(startOnline: true, operatingSystem: .macOS) + testContext.start() testContext.subject.setRunMode(.background) } it("leaves the event reporter online") { @@ -2164,9 +1519,8 @@ final class LDClientSpec: QuickSpec { } context("polling mode") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, streamingMode: .polling, enableBackgroundUpdates: true, runMode: .foreground, operatingSystem: .macOS, completion: done) - } + testContext = TestContext(startOnline: true, streamingMode: .polling, operatingSystem: .macOS) + testContext.start() testContext.subject.setRunMode(.background) } it("leaves the event reporter online") { @@ -2181,9 +1535,8 @@ final class LDClientSpec: QuickSpec { } context("with background updates disabled") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, runMode: .foreground, operatingSystem: .macOS, completion: done) - } + testContext = TestContext(startOnline: true, enableBackgroundUpdates: false, operatingSystem: .macOS) + testContext.start() testContext.subject.setRunMode(.background) } it("leaves the event reporter online") { @@ -2201,9 +1554,8 @@ final class LDClientSpec: QuickSpec { var flagSynchronizerIsOnlineSetCount: Int! var makeFlagSynchronizerCallCount: Int! beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, runMode: .foreground, operatingSystem: .macOS, completion: done) - } + testContext = TestContext(startOnline: true, operatingSystem: .macOS) + testContext.start() eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount flagSynchronizerIsOnlineSetCount = testContext.flagSynchronizerMock.isOnlineSetCount makeFlagSynchronizerCallCount = testContext.serviceFactoryMock.makeFlagSynchronizerCallCount @@ -2224,9 +1576,9 @@ final class LDClientSpec: QuickSpec { var flagSynchronizerIsOnlineSetCount: Int! var makeFlagSynchronizerCallCount: Int! beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, enableBackgroundUpdates: true, runMode: .background, operatingSystem: .macOS, completion: done) - } + testContext = TestContext(startOnline: true, operatingSystem: .macOS) + testContext.start() + testContext.subject.setRunMode(.background) eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount flagSynchronizerIsOnlineSetCount = testContext.flagSynchronizerMock.isOnlineSetCount makeFlagSynchronizerCallCount = testContext.serviceFactoryMock.makeFlagSynchronizerCallCount @@ -2243,9 +1595,8 @@ final class LDClientSpec: QuickSpec { context("set foreground") { context("streaming mode") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, streamingMode: .streaming, runMode: .background, operatingSystem: .macOS, completion: done) - } + testContext = TestContext(startOnline: true, operatingSystem: .macOS) + testContext.start(runMode: .background) testContext.subject.setRunMode(.foreground) } it("takes the event reporter online") { @@ -2258,9 +1609,8 @@ final class LDClientSpec: QuickSpec { } context("polling mode") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, streamingMode: .polling, runMode: .background, operatingSystem: .macOS, completion: done) - } + testContext = TestContext(startOnline: true, streamingMode: .polling, operatingSystem: .macOS) + testContext.start(runMode: .background) testContext.subject.setRunMode(.foreground) } it("takes the event reporter online") { @@ -2281,7 +1631,8 @@ final class LDClientSpec: QuickSpec { context("with background updates enabled") { beforeEach { waitUntil { done in - testContext = TestContext(startOnline: false, enableBackgroundUpdates: true, runMode: .foreground, operatingSystem: .macOS, completion: done) + testContext = TestContext(operatingSystem: .macOS) + testContext.start(completion: done) } testContext.subject.setRunMode(.background) } @@ -2296,7 +1647,8 @@ final class LDClientSpec: QuickSpec { context("with background updates disabled") { beforeEach { waitUntil { done in - testContext = TestContext(startOnline: false, enableBackgroundUpdates: false, runMode: .foreground, operatingSystem: .macOS, completion: done) + testContext = TestContext(enableBackgroundUpdates: false, operatingSystem: .macOS) + testContext.start(completion: done) } testContext.subject.setRunMode(.background) } @@ -2316,7 +1668,8 @@ final class LDClientSpec: QuickSpec { var makeFlagSynchronizerCallCount: Int! beforeEach { waitUntil { done in - testContext = TestContext(startOnline: false, runMode: .foreground, operatingSystem: .macOS, completion: done) + testContext = TestContext(operatingSystem: .macOS) + testContext.start(completion: done) } eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount @@ -2340,7 +1693,8 @@ final class LDClientSpec: QuickSpec { var makeFlagSynchronizerCallCount: Int! beforeEach { waitUntil { done in - testContext = TestContext(startOnline: false, runMode: .background, operatingSystem: .macOS, completion: done) + testContext = TestContext(operatingSystem: .macOS) + testContext.start(runMode: .background, completion: done) } eventReporterIsOnlineSetCount = testContext.eventReporterMock.isOnlineSetCount @@ -2360,7 +1714,8 @@ final class LDClientSpec: QuickSpec { context("streaming mode") { beforeEach { waitUntil { done in - testContext = TestContext(startOnline: false, streamingMode: .streaming, runMode: .background, operatingSystem: .macOS, completion: done) + testContext = TestContext(operatingSystem: .macOS) + testContext.start(runMode: .background, completion: done) } testContext.subject.setRunMode(.foreground) } @@ -2375,7 +1730,8 @@ final class LDClientSpec: QuickSpec { context("polling mode") { beforeEach { waitUntil { done in - testContext = TestContext(startOnline: false, streamingMode: .polling, runMode: .background, operatingSystem: .macOS, completion: done) + testContext = TestContext(streamingMode: .polling, operatingSystem: .macOS) + testContext.start(runMode: .background, completion: done) } testContext.subject.setRunMode(.foreground) } @@ -2399,55 +1755,40 @@ final class LDClientSpec: QuickSpec { describe("flag synchronizer streaming mode") { OperatingSystem.allOperatingSystems.forEach { os in - context("when running on \(os)") { - beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, runMode: .foreground, operatingSystem: os, completion: done) - } - } - it("sets the flag synchronizer streaming mode") { - expect(testContext.makeFlagSynchronizerStreamingMode) == (os.isStreamingEnabled ? LDStreamingMode.streaming : LDStreamingMode.polling) - } + it("on \(os) sets the flag synchronizer streaming mode") { + testContext = TestContext(startOnline: true, operatingSystem: os) + testContext.start() + expect(testContext.makeFlagSynchronizerStreamingMode) == (os.isStreamingEnabled ? LDStreamingMode.streaming : LDStreamingMode.polling) } } } } private func flushSpec() { - var testContext: TestContext! - describe("flush") { - beforeEach { - waitUntil { done in - testContext = TestContext(completion: done) - } - testContext.subject.flush() - } it("tells the event reporter to report events") { + let testContext = TestContext() + testContext.start() + testContext.subject.flush() expect(testContext.eventReporterMock.flushCallCount) == 1 } } } - private func allFlagValuesSpec() { + private func allFlagsSpec() { + let stubFlags = FlagMaintainingMock.stubFlags() var testContext: TestContext! - var featureFlagValues: [LDFlagKey: Any]? - describe("allFlagValues") { - context("when client was started") { - var featureFlags: [LDFlagKey: FeatureFlag]! - beforeEach { - waitUntil { done in - testContext = TestContext(completion: done) - } - featureFlags = testContext.subject.flagStore.featureFlags - featureFlagValues = testContext.subject.allFlags - } - it("returns a matching dictionary of flag keys and values") { - expect(featureFlagValues?.count) == featureFlags.count - 1 //nil is omitted - featureFlags.keys.forEach { flagKey in - expect(AnyComparer.isEqual(featureFlagValues?[flagKey], to: featureFlags[flagKey]?.value)).to(beTrue()) - } - } + describe("allFlags") { + beforeEach { + testContext = TestContext().withCached(flags: stubFlags) + testContext.start() + } + it("returns all non-null flag values from store") { + expect(AnyComparer.isEqual(testContext.subject.allFlags, to: stubFlags.compactMapValues { $0.value })).to(beTrue()) + } + it("returns nil when client is closed") { + testContext.subject.close() + expect(testContext.subject.allFlags).to(beNil()) } } } @@ -2458,9 +1799,8 @@ final class LDClientSpec: QuickSpec { describe("ConnectionInformation") { context("when client was started in foreground") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, streamingMode: .streaming, runMode: .foreground, completion: done) - } + testContext = TestContext(startOnline: true) + testContext.start() } it("returns a ConnectionInformation object with currentConnectionMode.establishingStreamingConnection") { expect(testContext.subject.isOnline) == true @@ -2472,9 +1812,9 @@ final class LDClientSpec: QuickSpec { } context("when client was started in background") { beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, streamingMode: .streaming, runMode: .background, completion: done) - } + testContext = TestContext(startOnline: true, enableBackgroundUpdates: false) + testContext.start() + testContext.subject.setRunMode(.background) } it("returns a ConnectionInformation object with currentConnectionMode.offline") { expect(testContext.subject.connectionInformation.currentConnectionMode).to(equal(.offline)) @@ -2485,7 +1825,8 @@ final class LDClientSpec: QuickSpec { } context("when offline and client started") { beforeEach { - testContext = TestContext(startOnline: false) + testContext = TestContext() + testContext.start() } it("leaves the sdk offline") { expect(testContext.subject.isOnline) == false @@ -2494,29 +1835,16 @@ final class LDClientSpec: QuickSpec { expect(testContext.subject.connectionInformation.currentConnectionMode).to(equal(.offline)) } } - context("when client was not started") { - beforeEach { - testContext = TestContext() - } - it("returns nil") { - expect(testContext.subject.connectionInformation.currentConnectionMode).to(equal(.offline)) - } - } } } private func variationDetailSpec() { - var testContext: TestContext! - describe("variationDetail") { context("when client was started and flag key doesn't exist") { - beforeEach { - waitUntil { done in - testContext = TestContext(startOnline: true, streamingMode: .streaming, runMode: .foreground, completion: done) - } - } it("returns FLAG_NOT_FOUND") { - let detail = testContext.subject.variationDetail(forKey: BadFlagKeys.bool, defaultValue: DefaultFlagValues.bool).reason + let testContext = TestContext() + testContext.start() + let detail = testContext.subject.variationDetail(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultFlagValues.bool).reason if let errorKind = detail?["errorKind"] as? String { expect(errorKind) == "FLAG_NOT_FOUND" } @@ -2524,6 +1852,55 @@ final class LDClientSpec: QuickSpec { } } } + + private func isInitializedSpec() { + var testContext: TestContext! + + describe("isInitialized") { + context("when client was started but no flag update") { + beforeEach { + testContext = TestContext(startOnline: true) + testContext.start() + } + it("returns false") { + expect(testContext.subject.isInitialized) == false + } + it("and then stopped returns false") { + testContext.subject.close() + expect(testContext.subject.isInitialized) == false + } + } + context("when client was started offline") { + beforeEach { + testContext = TestContext() + testContext.start() + } + it("returns true") { + expect(testContext.subject.isInitialized) == true + } + it("and then stopped returns false") { + testContext.subject.close() + expect(testContext.subject.isInitialized) == false + } + } + for eventType in [nil, FlagUpdateType.ping, FlagUpdateType.put] { + context("when client was started and after receiving flags as " + (eventType?.rawValue ?? "poll")) { + beforeEach { + testContext = TestContext(startOnline: true) + testContext.start() + testContext.onSyncComplete?(.success([:], eventType)) + } + it("returns true") { + expect(testContext.subject.isInitialized).toEventually(beTrue(), timeout: DispatchTimeInterval.seconds(2)) + } + it("and then stopped returns false") { + testContext.subject.close() + expect(testContext.subject.isInitialized) == false + } + } + } + } + } } extension FeatureFlagCachingMock { diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift index 22bf8bc8..5b8fb6a7 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/ClientServiceMockFactory.swift @@ -67,8 +67,9 @@ final class ClientServiceMockFactory: ClientServiceCreating { makeFlagSynchronizer(streamingMode: streamingMode, pollingInterval: pollingInterval, useReport: useReport, service: service, onSyncComplete: nil) } + var makeFlagChangeNotifierReturnValue: FlagChangeNotifying = FlagChangeNotifyingMock() func makeFlagChangeNotifier() -> FlagChangeNotifying { - FlagChangeNotifyingMock() + return makeFlagChangeNotifierReturnValue } var makeEventReporterCallCount = 0 @@ -118,10 +119,10 @@ final class ClientServiceMockFactory: ClientServiceCreating { } var makeDiagnosticReporterCallCount = 0 - var makeDiagnosticReporterReceivedParameters: (service: DarklyServiceProvider, runMode: LDClientRunMode)? = nil - func makeDiagnosticReporter(service: DarklyServiceProvider, runMode: LDClientRunMode) -> DiagnosticReporting { + var makeDiagnosticReporterReceivedService: DarklyServiceProvider? = nil + func makeDiagnosticReporter(service: DarklyServiceProvider) -> DiagnosticReporting { makeDiagnosticReporterCallCount += 1 - makeDiagnosticReporterReceivedParameters = (service: service, runMode: runMode) + makeDiagnosticReporterReceivedService = service return DiagnosticReportingMock() } diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift index 7791899e..e2a35afb 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/DarklyServiceMock.swift @@ -124,16 +124,16 @@ final class DarklyServiceMock: DarklyServiceProvider { let flagKeys = includeNullValue ? FlagKeys.knownFlags : FlagKeys.flagsWithAnAlternateValue let featureFlagTuples = flagKeys.map { flagKey in - return (flagKey, stubFeatureFlag(for: flagKey, - includeVariation: includeVariations, - includeVersion: includeVersions, - includeFlagVersion: includeFlagVersions, - useAlternateValue: useAlternateValue(for: flagKey, alternateValueKeys: alternateValueKeys), - useAlternateVersion: bumpFlagVersions && useAlternateValue(for: flagKey, alternateValueKeys: alternateValueKeys), - useAlternateFlagVersion: bumpFlagVersions && useAlternateValue(for: flagKey, alternateValueKeys: alternateValueKeys), - useAlternateVariationNumber: alternateVariationNumber, - trackEvents: trackEvents, - debugEventsUntilDate: debugEventsUntilDate)) + (flagKey, stubFeatureFlag(for: flagKey, + includeVariation: includeVariations, + includeVersion: includeVersions, + includeFlagVersion: includeFlagVersions, + useAlternateValue: useAlternateValue(for: flagKey, alternateValueKeys: alternateValueKeys), + useAlternateVersion: bumpFlagVersions && useAlternateValue(for: flagKey, alternateValueKeys: alternateValueKeys), + useAlternateFlagVersion: bumpFlagVersions && useAlternateValue(for: flagKey, alternateValueKeys: alternateValueKeys), + useAlternateVariationNumber: alternateVariationNumber, + trackEvents: trackEvents, + debugEventsUntilDate: debugEventsUntilDate)) } return Dictionary(uniqueKeysWithValues: featureFlagTuples) @@ -270,9 +270,11 @@ final class DarklyServiceMock: DarklyServiceProvider { var stubbedDiagnosticResponse: ServiceResponse? var publishDiagnosticCallCount = 0 var publishedDiagnostic: DiagnosticEvent? + var publishDiagnosticCallback: (() -> Void)? func publishDiagnostic(diagnosticEvent: T, completion: ServiceCompletionHandler?) { publishDiagnosticCallCount += 1 publishedDiagnostic = diagnosticEvent + publishDiagnosticCallback?() completion?(stubbedDiagnosticResponse ?? (nil, nil, nil)) } } @@ -421,29 +423,6 @@ extension DarklyServiceMock { stubRequest(passingTest: eventRequestStubTest, stub: stubResponse, name: Constants.stubNameDiagnostic, onActivation: activate) } - ///Use when testing requires the mock service to provide a service response to the diagnostic request callback - func stubDiagnosticResponse(success: Bool, responseOnly: Bool = false, errorOnly: Bool = false) { - if success { - let response = HTTPURLResponse(url: config.eventsUrl, - statusCode: HTTPURLResponse.StatusCodes.accepted, - httpVersion: Constants.httpVersion, - headerFields: [:]) - stubbedDiagnosticResponse = (nil, response, nil) - return - } - - if responseOnly { - stubbedDiagnosticResponse = (nil, errorDiagnosticHTTPURLResponse, nil) - } else if errorOnly { - stubbedDiagnosticResponse = (nil, nil, Constants.error) - } else { - stubbedDiagnosticResponse = (nil, errorDiagnosticHTTPURLResponse, Constants.error) - } - } - var errorDiagnosticHTTPURLResponse: HTTPURLResponse! { - HTTPURLResponse(url: config.eventsUrl, statusCode: HTTPURLResponse.StatusCodes.internalServerError, httpVersion: Constants.httpVersion, headerFields: nil) - } - // MARK: Stub var anyRequestStubTest: HTTPStubsTestBlock { { _ in true } } @@ -491,12 +470,3 @@ extension HTTPURLResponse { return [HTTPURLResponse.HeaderKeys.date: DateFormatter.httpUrlHeaderFormatter.string(from: date)] } } - -extension LDFlagKey { - var isKnownFlagKey: Bool { - DarklyServiceMock.FlagKeys.knownFlags.contains(self) - } - var hasAlternateValue: Bool { - DarklyServiceMock.FlagKeys.flagsWithAnAlternateValue.contains(self) - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagChangeNotifyingMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/FlagChangeNotifyingMock.swift deleted file mode 100644 index 28375064..00000000 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagChangeNotifyingMock.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// FlagChangeNotifyingMock.swift -// LaunchDarklyTests -// -// Copyright © 2017 Catamorphic Co. All rights reserved. -// - -import Foundation -@testable import LaunchDarkly - -extension FlagChangeNotifyingMock { - func removeObserver(_ key: String, owner: LDObserverOwner) { - removeObserverCallCount += 1 - removeObserverReceivedArguments = ([key], owner) - } - - func removeObserver(owner: LDObserverOwner) { - removeObserverCallCount += 1 - removeObserverReceivedArguments = ([], owner) - } -} diff --git a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift b/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift index f3cf035a..7f0b4503 100644 --- a/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift +++ b/LaunchDarkly/LaunchDarklyTests/Mocks/FlagMaintainingMock.swift @@ -8,24 +8,52 @@ import Foundation @testable import LaunchDarkly -extension FlagMaintainingMock { +final class FlagMaintainingMock: FlagMaintaining { struct Constants { static let updateDictionaryExtraKey = "FlagMaintainingMock.UpdateDictionary.extraKey" static let updateDictionaryExtraValue = "FlagMaintainingMock.UpdateDictionary.extraValue" } - convenience init(flags: [LDFlagKey: FeatureFlag]) { - self.init() - featureFlags = flags - featureFlagsSetCount = 0 + let innerStore: FlagStore + + init() { + innerStore = FlagStore() } - func featureFlag(for flagKey: LDFlagKey) -> FeatureFlag? { - featureFlags[flagKey] + init(flags: [LDFlagKey: FeatureFlag]) { + innerStore = FlagStore(featureFlags: flags) + } + + var featureFlags: [LDFlagKey: FeatureFlag] { + innerStore.featureFlags + } + + var replaceStoreCallCount = 0 + var replaceStoreReceivedArguments: (newFlags: [LDFlagKey: Any], completion: CompletionClosure?)? + func replaceStore(newFlags: [LDFlagKey: Any], completion: CompletionClosure?) { + replaceStoreCallCount += 1 + replaceStoreReceivedArguments = (newFlags: newFlags, completion: completion) + innerStore.replaceStore(newFlags: newFlags, completion: completion) } - func variation(forKey key: String, defaultValue: T) -> T { - featureFlags[key]?.value as? T ?? defaultValue + var updateStoreCallCount = 0 + var updateStoreReceivedArguments: (updateDictionary: [String: Any], completion: CompletionClosure?)? + func updateStore(updateDictionary: [String: Any], completion: CompletionClosure?) { + updateStoreCallCount += 1 + updateStoreReceivedArguments = (updateDictionary: updateDictionary, completion: completion) + innerStore.updateStore(updateDictionary: updateDictionary, completion: completion) + } + + var deleteFlagCallCount = 0 + var deleteFlagReceivedArguments: (deleteDictionary: [String: Any], completion: CompletionClosure?)? + func deleteFlag(deleteDictionary: [String: Any], completion: CompletionClosure?) { + deleteFlagCallCount += 1 + deleteFlagReceivedArguments = (deleteDictionary: deleteDictionary, completion: completion) + innerStore.deleteFlag(deleteDictionary: deleteDictionary, completion: completion) + } + + func featureFlag(for flagKey: LDFlagKey) -> FeatureFlag? { + innerStore.featureFlag(for: flagKey) } static func stubPatchDictionary(key: LDFlagKey?, value: Any?, variation: Int?, version: Int?, includeExtraKey: Bool = false) -> [String: Any] { @@ -59,7 +87,6 @@ extension FlagMaintainingMock { return deleteDictionary } - static func stubFlags(includeNullValue: Bool = true, includeVersions: Bool = true) -> [String: FeatureFlag] { var flags = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: includeNullValue, includeVersions: includeVersions) flags["userKey"] = FeatureFlag(flagKey: "userKey", diff --git a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift index 0697e148..2023bab8 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/DiagnosticEventSpec.swift @@ -275,7 +275,8 @@ final class DiagnosticEventSpec: QuickSpec { context("using \(desc) encoding") { it("encodes correct values to keys") { let decoded = self.loadAndRestoreRaw(scheme, diagnosticConfig) - expect(decoded.count) == 18 + expect(decoded.count) == 19 + expect((decoded["autoAliasingOptOut"] as! Bool)) == diagnosticConfig.autoAliasingOptOut expect((decoded["customBaseURI"] as! Bool)) == diagnosticConfig.customBaseURI expect((decoded["customEventsURI"] as! Bool)) == diagnosticConfig.customEventsURI expect((decoded["customStreamURI"] as! Bool)) == diagnosticConfig.customStreamURI diff --git a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift index 00e83cf2..4162646f 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/EventSpec.swift @@ -35,7 +35,7 @@ final class EventSpec: QuickSpec { override func spec() { initSpec() - kindSpec() + aliasSpec() featureEventSpec() debugEventSpec() customEventSpec() @@ -94,11 +94,32 @@ final class EventSpec: QuickSpec { } } - private func kindSpec() { - describe("isAlwaysInlineUserKind") { - it("returns true when event kind should inline user") { - for kind in Event.Kind.allKinds { - expect(kind.isAlwaysInlineUserKind) == Event.Kind.alwaysInlineUserKinds.contains(kind) + private func aliasSpec() { + describe("alias events") { + var event: Event! + context("aliasing users") { + it("has correct fields") { + event = Event.aliasEvent(newUser: LDUser(), oldUser: LDUser()) + + expect(event.kind) == Event.Kind.alias + } + + it("from user to user") { + event = Event.aliasEvent(newUser: LDUser(key: "new"), oldUser: LDUser(key: "old")) + + expect(event.key) == "new" + expect(event.previousKey) == "old" + expect(event.contextKind) == "user" + expect(event.previousContextKind) == "user" + } + + it("from anon to anon") { + event = Event.aliasEvent(newUser: LDUser(key: "new", isAnonymous: true), oldUser: LDUser(key: "old", isAnonymous: true)) + + expect(event.key) == "new" + expect(event.previousKey) == "old" + expect(event.contextKind) == "anonymousUser" + expect(event.previousContextKind) == "anonymousUser" } } } @@ -278,6 +299,7 @@ final class EventSpec: QuickSpec { describe("dictionaryValue") { dictionaryValueFeatureEventSpec() dictionaryValueIdentifyEventSpec() + dictionaryValueAliasEventSpec() dictionaryValueCustomEventSpec() dictionaryValueDebugEventSpec() dictionaryValueSummaryEventSpec() @@ -312,6 +334,9 @@ final class EventSpec: QuickSpec { expect(eventDictionary.eventVersion) == featureFlag.flagVersion //Since feature flags include the flag version, it should be used. expect(eventDictionary.eventData).to(beNil()) expect(AnyComparer.isEqual(eventDictionary.reason, to: DarklyServiceMock.Constants.reason)).to(beTrue()) + expect(eventDictionary.eventPreviousKey).to(beNil()) + expect(eventDictionary.eventContextKind).to(beNil()) + expect(eventDictionary.eventPreviousContextKind).to(beNil()) } it("creates a dictionary with the user key only") { expect(eventDictionary.eventUserKey) == user.key @@ -399,6 +424,11 @@ final class EventSpec: QuickSpec { expect(eventDictionary.eventUser).to(beNil()) } } + it("creates a dictionary with contextKind for anonymous user") { + featureFlag = DarklyServiceMock.Constants.stubFeatureFlag(for: DarklyServiceMock.FlagKeys.null) + event = Event.featureEvent(key: Constants.eventKey, value: nil, defaultValue: nil, featureFlag: featureFlag, user: LDUser(), includeReason: false) + expect(event.dictionaryValue(config: config).eventContextKind) == "anonymousUser" + } } } @@ -431,6 +461,40 @@ final class EventSpec: QuickSpec { } } + private func dictionaryValueAliasEventSpec() { + let config = LDConfig.stub + let user1 = LDUser(key: "abc") + let user2 = LDUser(key: "def") + let anonUser1 = LDUser(key: "anon1", isAnonymous: true) + let anonUser2 = LDUser(key: "anon2", isAnonymous: true) + context("alias event") { + it("known to known") { + let eventDictionary = Event.aliasEvent(newUser: user1, oldUser: user2).dictionaryValue(config: config) + expect(eventDictionary.eventKind) == .alias + expect(eventDictionary.eventKey) == user1.key + expect(eventDictionary.eventPreviousKey) == user2.key + expect(eventDictionary.eventContextKind) == "user" + expect(eventDictionary.eventPreviousContextKind) == "user" + } + it("unknown to known") { + let eventDictionary = Event.aliasEvent(newUser: user1, oldUser: anonUser1).dictionaryValue(config: config) + expect(eventDictionary.eventKind) == .alias + expect(eventDictionary.eventKey) == user1.key + expect(eventDictionary.eventPreviousKey) == anonUser1.key + expect(eventDictionary.eventContextKind) == "user" + expect(eventDictionary.eventPreviousContextKind) == "anonymousUser" + } + it("unknown to unknown") { + let eventDictionary = Event.aliasEvent(newUser: anonUser1, oldUser: anonUser2).dictionaryValue(config: config) + expect(eventDictionary.eventKind) == .alias + expect(eventDictionary.eventKey) == anonUser1.key + expect(eventDictionary.eventPreviousKey) == anonUser2.key + expect(eventDictionary.eventContextKind) == "anonymousUser" + expect(eventDictionary.eventPreviousContextKind) == "anonymousUser" + } + } + } + private func dictionaryValueCustomEventSpec() { var config: LDConfig! let user = LDUser.stub() @@ -537,12 +601,26 @@ final class EventSpec: QuickSpec { expect(eventDictionary.eventValue).to(beNil()) expect(eventDictionary.eventDefaultValue).to(beNil()) expect(eventDictionary.eventVariation).to(beNil()) + expect(eventDictionary.eventContextKind).to(beNil()) } it("creates a dictionary with the full user") { expect(AnyComparer.isEqual(eventDictionary.eventUserDictionary, to: user.dictionaryValue(includePrivateAttributes: false, config: config))).to(beTrue()) expect(eventDictionary.eventUserKey).to(beNil()) } } + context("with anonymous user") { + it("sets contextKind field") { + do { + event = try Event.customEvent(key: Constants.eventKey, user: LDUser()) + } catch is LDInvalidArgumentError { + fail("customEvent threw an invalid argument exception") + } catch { + fail("customEvent threw an exception") + } + eventDictionary = event.dictionaryValue(config: config) + expect(eventDictionary.eventContextKind) == "anonymousUser" + } + } } } @@ -717,26 +795,10 @@ final class EventSpec: QuickSpec { } expect(eventDictionary.eventUser).to(beNil()) } - if let eventValue = event.value { - expect(AnyComparer.isEqual(eventDictionary.eventValue, to: eventValue)).to(beTrue()) - } else { - expect(eventDictionary.eventValue).to(beNil()) - } - if let eventDefaultValue = event.defaultValue { - expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: eventDefaultValue)).to(beTrue()) - } else { - expect(eventDictionary.eventDefaultValue).to(beNil()) - } - if let eventVariation = event.featureFlag?.variation { - expect(eventDictionary.eventVariation) == eventVariation - } else { - expect(eventDictionary.eventVariation).to(beNil()) - } - if let eventFlagVersion = event.featureFlag?.flagVersion { - expect(eventDictionary.eventVersion) == eventFlagVersion - } else { - expect(eventDictionary.eventVersion).to(beNil()) - } + expect(AnyComparer.isEqual(eventDictionary.eventValue, to: event.value)) == true + expect(AnyComparer.isEqual(eventDictionary.eventDefaultValue, to: event.defaultValue)) == true + expect(AnyComparer.isEqual(eventDictionary.eventVariation, to: event.featureFlag?.variation)) == true + expect(AnyComparer.isEqual(eventDictionary.eventVersion, to: event.featureFlag?.flagVersion)) == true if let eventData = event.data { expect(eventDictionary.eventData).toNot(beNil()) if let eventDictionaryData = eventDictionary.eventData { @@ -765,21 +827,15 @@ final class EventSpec: QuickSpec { it("returns the event kind") { events.forEach { event in eventDictionary = event.dictionaryValue(config: config) - expect(eventDictionary.eventKind) == event.kind } } } - context("when the dictionary does not contain the event kind") { - var eventDictionary: [String: Any]! - beforeEach { - let event = Event.stub(.custom, with: user) - eventDictionary = event.dictionaryValue(config: config) - eventDictionary.removeValue(forKey: Event.CodingKeys.kind.rawValue) - } - it("returns nil") { - expect(eventDictionary.eventKind).to(beNil()) - } + it("returns nil when the dictionary does not contain the event kind") { + let event = Event.stub(.custom, with: user) + var eventDictionary = event.dictionaryValue(config: config) + eventDictionary.removeValue(forKey: Event.CodingKeys.kind.rawValue) + expect(eventDictionary.eventKind).to(beNil()) } } @@ -790,18 +846,12 @@ final class EventSpec: QuickSpec { event = Event.stub(.custom, with: user) eventDictionary = event.dictionaryValue(config: config) } - context("when the dictionary contains a key") { - it("returns the key") { - expect(eventDictionary.eventKey) == event.key - } + it("returns the key when the dictionary contains a key") { + expect(eventDictionary.eventKey) == event.key } - context("when the dictionary does not contain a key") { - beforeEach { - eventDictionary.removeValue(forKey: Event.CodingKeys.key.rawValue) - } - it("returns nil") { - expect(eventDictionary.eventKey).to(beNil()) - } + it("returns nil when the dictionary does not contain a key") { + eventDictionary.removeValue(forKey: Event.CodingKeys.key.rawValue) + expect(eventDictionary.eventKey).to(beNil()) } } @@ -812,18 +862,12 @@ final class EventSpec: QuickSpec { event = Event.stub(.custom, with: user) eventDictionary = event.dictionaryValue(config: config) } - context("when the dictionary contains a creation date") { - it("returns the creation date millis") { - expect(eventDictionary.eventCreationDateMillis) == event.creationDate?.millisSince1970 - } + it("returns the creation date millis when the dictionary contains a creation date") { + expect(eventDictionary.eventCreationDateMillis) == event.creationDate?.millisSince1970 } - context("when the dictionary does not contain a creation date") { - beforeEach { - eventDictionary.removeValue(forKey: Event.CodingKeys.creationDate.rawValue) - } - it("returns nil") { - expect(eventDictionary.eventCreationDateMillis).to(beNil()) - } + it("returns nil when the dictionary does not contain a creation date") { + eventDictionary.removeValue(forKey: Event.CodingKeys.creationDate.rawValue) + expect(eventDictionary.eventCreationDateMillis).to(beNil()) } } @@ -834,18 +878,12 @@ final class EventSpec: QuickSpec { event = Event.stub(.summary, with: user) eventDictionary = event.dictionaryValue(config: config) } - context("when the dictionary contains the event endDate") { - it("returns the event kind") { - expect(eventDictionary.eventEndDate?.isWithin(0.001, of: event.endDate)).to(beTrue()) - } + it("returns the event kind when the dictionary contains the event endDate") { + expect(eventDictionary.eventEndDate?.isWithin(0.001, of: event.endDate)).to(beTrue()) } - context("when the dictionary does not contain the event kind") { - beforeEach { - eventDictionary.removeValue(forKey: Event.CodingKeys.endDate.rawValue) - } - it("returns nil") { - expect(eventDictionary.eventEndDate).to(beNil()) - } + it("returns nil when the dictionary does not contain the event kind") { + eventDictionary.removeValue(forKey: Event.CodingKeys.endDate.rawValue) + expect(eventDictionary.eventEndDate).to(beNil()) } } @@ -856,58 +894,32 @@ final class EventSpec: QuickSpec { eventDictionary = Event.stub(.custom, with: user).dictionaryValue(config: config) otherDictionary = eventDictionary } - context("when keys and creationDateMillis are equal") { - it("returns true") { - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == true - } + it("returns true when keys and creationDateMillis are equal") { + expect(eventDictionary.matches(eventDictionary: otherDictionary)) == true } - context("when keys differ") { - beforeEach { - otherDictionary[Event.CodingKeys.key.rawValue] = otherDictionary.eventKey! + "dummy" - } - it("returns false") { - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } + it("returns false when keys differ") { + otherDictionary[Event.CodingKeys.key.rawValue] = otherDictionary.eventKey! + "dummy" + expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false } - context("when creationDateMillis differ") { - beforeEach { - otherDictionary[Event.CodingKeys.creationDate.rawValue] = otherDictionary.eventCreationDateMillis! + 1 - } - it("returns false") { - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } + it("returns false when creationDateMillis differ") { + otherDictionary[Event.CodingKeys.creationDate.rawValue] = otherDictionary.eventCreationDateMillis! + 1 + expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false } - context("when dictionary key is nil") { - beforeEach { - eventDictionary.removeValue(forKey: Event.CodingKeys.key.rawValue) - } - it("returns false") { - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } + it("returns false when dictionary key is nil") { + eventDictionary.removeValue(forKey: Event.CodingKeys.key.rawValue) + expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false } - context("when other dictionary key is nil") { - beforeEach { - otherDictionary.removeValue(forKey: Event.CodingKeys.key.rawValue) - } - it("returns false") { - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } + it("returns false when other dictionary key is nil") { + otherDictionary.removeValue(forKey: Event.CodingKeys.key.rawValue) + expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false } - context("when dictionary creationDateMillis is nil") { - beforeEach { - eventDictionary.removeValue(forKey: Event.CodingKeys.creationDate.rawValue) - } - it("returns false") { - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } + it("returns false when dictionary creationDateMillis is nil") { + eventDictionary.removeValue(forKey: Event.CodingKeys.creationDate.rawValue) + expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false } - context("when other dictionary creationDateMillis is nil") { - beforeEach { - otherDictionary.removeValue(forKey: Event.CodingKeys.creationDate.rawValue) - } - it("returns false") { - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } + it("returns false when other dictionary creationDateMillis is nil") { + otherDictionary.removeValue(forKey: Event.CodingKeys.creationDate.rawValue) + expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false } context("for summary event dictionaries") { var event: Event! @@ -915,50 +927,30 @@ final class EventSpec: QuickSpec { event = Event.stub(.summary, with: user) eventDictionary = event.dictionaryValue(config: config) } - context("when the kinds and endDates match") { - beforeEach { - otherDictionary = event.dictionaryValue(config: config) - } - it("returns true") { - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == true - } + it("when the kinds and endDates match returns true") { + otherDictionary = event.dictionaryValue(config: config) + expect(eventDictionary.matches(eventDictionary: otherDictionary)) == true } - context("when the kinds do not match") { - beforeEach { - otherDictionary = event.dictionaryValue(config: config) - otherDictionary[Event.CodingKeys.kind.rawValue] = Event.Kind.feature.rawValue - } - it("returns false") { - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } + it("when the kinds do not match returns false") { + otherDictionary = event.dictionaryValue(config: config) + otherDictionary[Event.CodingKeys.kind.rawValue] = Event.Kind.feature.rawValue + expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false } context("when the endDates do not match") { - context("endDates differ") { - beforeEach { - otherDictionary = event.dictionaryValue(config: config) - otherDictionary[Event.CodingKeys.endDate.rawValue] = event.endDate!.addingTimeInterval(0.002).millisSince1970 - } - it("returns false") { - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } + it("and endDates differ returns false") { + otherDictionary = event.dictionaryValue(config: config) + otherDictionary[Event.CodingKeys.endDate.rawValue] = event.endDate!.addingTimeInterval(0.002).millisSince1970 + expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false } - context("endDate is nil") { - beforeEach { - eventDictionary.removeValue(forKey: Event.CodingKeys.endDate.rawValue) - otherDictionary = event.dictionaryValue(config: config) - } - it("returns false") { - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } + it("and endDate is nil returns false") { + eventDictionary.removeValue(forKey: Event.CodingKeys.endDate.rawValue) + otherDictionary = event.dictionaryValue(config: config) + expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false } - context("other endDate is nil") { - beforeEach { - otherDictionary = event.dictionaryValue(config: config) - otherDictionary.removeValue(forKey: Event.CodingKeys.endDate.rawValue) - } - it("returns false") { - expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false - } + it("and other endDate is nil returns false") { + otherDictionary = event.dictionaryValue(config: config) + otherDictionary.removeValue(forKey: Event.CodingKeys.endDate.rawValue) + expect(eventDictionary.matches(eventDictionary: otherDictionary)) == false } } } @@ -1021,9 +1013,18 @@ extension Dictionary where Key == String, Value == Any { var eventKey: String? { self[Event.CodingKeys.key.rawValue] as? String } + var eventPreviousKey: String? { + self[Event.CodingKeys.previousKey.rawValue] as? String + } var eventCreationDateMillis: Int64? { self[Event.CodingKeys.creationDate.rawValue] as? Int64 } + var eventContextKind: String? { + self[Event.CodingKeys.contextKind.rawValue] as? String + } + var eventPreviousContextKind: String? { + self[Event.CodingKeys.previousContextKind.rawValue] as? String + } func matches(eventDictionary other: [String: Any]) -> Bool { guard let kind = eventKind @@ -1067,6 +1068,7 @@ extension Event { case .identify: return Event.identifyEvent(user: user) case .custom: return (try? Event.customEvent(key: UUID().uuidString, user: user, data: ["custom": UUID().uuidString]))! case .summary: return Event.summaryEvent(flagRequestTracker: FlagRequestTracker.stub())! + case .alias: return Event.aliasEvent(newUser: LDUser(), oldUser: LDUser()) } } diff --git a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift index 8543d59c..c6d2bd16 100644 --- a/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Models/LDConfigSpec.swift @@ -38,6 +38,7 @@ final class LDConfigSpec: XCTestCase { fileprivate static let wrapperName = "ReactNative" fileprivate static let wrapperVersion = "0.1.0" fileprivate static let additionalHeaders = ["Proxy-Authorization": "creds"] + fileprivate static let autoAliasingOptOut = true } let testFields: [(String, Any, (inout LDConfig, Any?) -> Void)] = @@ -64,7 +65,8 @@ final class LDConfigSpec: XCTestCase { ("diagnostic recording interval", Constants.diagnosticRecordingInterval, { c, v in c.diagnosticRecordingInterval = v as! TimeInterval }), ("wrapper name", Constants.wrapperName, { c, v in c.wrapperName = v as! String? }), ("wrapper version", Constants.wrapperVersion, { c, v in c.wrapperVersion = v as! String? }), - ("additional headers", Constants.additionalHeaders, { c, v in c.additionalHeaders = v as! [String: String]})] + ("additional headers", Constants.additionalHeaders, { c, v in c.additionalHeaders = v as! [String: String]}), + ("auto aliasing opt out", Constants.autoAliasingOptOut, { c, v in c.autoAliasingOptOut = v as! Bool })] func testInitDefault() { let config = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey) @@ -92,6 +94,7 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.wrapperName, LDConfig.Defaults.wrapperName) XCTAssertEqual(config.wrapperVersion, LDConfig.Defaults.wrapperVersion) XCTAssertEqual(config.additionalHeaders, LDConfig.Defaults.additionalHeaders) + XCTAssertEqual(config.autoAliasingOptOut, LDConfig.Defaults.autoAliasingOptOut) } func testInitUpdate() { @@ -126,6 +129,7 @@ final class LDConfigSpec: XCTestCase { XCTAssertEqual(config.wrapperName, Constants.wrapperName, "\(os)") XCTAssertEqual(config.wrapperVersion, Constants.wrapperVersion, "\(os)") XCTAssertEqual(config.additionalHeaders, Constants.additionalHeaders, "\(os)") + XCTAssertEqual(config.autoAliasingOptOut, Constants.autoAliasingOptOut, "\(os)") } } @@ -200,10 +204,11 @@ final class LDConfigSpec: XCTestCase { symmetricAssertEqual(defaultConfig, LDConfig(mobileKey: LDConfig.Constants.mockMobileKey)) // different mobile key symmetricAssertNotEqual(defaultConfig, LDConfig(mobileKey: LDConfig.Constants.alternateMobileKey)) + testFields.forEach { name, otherVal, setter in var otherConfig = LDConfig(mobileKey: LDConfig.Constants.mockMobileKey, environmentReporter: environmentReporter) setter(&otherConfig, otherVal) - symmetricAssertNotEqual(defaultConfig, otherConfig, "\(name) differs") + symmetricAssertNotEqual(defaultConfig, otherConfig, "\(name) is the same") } } diff --git a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift index 4374774d..12502128 100644 --- a/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/Networking/DarklyServiceSpec.swift @@ -773,6 +773,7 @@ final class DarklyServiceSpec: QuickSpec { describe("clearFlagResponseCache") { context("cached responses and etags exist") { beforeEach { + URLCache.shared.diskCapacity = 0 testContext = TestContext(mobileKeyCount: Constants.mobileKeyCount) HTTPHeaders.loadFlagRequestEtags(testContext.flagRequestEtags) flagRequestEtag = UUID().uuidString diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift index 70e85ab4..e163a16f 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/Cache/UserEnvironmentFlagCacheSpec.swift @@ -93,13 +93,9 @@ final class UserEnvironmentFlagCacheSpec: QuickSpec { private func initSpec() { var testContext: TestContext! describe("init") { - context("with keyedValueCache") { - beforeEach { - testContext = TestContext() - } - it("creates a UserEnvironmentCache with the passed in keyedValueCache") { - expect(testContext.userEnvironmentFlagCache.keyedValueCache) === testContext.keyedValueCacheMock - } + it("creates a UserEnvironmentCache with the passed in keyedValueCache") { + testContext = TestContext() + expect(testContext.userEnvironmentFlagCache.keyedValueCache) === testContext.keyedValueCacheMock } } } diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift new file mode 100644 index 00000000..549aff21 --- /dev/null +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/DiagnosticReporterSpec.swift @@ -0,0 +1,139 @@ +import Foundation +import XCTest + +@testable import LaunchDarkly + +final class DiagnosticReporterSpec: XCTestCase { + final class TestContext { + let awaiter = DispatchSemaphore(value: 0) + let cachingMock = DiagnosticCachingMock() + let diagnosticId = DiagnosticId(diagnosticId: "abc", sdkKey: "fake_mobile_key") + var queuedResponses: [ServiceResponse] = [] + var receivedEvents: [DiagnosticEvent] = [] + var subject: DiagnosticReporter + var service: DarklyServiceMock + + init() { + service = DarklyServiceMock() + subject = DiagnosticReporter(service: service) + + cachingMock.getDiagnosticIdReturnValue = diagnosticId + service.diagnosticCache = cachingMock + service.publishDiagnosticCallback = { + if self.queuedResponses.first != nil { + self.service.stubbedDiagnosticResponse = self.queuedResponses.removeFirst() + } else { + XCTFail("Unexpected request to diagnostic endpoint during test") + } + XCTAssertNotNil(self.service.stubbedDiagnosticResponse) + self.receivedEvents.append(self.service.publishedDiagnostic!) + self.awaiter.signal() + } + } + + func queueResponse(status: Int? = nil, withError: Bool = false) { + var response: HTTPURLResponse? = nil + var error: Error? = nil + if let status = status { + response = HTTPURLResponse(url: service.config.eventsUrl, + statusCode: status, + httpVersion: DarklyServiceMock.Constants.httpVersion, + headerFields: [:]) + } + if withError { + error = DarklyServiceMock.Constants.error + } + queuedResponses.append((nil, response, error)) + } + + func takeEvent() -> DiagnosticEvent { + awaiter.wait() + if receivedEvents.first == nil { + XCTFail("Missing expected diagnostic event") + } + return receivedEvents.remove(at: 0) + } + + func expectNoEvent() { + XCTAssertEqual(awaiter.wait(timeout: DispatchTime.now() + 1.0), .timedOut) + XCTAssertTrue(receivedEvents.isEmpty) + } + } + + func testInitEvent() { + let tst = TestContext() + tst.queueResponse(status: 202) + tst.subject.setMode(.foreground, online: true) + + let published = tst.takeEvent() + XCTAssertEqual(published.kind, .diagnosticInit) + XCTAssertEqual(published.id.diagnosticId, tst.diagnosticId.diagnosticId) + XCTAssertEqual(published.id.sdkKeySuffix, tst.diagnosticId.sdkKeySuffix) + XCTAssertTrue(published is DiagnosticInit) + + // Test that init is not sent again when client changes online state + tst.subject.setMode(.foreground, online: true) + tst.subject.setMode(.foreground, online: false) + tst.subject.setMode(.foreground, online: true) + + tst.expectNoEvent() + } + + func testInitInBackground() { + let tst = TestContext() + tst.queueResponse(status: 202) + tst.subject.setMode(.background, online: true) + // Should not send init event while in background, even if set to background again. + tst.subject.setMode(.background, online: true) + + tst.expectNoEvent() + + // Should sent init once in foreground + tst.subject.setMode(.foreground, online: true) + let published = tst.takeEvent() + XCTAssertEqual(published.kind, .diagnosticInit) + XCTAssertEqual(published.id.diagnosticId, tst.diagnosticId.diagnosticId) + XCTAssertEqual(published.id.sdkKeySuffix, tst.diagnosticId.sdkKeySuffix) + XCTAssertTrue(published is DiagnosticInit) + } + + func testLastStatsSent() { + let tst = TestContext() + let now = Date().millisSince1970 + let stats = DiagnosticStats(id: tst.diagnosticId, creationDate: now, dataSinceDate: now, droppedEvents: 0, eventsInLastBatch: 0, streamInits: []) + tst.cachingMock.lastStats = stats + tst.queueResponse(status: 202) + tst.queueResponse(status: 202) + tst.subject.setMode(.foreground, online: true) + + var published = tst.takeEvent() + XCTAssertEqual(published.kind, .diagnosticStats) + XCTAssertEqual(published.id.diagnosticId, tst.diagnosticId.diagnosticId) + XCTAssertTrue(published is DiagnosticStats) + if let published = published as? DiagnosticStats { + XCTAssertEqual(published.creationDate, now) + } + + published = tst.takeEvent() + XCTAssertEqual(published.kind, .diagnosticInit) + XCTAssertEqual(published.id.diagnosticId, tst.diagnosticId.diagnosticId) + XCTAssertEqual(published.id.sdkKeySuffix, tst.diagnosticId.sdkKeySuffix) + XCTAssertTrue(published is DiagnosticInit) + + tst.expectNoEvent() + } + + func testRetries() { + let tst = TestContext() + tst.queueResponse(status: 500) + tst.queueResponse(withError: true) + tst.subject.setMode(.foreground, online: true) + + var published = tst.takeEvent() + XCTAssertEqual(published.kind, .diagnosticInit) + published = tst.takeEvent() + XCTAssertEqual(published.kind, .diagnosticInit) + + tst.expectNoEvent() + } +} diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift index 80b190de..565308ab 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagChangeNotifierSpec.swift @@ -193,195 +193,11 @@ final class FlagChangeNotifierSpec: QuickSpec { } private func removeObserverSpec() { - describe("remove observer") { - removeObserverForKeySpec() - removeObserverForKeysSpec() - removeObserverForOwnerSpec() - } - } - - private func removeObserverForKeySpec() { - var testContext: TestContext! - var targetObserver: FlagChangeObserver! - - context("with a single flag key") { - context("when several observers exist") { - beforeEach { - testContext = TestContext(observers: Constants.observerCount, observerType: .singleKey) - targetObserver = testContext.subject.flagObservers[Constants.observerCount - 2] //Take the middle one - - testContext.subject.removeObserver(targetObserver.flagKeys.first!, owner: targetObserver.owner!) - } - it("removes the observer") { - expect(testContext.subject.flagObservers.count) == Constants.observerCount - 1 - expect(testContext.subject.flagObservers.contains(targetObserver)).to(beFalse()) - } - } - context("when 1 observer exists") { - beforeEach { - testContext = TestContext(observers: 1, observerType: .singleKey) - targetObserver = testContext.subject.flagObservers.first! - - testContext.subject.removeObserver(targetObserver.flagKeys.first!, owner: targetObserver.owner!) - } - it("removes the observer") { - expect(testContext.subject.flagObservers.isEmpty).to(beTrue()) - } - } - context("when the target observer doesnt exist") { - var owner: FlagChangeHandlerOwnerMock! - context("because the target has a different key") { - beforeEach { - testContext = TestContext(observers: Constants.observerCount, observerType: .singleKey) - owner = (testContext.subject.flagObservers.first!.owner as! FlagChangeHandlerOwnerMock) - targetObserver = FlagChangeObserver(key: DarklyServiceMock.FlagKeys.int, owner: owner, flagChangeHandler: testContext.flagChangeHandler) - - testContext.subject.removeObserver(targetObserver.flagKeys.first!, owner: targetObserver.owner!) - } - it("leaves the observers unchanged") { - expect(testContext.subject.flagObservers.count) == Constants.observerCount - expect(testContext.subject.flagObservers) == testContext.originalFlagChangeObservers - } - } - context("because the target has a different owner") { - beforeEach { - testContext = TestContext(observers: Constants.observerCount, observerType: .singleKey) - owner = FlagChangeHandlerOwnerMock() - targetObserver = FlagChangeObserver(key: DarklyServiceMock.FlagKeys.bool, owner: owner, flagChangeHandler: testContext.flagChangeHandler) - - testContext.subject.removeObserver(targetObserver.flagKeys.first!, owner: targetObserver.owner!) - } - it("leaves the observers unchanged") { - expect(testContext.subject.flagObservers.count) == Constants.observerCount - expect(testContext.subject.flagObservers) == testContext.originalFlagChangeObservers - } - } - context("because the target has a different key and owner") { - beforeEach { - testContext = TestContext(observers: Constants.observerCount, observerType: .singleKey) - owner = FlagChangeHandlerOwnerMock() - targetObserver = FlagChangeObserver(key: DarklyServiceMock.FlagKeys.int, owner: owner, flagChangeHandler: testContext.flagChangeHandler) - - testContext.subject.removeObserver(targetObserver.flagKeys.first!, owner: targetObserver.owner!) - } - it("leaves the observers unchanged") { - expect(testContext.subject.flagObservers.count) == Constants.observerCount - expect(testContext.subject.flagObservers) == testContext.originalFlagChangeObservers - } - } - } - context("when 2 target observers exist") { - beforeEach { - testContext = TestContext(observers: Constants.observerCount, observerType: .singleKey, repeatFirstObserver: true) - targetObserver = testContext.subject.flagObservers.first! - - testContext.subject.removeObserver(targetObserver.flagKeys.first!, owner: targetObserver.owner!) - } - it("removes both observers") { - expect(testContext.subject.flagObservers.count) == Constants.observerCount - 2 - expect(testContext.subject.flagObservers.contains(targetObserver)).to(beFalse()) - } - } - } - } - - private func removeObserverForKeysSpec() { - var testContext: TestContext! - var targetObserver: FlagChangeObserver! - - context("with multiple flag keys") { - context("when several observers exist") { - beforeEach { - testContext = TestContext(observers: Constants.observerCount, observerType: .multipleKey) - targetObserver = testContext.subject.flagObservers[Constants.observerCount - 2] //Take the middle one - - testContext.subject.removeObserver(targetObserver.flagKeys, owner: targetObserver.owner!) - } - it("removes the observer") { - expect(testContext.subject.flagObservers.count) == Constants.observerCount - 1 - expect(testContext.subject.flagObservers.contains(targetObserver)).to(beFalse()) - } - } - context("when 1 observer exists") { - beforeEach { - testContext = TestContext(observers: 1, observerType: .multipleKey) - targetObserver = testContext.subject.flagObservers.first! - - testContext.subject.removeObserver(targetObserver.flagKeys, owner: targetObserver.owner!) - } - it("removes the observer") { - expect(testContext.subject.flagObservers.isEmpty).to(beTrue()) - } - } - context("when the target observer doesnt exist") { - var owner: FlagChangeHandlerOwnerMock! - context("because the target has different keys") { - beforeEach { - testContext = TestContext(observers: Constants.observerCount, observerType: .multipleKey) - owner = (testContext.subject.flagObservers.first!.owner as! FlagChangeHandlerOwnerMock) - var keys = DarklyServiceMock.FlagKeys.knownFlags - keys.remove(at: 0) - targetObserver = FlagChangeObserver(keys: keys, owner: owner, flagCollectionChangeHandler: testContext.flagCollectionChangeHandler) - - testContext.subject.removeObserver(targetObserver.flagKeys, owner: targetObserver.owner!) - } - it("leaves the observers unchanged") { - expect(testContext.subject.flagObservers.count) == Constants.observerCount - expect(testContext.subject.flagObservers) == testContext.originalFlagChangeObservers - } - } - context("because the target has a different owner") { - beforeEach { - testContext = TestContext(observers: Constants.observerCount, observerType: .multipleKey) - owner = FlagChangeHandlerOwnerMock() - targetObserver = FlagChangeObserver(keys: DarklyServiceMock.FlagKeys.knownFlags, - owner: owner, - flagCollectionChangeHandler: testContext.flagCollectionChangeHandler) - - testContext.subject.removeObserver(targetObserver.flagKeys, owner: targetObserver.owner!) - } - it("leaves the observers unchanged") { - expect(testContext.subject.flagObservers.count) == Constants.observerCount - expect(testContext.subject.flagObservers) == testContext.originalFlagChangeObservers - } - } - context("because the target has different keys and owner") { - beforeEach { - testContext = TestContext(observers: Constants.observerCount, observerType: .multipleKey) - owner = FlagChangeHandlerOwnerMock() - var keys = DarklyServiceMock.FlagKeys.knownFlags - keys.remove(at: 0) - targetObserver = FlagChangeObserver(keys: keys, owner: owner, flagCollectionChangeHandler: testContext.flagCollectionChangeHandler) - - testContext.subject.removeObserver(targetObserver.flagKeys, owner: targetObserver.owner!) - } - it("leaves the observers unchanged") { - expect(testContext.subject.flagObservers.count) == Constants.observerCount - expect(testContext.subject.flagObservers) == testContext.originalFlagChangeObservers - } - } - } - context("when 2 target observers exist") { - beforeEach { - testContext = TestContext(observers: Constants.observerCount, observerType: .multipleKey, repeatFirstObserver: true) - targetObserver = testContext.subject.flagObservers.first! - - testContext.subject.removeObserver(targetObserver.flagKeys, owner: targetObserver.owner!) - } - it("removes both observers") { - expect(testContext.subject.flagObservers.count) == Constants.observerCount - 2 - expect(testContext.subject.flagObservers.contains(targetObserver)).to(beFalse()) - } - } - } - } - - private func removeObserverForOwnerSpec() { var testContext: TestContext! var targetObserver: FlagChangeObserver! var targetOwner: FlagChangeHandlerOwnerMock! - context("with owner") { + context("remove observer") { context("when several observers exist") { beforeEach { testContext = TestContext(observers: Constants.observerCount) diff --git a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift index 361fa7f3..0ee53e0a 100644 --- a/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift +++ b/LaunchDarkly/LaunchDarklyTests/ServiceObjects/FlagStoreSpec.swift @@ -41,7 +41,6 @@ final class FlagStoreSpec: QuickSpec { updateStoreSpec() deleteFlagSpec() featureFlagSpec() - variationSpec() } func initSpec() { @@ -49,81 +48,63 @@ final class FlagStoreSpec: QuickSpec { var featureFlags: [LDFlagKey: FeatureFlag]! describe("init") { context("without an initial flag store") { - beforeEach { - subject = FlagStore() - } it("has no feature flags") { - expect(subject.featureFlags.isEmpty).to(beTrue()) + subject = FlagStore() + expect(subject.featureFlags.isEmpty) == true } } context("with an initial flag store") { - beforeEach { + it("has matching feature flags") { featureFlags = DarklyServiceMock.Constants.stubFeatureFlags() subject = FlagStore(featureFlags: featureFlags) - } - it("has matching feature flags") { - expect(subject.featureFlags == featureFlags).to(beTrue()) + expect(subject.featureFlags) == featureFlags } } context("with an initial flag store without elements") { - beforeEach { + it("has matching feature flags") { featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeVariations: false, includeVersions: false, includeFlagVersions: false) subject = FlagStore(featureFlags: featureFlags) - } - it("has matching feature flags") { - expect(subject.featureFlags == featureFlags).to(beTrue()) + expect(subject.featureFlags) == featureFlags } } context("with an initial flag dictionary") { - beforeEach { + it("has the feature flags") { featureFlags = DarklyServiceMock.Constants.stubFeatureFlags() subject = FlagStore(featureFlagDictionary: featureFlags.dictionaryValue) - } - it("has the feature flags") { - expect(subject.featureFlags == featureFlags).to(beTrue()) + expect(subject.featureFlags) == featureFlags } } } } func replaceStoreSpec() { - var featureFlags: [LDFlagKey: FeatureFlag]! + let featureFlags: [LDFlagKey: FeatureFlag] = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false) var flagStore: FlagStore! describe("replaceStore") { context("with new flag values") { - beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false) + it("causes FlagStore to replace the flag values") { flagStore = FlagStore() waitUntil(timeout: .seconds(1)) { done in flagStore.replaceStore(newFlags: featureFlags, completion: done) } - } - it("causes FlagStore to replace the flag values") { expect(flagStore.featureFlags) == featureFlags } } context("with new flag value dictionary") { - beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false) + it("causes FlagStore to replace the flag values") { flagStore = FlagStore() waitUntil(timeout: .seconds(1)) { done in flagStore.replaceStore(newFlags: featureFlags.dictionaryValue, completion: done) } - } - it("causes FlagStore to replace the flag values") { expect(flagStore.featureFlags) == featureFlags } } - context("with nil flag values") { - beforeEach { - featureFlags = DarklyServiceMock.Constants.stubFeatureFlags(includeNullValue: false) + context("with invalid dictionary") { + it("causes FlagStore to empty the flag values") { flagStore = FlagStore(featureFlags: featureFlags) - waitUntil(timeout: .seconds(1)) { done in - flagStore.replaceStore(newFlags: nil, completion: done) + flagStore.replaceStore(newFlags: ["fakeKey": "Not a flag dict"], completion: done) } - } - it("causes FlagStore to empty the flag values and replace the source") { expect(flagStore.featureFlags.isEmpty).to(beTrue()) } } @@ -412,57 +393,11 @@ final class FlagStoreSpec: QuickSpec { } } context("when flag key doesn't exist") { - var featureFlag: FeatureFlag? - beforeEach { - featureFlag = flagStore.featureFlag(for: DarklyServiceMock.FlagKeys.unknown) - } it("returns nil") { + let featureFlag = flagStore.featureFlag(for: DarklyServiceMock.FlagKeys.unknown) expect(featureFlag).to(beNil()) } } } } - - func variationSpec() { - var subject: FlagStore! - describe("variation") { - context("when flags exist") { - beforeEach { - subject = FlagStore(featureFlags: DarklyServiceMock.Constants.stubFeatureFlags()) - } - it("causes the FlagStore to provide the flag value") { - expect(subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultValues.bool)) == DarklyServiceMock.FlagValues.bool - expect(subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultValues.int)) == DarklyServiceMock.FlagValues.int - expect(subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultValues.double)) == DarklyServiceMock.FlagValues.double - expect(subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultValues.string)) == DarklyServiceMock.FlagValues.string - - let arrayValue = subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultValues.array) - expect(arrayValue) == DarklyServiceMock.FlagValues.array - - let dictionaryValue = subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultValues.dictionary) - expect(dictionaryValue == DarklyServiceMock.FlagValues.dictionary).to(beTrue()) - } - } - context("when flags do not exist") { - beforeEach { - subject = FlagStore() - } - it("causes the FlagStore to provide the defaultValue flag value") { - expect(subject.variation(forKey: DarklyServiceMock.FlagKeys.bool, defaultValue: DefaultValues.bool)) == DefaultValues.bool - expect(subject.variation(forKey: DarklyServiceMock.FlagKeys.int, defaultValue: DefaultValues.int)) == DefaultValues.int - expect(subject.variation(forKey: DarklyServiceMock.FlagKeys.double, defaultValue: DefaultValues.double)) == DefaultValues.double - expect(subject.variation(forKey: DarklyServiceMock.FlagKeys.string, defaultValue: DefaultValues.string)) == DefaultValues.string - - let arrayValue = subject.variation(forKey: DarklyServiceMock.FlagKeys.array, defaultValue: DefaultValues.array) - expect(arrayValue) == DefaultValues.array - - let dictionaryValue = subject.variation(forKey: DarklyServiceMock.FlagKeys.dictionary, defaultValue: DefaultValues.dictionary) - expect(dictionaryValue == DefaultValues.dictionary).to(beTrue()) - - let nullValue = subject.variation(forKey: DarklyServiceMock.FlagKeys.null, defaultValue: DefaultValues.int) - expect(nullValue) == DefaultValues.int - } - } - } - } } diff --git a/README.md b/README.md index 4cf97c0b..68a294f2 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ To include LaunchDarkly in a Swift package, simply add it to the dependencies se ```swift dependencies: [ - .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor(from: "5.3.2")) + .package(url: "https://github.com/launchdarkly/ios-client-sdk.git", .upToNextMinor(from: "5.4.0")) ] ``` @@ -51,7 +51,7 @@ To use the [CocoaPods](https://cocoapods.org) dependency manager to integrate La ```ruby use_frameworks! target 'YourTargetName' do - pod 'LaunchDarkly', '~> 5.3' + pod 'LaunchDarkly', '~> 5.4' end ``` @@ -62,7 +62,7 @@ To use the [Carthage](https://github.com/Carthage/Carthage) dependency manager t To integrate LaunchDarkly into your Xcode project using Carthage, specify it in your `Cartfile`: ```ogdl -github "launchdarkly/ios-client-sdk" ~> 5.3 +github "launchdarkly/ios-client-sdk" ~> 5.4 ``` ### Manual installation diff --git a/SourceryTemplates/mocks.stencil b/SourceryTemplates/mocks.stencil index 3d380b85..4ba5443f 100644 --- a/SourceryTemplates/mocks.stencil +++ b/SourceryTemplates/mocks.stencil @@ -3,16 +3,14 @@ import {{ argument }} {% endfor %} {% if argument.app %}@testable import {{ argument.app }}{% endif %} -{# Protocol Mocks #} +// swiftlint:disable large_tuple {% for type in types.protocols %} {% if type.annotations.autoMockable %} // MARK: - {{ type.name}}Mock final class {{ type.name }}Mock: {{ type.name }} { {% for variable in type.allVariables|!annotated:"noMock" %} -{% if variable.writeAccess != "private" or variable.writeAccess != "fileprivate" %} - // MARK: {{ variable.name }} var {{ variable.name }}SetCount = 0 var set{{ variable.name|upperFirstLetter }}Callback: (() -> Void)? var {{ variable.name }}: {{ variable.typeName }}{% if variable|annotated:"defaultMockValue" %} = {{ variable.annotations.defaultMockValue }}{% elif variable.isOptional %} = nil{% elif variable.isArray %} = []{% elif variable.isDictionary %} = [:]{% else %} // You must annotate mocked variables that are not optional, arrays, or dictionaries, using a comment: //sourcery: defaultMockValue = {% endif %} { @@ -21,76 +19,24 @@ final class {{ type.name }}Mock: {{ type.name }} { set{{ variable.name|upperFirstLetter }}Callback?() } } -{% endif %} {% endfor %} {% for method in type.allMethods|!annotated:"noMock" %} - // MARK: {{ method.annotations.StubName|default:method.callName }} -{% if not method.shortName == "init" %} var {{ method.annotations.StubName|default:method.callName }}CallCount = 0{% endif %} -{% if not method.shortName == "init" %} var {{ method.annotations.StubName|default:method.callName }}Callback: (() -> Void)?{% endif %} -{% if method.throws %} var {{ method.annotations.StubName|default:method.callName }}ShouldThrow: Error?{% endif %} -{% if method.parameters.count > 3 %} //swiftlint:disable:next large_tuple {% endif %} -{% if method.parameters.count == 1 %} var {{ method.annotations.StubName|default:method.callName }}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}: {% if param.typeName.unwrappedTypeName == "LDEvent" %}Darkly.{% endif %}{{ param.typeName.unwrappedTypeName }}?{% endfor %} -{% else %}{% if not method.parameters.count == 0 %} var {{ method.annotations.StubName|default:method.callName }}ReceivedArguments: ({% for param in method.parameters %}{{ param.name }}: {% if param.typeAttributes.escaping %}{{ param.unwrappedTypeName }}{% else %}{% if param.typeName.unwrappedTypeName == "LDEvent" %}Darkly.{% endif %}{{ param.typeName }}{% endif %}{% if not forloop.last %}, {% endif %}{% endfor %})?{% endif %} -{% endif %} -{% if not method.returnTypeName.isVoid and not method.shortName == "init" %} var {{ method.annotations.StubName|default:method.callName }}ReturnValue: {{ method.returnTypeName }}{% if method.annotations.DefaultReturnValue %} = {{ method.annotations.DefaultReturnValue }}{% else %}{% if not method.isOptionalReturnType %}!{% endif %}{% endif %}{% endif %} - func {{ method.shortName }}({% for param in method.parameters %}{% if param.argumentLabel == nil %}_{% else %}{{ param.argumentLabel }}{% endif %}{% if not param.argumentLabel == param.name %} {{ param.name }}{% endif %}: {% if param.typeName.unwrappedTypeName == "LDEvent" %}Darkly.{% endif %}{{ param.typeName }}{% if not forloop.last %}, {% endif %}{% endfor %}){% if method.throws %} throws{% endif %}{% if not method.returnTypeName.isVoid %} -> {{ method.returnTypeName }}{% endif %} { -{% if not method.shortName == "init" %} {{ method.annotations.StubName|default:method.callName }}CallCount += 1{% endif %} -{%if method.parameters.count == 1 %} {{ method.annotations.StubName|default:method.callName }}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }} = {{ param.name }}{% endfor %}{% else %}{% if not method.parameters.count == 0 %} {{ method.annotations.StubName|default:method.callName }}ReceivedArguments = ({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %}){% endif %}{% if not method.returnTypeName.isVoid %}{% endif %}{% endif %} -{% if method.throws %} if let {{ method.annotations.StubName|default:method.callName }}ShouldThrow = {{ method.annotations.StubName|default:method.callName }}ShouldThrow { throw {{ method.annotations.StubName|default:method.callName }}ShouldThrow }{% endif %} - {{ method.annotations.StubName|default:method.callName }}Callback?() -{% if not method.returnTypeName.isVoid and not method.shortName == "init" %} - return {{ method.annotations.StubName|default:method.callName }}ReturnValue{% endif %} - } -{% endfor %} -} -{% endif %} -{% endfor %} -{# Class Mocks #} -{% for type in types.classes %} -{% if type.annotations.autoMockable %} - final class {{ type.name }}Mock: {% if type.annotations.MockBaseClass %}{{ type.annotations.MockBaseClass }}, {% endif %}{{ type.name }} { - -// MARK: - {{ type.name}}Mock -final class {{ type.name }}Mock: {% if type.annotations.MockBaseClass %}{{ type.annotations.MockBaseClass }}, {% endif %}{{ type.name }} { -{% for variable in type.allVariables|!annotated:"noMock" %} -{% if variable.writeAccess == "public" or variable.writeAccess == "internal" %} - - // MARK: {{ variable.name }} - public var {{ variable.name }}SetCount = 0 - public var {{ variable.name }}Callback:(() -> Void)? -public override var {{ variable.name }}: {{ variable.typeName }}{% if not variable.isOptional %}{% if variable|annotated:"defaultMockValue" %} = {{ variable.annotations.defaultMockValue }}{% else %}{% if variable.isArray %} = []{% endif %}{% if variable.isDictionary %} = [:]{% endif %}{% endif %}{% endif %} { - didSet { - {{ variable.name }}SetCount += 1 - {{ variable.name }}Callback?() - } - } + var {{ method.callName }}CallCount = 0 + var {{ method.callName }}Callback: (() -> Void)? +{% if method.throws %} var {{ method.callName }}ShouldThrow: Error?{% endif %} +{% if method.parameters.count == 1 %} var {{ method.callName }}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}: {{ param.typeName.unwrappedTypeName }}?{% endfor %} +{% else %}{% if not method.parameters.count == 0 %} var {{ method.callName }}ReceivedArguments: ({% for param in method.parameters %}{{ param.name }}: {% if param.typeAttributes.escaping %}{{ param.unwrappedTypeName }}{% else %}{{ param.typeName }}{% endif %}{% if not forloop.last %}, {% endif %}{% endfor %})?{% endif %} {% endif %} -{% endfor %} - -{% if type.annotations.MockInitializerBody %} -public init() { -{{ type.annotations.MockInitializerBody }} -} -{% endif %} - -{% for method in type.allMethods|!annotated:"noMock" %} -{% if not method.shortName == "init" and not method.accessLevel == "private" and not method.accessLevel == "fileprivate" %} -// MARK: - {{ method.annotations.StubName|default:method.callName }} -public var {{ method.annotations.StubName|default:method.callName }}CallCount = 0 -public var {{ method.annotations.StubName|default:method.callName }}Callback: (() -> Void)? -{% if method.throws %}public var {{ method.annotations.StubName|default:method.callName }}ShouldThrow: Error?{% endif %} -{% if method.parameters.count > 2 %}// swiftlint:disable:next large_tuple {% endif %} -{% if method.parameters.count == 1 %}public var {{ method.annotations.StubName|default:method.callName }}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }}: {{ param.typeName.unwrappedTypeName }}?{% endfor %}{% else %}{% if not method.parameters.count == 0 %}var {{ method.annotations.StubName|default:method.callName }}ReceivedArguments: ({% for param in method.parameters %}{{ param.name }}: {% if param.typeAttributes.escaping %}{{ param.unwrappedTypeName }}{% else %}{{ param.typeName }}{% endif %}{% if not forloop.last %}, {% endif %}{% endfor %})?{% endif %}{% endif %} -{% if not method.returnTypeName.isVoid %}public var {{ method.annotations.StubName|default:method.callName }}ReturnValue: {{ method.returnTypeName }}!{% endif %} -public override func {{ method.annotations.StubName|default:method.callName }}({% for param in method.parameters %}{% if param.argumentLabel == nil %}_{% else %}{{ param.argumentLabel }}{% endif %}{% if not param.argumentLabel == param.name %} {{ param.name }}{% endif %}: {{ param.typeName }}{% if not forloop.last %}, {% endif %}{% endfor %}){% if method.throws %} throws{% endif %}{% if not method.returnTypeName.isVoid %} -> {{ method.returnTypeName }}{% endif %} { -{{ method.annotations.StubName|default:method.callName }}CallCount += 1 -{%if method.parameters.count == 1 %}{{ method.annotations.StubName|default:method.callName }}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }} = {{ param.name }}{% endfor %}{% else %}{% if not method.parameters.count == 0 %}{{ method.annotations.StubName|default:method.callName }}ReceivedArguments = ({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %}){% endif %}{% if not method.returnTypeName.isVoid %}{% endif %}{% endif %} -{% if method.throws %}if let {{ method.annotations.StubName|default:method.callName }}ShouldThrow = {{ method.annotations.StubName|default:method.callName }}ShouldThrow { throw {{ method.annotations.StubName|default:method.callName }}ShouldThrow }{% endif %} - {{ method.annotations.StubName|default:method.callName }}Callback?() -{% if not method.returnTypeName.isVoid %}return {{ method.annotations.StubName|default:method.callName }}ReturnValue{% endif %} +{% if not method.returnTypeName.isVoid %} var {{ method.callName }}ReturnValue: {{ method.returnTypeName }}{% if method.annotations.DefaultReturnValue %} = {{ method.annotations.DefaultReturnValue }}{% else %}{% if not method.isOptionalReturnType %}!{% endif %}{% endif %}{% endif %} + func {{ method.shortName }}({% for param in method.parameters %}{% if param.argumentLabel == nil %}_{% else %}{{ param.argumentLabel }}{% endif %}{% if not param.argumentLabel == param.name %} {{ param.name }}{% endif %}: {{ param.typeName }}{% if not forloop.last %}, {% endif %}{% endfor %}){% if method.throws %} throws{% endif %}{% if not method.returnTypeName.isVoid %} -> {{ method.returnTypeName }}{% endif %} { + {{ method.callName }}CallCount += 1 +{%if method.parameters.count == 1 %} {{ method.callName }}Received{% for param in method.parameters %}{{ param.name|upperFirstLetter }} = {{ param.name }}{% endfor %}{% else %}{% if not method.parameters.count == 0 %} {{ method.callName }}ReceivedArguments = ({% for param in method.parameters %}{{ param.name }}: {{ param.name }}{% if not forloop.last%}, {% endif %}{% endfor %}){% endif %}{% if not method.returnTypeName.isVoid %}{% endif %}{% endif %} +{% if method.throws %} if let {{ method.callName }}ShouldThrow = {{ method.callName }}ShouldThrow { throw {{ method.callName }}ShouldThrow }{% endif %} + {{ method.callName }}Callback?() +{% if not method.returnTypeName.isVoid %} + return {{ method.callName }}ReturnValue{% endif %} } -{% endif %} {# Checking for init and access level #} {% endfor %} } {% endif %}