diff --git a/.circleci/config.yml b/.circleci/config.yml index 0b584823..13eabd4a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,7 +12,7 @@ jobs: FL_OUTPUT_DIR: output macos: - xcode: '10.0.0' + xcode: '10.1.0' steps: - checkout diff --git a/CHANGELOG.md b/CHANGELOG.md index 85ae6ae4..191bc2bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All notable changes to the LaunchDarkly iOS SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.14.0] - 2018-12-05 +### Added +- Added `allFlags` property to `LDClient` that provides a dictionary of feature flag keys and values. Accessing feature flags via `allFlags` does not record any analytics events. +- Support for multiple LaunchDarkly projects or environments. Each set of feature flags associated with a mobile key is called an `environment`. + • Added `secondaryMobileKeys` to LDConfig. LDConfig `mobileKey` refers to the *primary* environment, and must be present. All entries in `secondaryMobileKeys` refer to optional *secondary* environments. + NOTE: See `LDClient.h` for the requirements to add `secondaryMobileKeys`. The SDK will throw an `NSInvalidArgumentException` if an attempt is made to set mobile keys that do not meet these requirements. + • Installed `LDClientInterface` protocol used to access secondary environment feature flags. May also be used on the primary environment to provide normalized access to feature flags. + • Adds `environmentForMobileKeyNamed:` to vend an environment (primary or secondary) object conforming to `LDClientInterface`. Use the vended object to access feature flags for the requested environment. + • Adds new constant `kLDPrimaryEnvironmentName` used to vend the primary environment's `LDClientInterface` from `environmentForMobileKeyNamed:`. + +### Changed +- `LDUserBuilder build` method no longer restores cached user attributes. The SDK sets into the `LDUserModel` object only the attributes in the `LDUserBuilder` at the time of the build message. On start, the SDK restores the last cached feature flags, which the SDK will use until the first feature flag update from the server. +- Changed the format for caching feature flags to associate a set of feature flags with a mobile key. Downgrading to an earlier version will be able to store feature flags, but without the environment association. As a result, the SDK will not restore cached feature flags from 2.14.0 if the SDK is downgraded to a version before 2.14.0. +- Installed a URL cache that does not use the `[NSURLSession defaultSession]` or the `[NSURLCache sharedURLCache]`, precluding conflicts with custom client app URL caching. + +### Fixed +- Fixed defect preventing SDK from calling `userUpdated` or `featureFlagDidUpdate` when deleting a feature flag under certain conditions. +- Fixed defect preventing URL caching for feature flag requests using the `REPORT` verb. +- Fixed defect causing the loss of some analytics events when changing users. + ## [2.13.9] - 2018-11-05 ### Fixed - Fixed defect causing a crash when unknown data exists in a feature flag cache. diff --git a/Darkly.xcodeproj/project.pbxproj b/Darkly.xcodeproj/project.pbxproj index efb0d3eb..2902ee94 100644 --- a/Darkly.xcodeproj/project.pbxproj +++ b/Darkly.xcodeproj/project.pbxproj @@ -7,34 +7,35 @@ objects = { /* Begin PBXBuildFile section */ - 3B68CE91357AE8C38617A3CA /* Pods_Darkly_osx.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 782AE9231AE7A6BAA755F6DC /* Pods_Darkly_osx.framework */; }; + 0CCD4D65985E4AF05236C02D /* Pods_Darkly_osx.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1A46AA351E15FC95FE112FF /* Pods_Darkly_osx.framework */; }; + 591FBC28B8F35B8CA416933E /* Pods_DarklyTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56E0675A20537FFDFAD488FC /* Pods_DarklyTests.framework */; }; 690346C81E6872EA00E45133 /* Darkly.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 690346BE1E6872EA00E45133 /* Darkly.framework */; }; 690346F41E68990000E45133 /* Darkly-Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = 690346D81E68990000E45133 /* Darkly-Prefix.pch */; settings = {ATTRIBUTES = (Public, ); }; }; 690346F51E68990000E45133 /* DarklyConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346D91E68990000E45133 /* DarklyConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; 690346F61E68990000E45133 /* DarklyConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DA1E68990000E45133 /* DarklyConstants.m */; }; 690346F71E68990000E45133 /* LDClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DB1E68990000E45133 /* LDClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; 690346F81E68990000E45133 /* LDClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DC1E68990000E45133 /* LDClient.m */; }; - 690346F91E68990000E45133 /* LDClientManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DD1E68990000E45133 /* LDClientManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 690346FA1E68990000E45133 /* LDClientManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DE1E68990000E45133 /* LDClientManager.m */; }; + 690346F91E68990000E45133 /* LDEnvironmentController.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DD1E68990000E45133 /* LDEnvironmentController.h */; }; + 690346FA1E68990000E45133 /* LDEnvironmentController.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DE1E68990000E45133 /* LDEnvironmentController.m */; }; 690346FB1E68990000E45133 /* LDConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DF1E68990000E45133 /* LDConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 690346FC1E68990000E45133 /* LDConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E01E68990000E45133 /* LDConfig.m */; }; 690346FD1E68990000E45133 /* LDEventModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E11E68990000E45133 /* LDEventModel.h */; settings = {ATTRIBUTES = (Private, ); }; }; 690346FE1E68990000E45133 /* LDEventModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E21E68990000E45133 /* LDEventModel.m */; }; 690346FF1E68990000E45133 /* LDFlagConfigModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E31E68990000E45133 /* LDFlagConfigModel.h */; }; 690347001E68990000E45133 /* LDFlagConfigModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E41E68990000E45133 /* LDFlagConfigModel.m */; }; - 690347011E68990000E45133 /* LDPollingManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E51E68990000E45133 /* LDPollingManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 690347011E68990000E45133 /* LDPollingManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E51E68990000E45133 /* LDPollingManager.h */; }; 690347021E68990000E45133 /* LDPollingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E61E68990000E45133 /* LDPollingManager.m */; }; - 690347031E68990000E45133 /* LDRequestManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E71E68990000E45133 /* LDRequestManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 690347031E68990000E45133 /* LDRequestManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E71E68990000E45133 /* LDRequestManager.h */; }; 690347041E68990000E45133 /* LDRequestManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E81E68990000E45133 /* LDRequestManager.m */; }; 690347051E68990000E45133 /* LDUserBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E91E68990000E45133 /* LDUserBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 690347061E68990000E45133 /* LDUserBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346EA1E68990000E45133 /* LDUserBuilder.m */; }; - 690347071E68990000E45133 /* LDUserModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346EB1E68990000E45133 /* LDUserModel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 690347071E68990000E45133 /* LDUserModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346EB1E68990000E45133 /* LDUserModel.h */; }; 690347081E68990000E45133 /* LDUserModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346EC1E68990000E45133 /* LDUserModel.m */; }; - 690347091E68990000E45133 /* LDUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346ED1E68990000E45133 /* LDUtil.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 690347091E68990000E45133 /* LDUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346ED1E68990000E45133 /* LDUtil.h */; }; 6903470A1E68990000E45133 /* LDUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346EE1E68990000E45133 /* LDUtil.m */; }; - 6903470D1E68990000E45133 /* NSDictionary+JSON.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346F21E68990000E45133 /* NSDictionary+JSON.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 6903470E1E68990000E45133 /* NSDictionary+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346F31E68990000E45133 /* NSDictionary+JSON.m */; }; - 690347111E68994500E45133 /* LDDataManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6903470F1E68994500E45133 /* LDDataManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6903470D1E68990000E45133 /* NSDictionary+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346F21E68990000E45133 /* NSDictionary+LaunchDarkly.h */; }; + 6903470E1E68990000E45133 /* NSDictionary+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346F31E68990000E45133 /* NSDictionary+LaunchDarkly.m */; }; + 690347111E68994500E45133 /* LDDataManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6903470F1E68994500E45133 /* LDDataManager.h */; }; 690347121E68994500E45133 /* LDDataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690347101E68994500E45133 /* LDDataManager.m */; }; 690347261E689B9F00E45133 /* LDUserBuilderTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 690347131E689B9F00E45133 /* LDUserBuilderTest.m */; }; 690347291E689B9F00E45133 /* LDEventModelTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 690347171E689B9F00E45133 /* LDEventModelTest.m */; }; @@ -42,7 +43,7 @@ 6903472B1E689B9F00E45133 /* LDPollingManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 690347191E689B9F00E45133 /* LDPollingManagerTest.m */; }; 6903472C1E689B9F00E45133 /* LDClientTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6903471A1E689B9F00E45133 /* LDClientTest.m */; }; 6903472D1E689B9F00E45133 /* LDUtilTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6903471B1E689B9F00E45133 /* LDUtilTest.m */; }; - 6903472E1E689B9F00E45133 /* LDClientManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6903471C1E689B9F00E45133 /* LDClientManagerTest.m */; }; + 6903472E1E689B9F00E45133 /* LDEnvironmentControllerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6903471C1E689B9F00E45133 /* LDEnvironmentControllerTest.m */; }; 6903472F1E689B9F00E45133 /* featureFlags.json in Resources */ = {isa = PBXBuildFile; fileRef = 6903471E1E689B9F00E45133 /* featureFlags.json */; }; 690347301E689B9F00E45133 /* LDConfigTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6903471F1E689B9F00E45133 /* LDConfigTest.m */; }; 690347311E689B9F00E45133 /* LDFlagConfigModelTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 690347211E689B9F00E45133 /* LDFlagConfigModelTest.m */; }; @@ -51,91 +52,90 @@ 69071F7E1EA2A7CA00497F93 /* Darkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 69BAF40B1E9AAB4800747613 /* Darkly.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69071F7F1EA2A7CB00497F93 /* Darkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 69BAF40B1E9AAB4800747613 /* Darkly.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69071F801EA2A7CC00497F93 /* Darkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 69BAF40B1E9AAB4800747613 /* Darkly.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 69A87E971E74712800B88B23 /* LDDataManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6903470F1E68994500E45133 /* LDDataManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69A87E971E74712800B88B23 /* LDDataManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6903470F1E68994500E45133 /* LDDataManager.h */; }; 69A87E981E74712800B88B23 /* LDDataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690347101E68994500E45133 /* LDDataManager.m */; }; 69A87E991E74712800B88B23 /* Darkly-Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = 690346D81E68990000E45133 /* Darkly-Prefix.pch */; settings = {ATTRIBUTES = (Public, ); }; }; 69A87E9A1E74712800B88B23 /* DarklyConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346D91E68990000E45133 /* DarklyConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69A87E9B1E74712800B88B23 /* DarklyConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DA1E68990000E45133 /* DarklyConstants.m */; }; 69A87E9C1E74712800B88B23 /* LDClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DB1E68990000E45133 /* LDClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69A87E9D1E74712800B88B23 /* LDClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DC1E68990000E45133 /* LDClient.m */; }; - 69A87E9E1E74712800B88B23 /* LDClientManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DD1E68990000E45133 /* LDClientManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 69A87E9F1E74712800B88B23 /* LDClientManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DE1E68990000E45133 /* LDClientManager.m */; }; + 69A87E9E1E74712800B88B23 /* LDEnvironmentController.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DD1E68990000E45133 /* LDEnvironmentController.h */; }; + 69A87E9F1E74712800B88B23 /* LDEnvironmentController.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DE1E68990000E45133 /* LDEnvironmentController.m */; }; 69A87EA01E74712800B88B23 /* LDConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DF1E68990000E45133 /* LDConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69A87EA11E74712800B88B23 /* LDConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E01E68990000E45133 /* LDConfig.m */; }; 69A87EA21E74712800B88B23 /* LDEventModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E11E68990000E45133 /* LDEventModel.h */; settings = {ATTRIBUTES = (Private, ); }; }; 69A87EA31E74712800B88B23 /* LDEventModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E21E68990000E45133 /* LDEventModel.m */; }; 69A87EA41E74712800B88B23 /* LDFlagConfigModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E31E68990000E45133 /* LDFlagConfigModel.h */; }; 69A87EA51E74712800B88B23 /* LDFlagConfigModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E41E68990000E45133 /* LDFlagConfigModel.m */; }; - 69A87EA61E74712800B88B23 /* LDPollingManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E51E68990000E45133 /* LDPollingManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69A87EA61E74712800B88B23 /* LDPollingManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E51E68990000E45133 /* LDPollingManager.h */; }; 69A87EA71E74712800B88B23 /* LDPollingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E61E68990000E45133 /* LDPollingManager.m */; }; - 69A87EA81E74712800B88B23 /* LDRequestManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E71E68990000E45133 /* LDRequestManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69A87EA81E74712800B88B23 /* LDRequestManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E71E68990000E45133 /* LDRequestManager.h */; }; 69A87EA91E74712800B88B23 /* LDRequestManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E81E68990000E45133 /* LDRequestManager.m */; }; 69A87EAA1E74712800B88B23 /* LDUserBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E91E68990000E45133 /* LDUserBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69A87EAB1E74712800B88B23 /* LDUserBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346EA1E68990000E45133 /* LDUserBuilder.m */; }; - 69A87EAC1E74712800B88B23 /* LDUserModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346EB1E68990000E45133 /* LDUserModel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69A87EAC1E74712800B88B23 /* LDUserModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346EB1E68990000E45133 /* LDUserModel.h */; }; 69A87EAD1E74712800B88B23 /* LDUserModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346EC1E68990000E45133 /* LDUserModel.m */; }; - 69A87EAE1E74712800B88B23 /* LDUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346ED1E68990000E45133 /* LDUtil.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69A87EAE1E74712800B88B23 /* LDUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346ED1E68990000E45133 /* LDUtil.h */; }; 69A87EAF1E74712800B88B23 /* LDUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346EE1E68990000E45133 /* LDUtil.m */; }; - 69A87EB01E74712800B88B23 /* NSDictionary+JSON.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346F21E68990000E45133 /* NSDictionary+JSON.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 69A87EB11E74712800B88B23 /* NSDictionary+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346F31E68990000E45133 /* NSDictionary+JSON.m */; }; + 69A87EB01E74712800B88B23 /* NSDictionary+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346F21E68990000E45133 /* NSDictionary+LaunchDarkly.h */; }; + 69A87EB11E74712800B88B23 /* NSDictionary+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346F31E68990000E45133 /* NSDictionary+LaunchDarkly.m */; }; 69B205D31EA92ECD00487CA3 /* LDRequestManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 690347141E689B9F00E45133 /* LDRequestManagerTest.m */; }; 69BAF40D1E9AAB4800747613 /* Darkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 69BAF40B1E9AAB4800747613 /* Darkly.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 69BD7E181E6C79910056D70F /* LDDataManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6903470F1E68994500E45133 /* LDDataManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69BD7E181E6C79910056D70F /* LDDataManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6903470F1E68994500E45133 /* LDDataManager.h */; }; 69BD7E191E6C79910056D70F /* LDDataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690347101E68994500E45133 /* LDDataManager.m */; }; 69BD7E1A1E6C79910056D70F /* Darkly-Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = 690346D81E68990000E45133 /* Darkly-Prefix.pch */; settings = {ATTRIBUTES = (Public, ); }; }; 69BD7E1B1E6C79910056D70F /* DarklyConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346D91E68990000E45133 /* DarklyConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69BD7E1C1E6C79910056D70F /* DarklyConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DA1E68990000E45133 /* DarklyConstants.m */; }; 69BD7E1D1E6C79910056D70F /* LDClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DB1E68990000E45133 /* LDClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69BD7E1E1E6C79910056D70F /* LDClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DC1E68990000E45133 /* LDClient.m */; }; - 69BD7E1F1E6C79910056D70F /* LDClientManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DD1E68990000E45133 /* LDClientManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 69BD7E201E6C79910056D70F /* LDClientManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DE1E68990000E45133 /* LDClientManager.m */; }; + 69BD7E1F1E6C79910056D70F /* LDEnvironmentController.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DD1E68990000E45133 /* LDEnvironmentController.h */; }; + 69BD7E201E6C79910056D70F /* LDEnvironmentController.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DE1E68990000E45133 /* LDEnvironmentController.m */; }; 69BD7E211E6C79910056D70F /* LDConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DF1E68990000E45133 /* LDConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69BD7E221E6C79910056D70F /* LDConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E01E68990000E45133 /* LDConfig.m */; }; 69BD7E231E6C79910056D70F /* LDEventModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E11E68990000E45133 /* LDEventModel.h */; settings = {ATTRIBUTES = (Private, ); }; }; 69BD7E241E6C79910056D70F /* LDEventModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E21E68990000E45133 /* LDEventModel.m */; }; 69BD7E251E6C79910056D70F /* LDFlagConfigModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E31E68990000E45133 /* LDFlagConfigModel.h */; }; 69BD7E261E6C79910056D70F /* LDFlagConfigModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E41E68990000E45133 /* LDFlagConfigModel.m */; }; - 69BD7E271E6C79910056D70F /* LDPollingManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E51E68990000E45133 /* LDPollingManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69BD7E271E6C79910056D70F /* LDPollingManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E51E68990000E45133 /* LDPollingManager.h */; }; 69BD7E281E6C79910056D70F /* LDPollingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E61E68990000E45133 /* LDPollingManager.m */; }; - 69BD7E291E6C79910056D70F /* LDRequestManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E71E68990000E45133 /* LDRequestManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69BD7E291E6C79910056D70F /* LDRequestManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E71E68990000E45133 /* LDRequestManager.h */; }; 69BD7E2A1E6C79910056D70F /* LDRequestManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E81E68990000E45133 /* LDRequestManager.m */; }; 69BD7E2B1E6C79910056D70F /* LDUserBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E91E68990000E45133 /* LDUserBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69BD7E2C1E6C79910056D70F /* LDUserBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346EA1E68990000E45133 /* LDUserBuilder.m */; }; - 69BD7E2D1E6C79910056D70F /* LDUserModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346EB1E68990000E45133 /* LDUserModel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69BD7E2D1E6C79910056D70F /* LDUserModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346EB1E68990000E45133 /* LDUserModel.h */; }; 69BD7E2E1E6C79910056D70F /* LDUserModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346EC1E68990000E45133 /* LDUserModel.m */; }; - 69BD7E2F1E6C79910056D70F /* LDUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346ED1E68990000E45133 /* LDUtil.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69BD7E2F1E6C79910056D70F /* LDUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346ED1E68990000E45133 /* LDUtil.h */; }; 69BD7E301E6C79910056D70F /* LDUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346EE1E68990000E45133 /* LDUtil.m */; }; - 69BD7E311E6C79910056D70F /* NSDictionary+JSON.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346F21E68990000E45133 /* NSDictionary+JSON.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 69BD7E321E6C79910056D70F /* NSDictionary+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346F31E68990000E45133 /* NSDictionary+JSON.m */; }; + 69BD7E311E6C79910056D70F /* NSDictionary+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346F21E68990000E45133 /* NSDictionary+LaunchDarkly.h */; }; + 69BD7E321E6C79910056D70F /* NSDictionary+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346F31E68990000E45133 /* NSDictionary+LaunchDarkly.m */; }; 69E5275E1E6E948F00E4B63B /* LDDataManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 690347161E689B9F00E45133 /* LDDataManagerTest.m */; }; 69F3F6921E6BF7F600079A09 /* LDDataManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690347101E68994500E45133 /* LDDataManager.m */; }; - 69F3F6941E6BF80800079A09 /* LDDataManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6903470F1E68994500E45133 /* LDDataManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69F3F6941E6BF80800079A09 /* LDDataManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6903470F1E68994500E45133 /* LDDataManager.h */; }; 69F3F6951E6BF82100079A09 /* DarklyConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346D91E68990000E45133 /* DarklyConstants.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69F3F6961E6BF82100079A09 /* DarklyConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DA1E68990000E45133 /* DarklyConstants.m */; }; 69F3F6971E6BF82C00079A09 /* LDClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DB1E68990000E45133 /* LDClient.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69F3F6981E6BF82C00079A09 /* LDClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DC1E68990000E45133 /* LDClient.m */; }; - 69F3F6991E6BF82C00079A09 /* LDClientManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DD1E68990000E45133 /* LDClientManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 69F3F69A1E6BF82C00079A09 /* LDClientManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DE1E68990000E45133 /* LDClientManager.m */; }; + 69F3F6991E6BF82C00079A09 /* LDEnvironmentController.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DD1E68990000E45133 /* LDEnvironmentController.h */; }; + 69F3F69A1E6BF82C00079A09 /* LDEnvironmentController.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346DE1E68990000E45133 /* LDEnvironmentController.m */; }; 69F3F69B1E6BF82C00079A09 /* LDConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346DF1E68990000E45133 /* LDConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69F3F69C1E6BF82C00079A09 /* LDConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E01E68990000E45133 /* LDConfig.m */; }; 69F3F69D1E6BF82C00079A09 /* LDEventModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E11E68990000E45133 /* LDEventModel.h */; settings = {ATTRIBUTES = (Private, ); }; }; 69F3F69E1E6BF82C00079A09 /* LDEventModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E21E68990000E45133 /* LDEventModel.m */; }; 69F3F69F1E6BF82C00079A09 /* LDFlagConfigModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E31E68990000E45133 /* LDFlagConfigModel.h */; }; 69F3F6A01E6BF82C00079A09 /* LDFlagConfigModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E41E68990000E45133 /* LDFlagConfigModel.m */; }; - 69F3F6A11E6BF82C00079A09 /* LDPollingManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E51E68990000E45133 /* LDPollingManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69F3F6A11E6BF82C00079A09 /* LDPollingManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E51E68990000E45133 /* LDPollingManager.h */; }; 69F3F6A21E6BF82C00079A09 /* LDPollingManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E61E68990000E45133 /* LDPollingManager.m */; }; - 69F3F6A31E6BF82C00079A09 /* LDRequestManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E71E68990000E45133 /* LDRequestManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69F3F6A31E6BF82C00079A09 /* LDRequestManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E71E68990000E45133 /* LDRequestManager.h */; }; 69F3F6A41E6BF82C00079A09 /* LDRequestManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346E81E68990000E45133 /* LDRequestManager.m */; }; 69F3F6A51E6BF82C00079A09 /* LDUserBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346E91E68990000E45133 /* LDUserBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69F3F6A61E6BF82C00079A09 /* LDUserBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346EA1E68990000E45133 /* LDUserBuilder.m */; }; - 69F3F6A71E6BF82C00079A09 /* LDUserModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346EB1E68990000E45133 /* LDUserModel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69F3F6A71E6BF82C00079A09 /* LDUserModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346EB1E68990000E45133 /* LDUserModel.h */; }; 69F3F6A81E6BF82C00079A09 /* LDUserModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346EC1E68990000E45133 /* LDUserModel.m */; }; - 69F3F6A91E6BF82C00079A09 /* LDUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346ED1E68990000E45133 /* LDUtil.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69F3F6A91E6BF82C00079A09 /* LDUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346ED1E68990000E45133 /* LDUtil.h */; }; 69F3F6AA1E6BF82C00079A09 /* LDUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346EE1E68990000E45133 /* LDUtil.m */; }; - 69F3F6AB1E6BF82C00079A09 /* NSDictionary+JSON.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346F21E68990000E45133 /* NSDictionary+JSON.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 69F3F6AC1E6BF82C00079A09 /* NSDictionary+JSON.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346F31E68990000E45133 /* NSDictionary+JSON.m */; }; + 69F3F6AB1E6BF82C00079A09 /* NSDictionary+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 690346F21E68990000E45133 /* NSDictionary+LaunchDarkly.h */; }; + 69F3F6AC1E6BF82C00079A09 /* NSDictionary+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 690346F31E68990000E45133 /* NSDictionary+LaunchDarkly.m */; }; 69F3F6AE1E6BF84B00079A09 /* Darkly-Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = 690346D81E68990000E45133 /* Darkly-Prefix.pch */; settings = {ATTRIBUTES = (Public, ); }; }; - 76F29BB985D7A699111326A6 /* Pods_Darkly_watchOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BC617F04C8F0C71D0C8026AE /* Pods_Darkly_watchOS.framework */; }; 8305EC6720221973002F20DB /* LDFlagConfigValue.h in Headers */ = {isa = PBXBuildFile; fileRef = 8305EC6520221973002F20DB /* LDFlagConfigValue.h */; }; 8305EC6820221973002F20DB /* LDFlagConfigValue.h in Headers */ = {isa = PBXBuildFile; fileRef = 8305EC6520221973002F20DB /* LDFlagConfigValue.h */; }; 8305EC6920221973002F20DB /* LDFlagConfigValue.h in Headers */ = {isa = PBXBuildFile; fileRef = 8305EC6520221973002F20DB /* LDFlagConfigValue.h */; }; @@ -156,7 +156,10 @@ 830C2ACC207579AC001D645D /* LDThrottler.m in Sources */ = {isa = PBXBuildFile; fileRef = 830C2AC4207579AC001D645D /* LDThrottler.m */; }; 830C2ACF20757CE9001D645D /* LDThrottlerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 830C2ACE20757CE9001D645D /* LDThrottlerTest.m */; }; 830C2AD220768697001D645D /* LDThrottler+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 830C2AD120768697001D645D /* LDThrottler+Testable.m */; }; - 83258A3D1F323049008C2133 /* LDClientManager+EventSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 83258A3C1F323049008C2133 /* LDClientManager+EventSource.m */; }; + 831CAE50214B11050066360C /* LDEnvironmentController+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 831CAE4F214B11050066360C /* LDEnvironmentController+Testable.m */; }; + 831CAE53214B13B30066360C /* LDRequestManager+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 831CAE52214B13B30066360C /* LDRequestManager+Testable.m */; }; + 831CAE5A214B25110066360C /* LDRequestManagerDelegateMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 831CAE59214B25110066360C /* LDRequestManagerDelegateMock.m */; }; + 83258A3D1F323049008C2133 /* LDEnvironmentController+EventSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 83258A3C1F323049008C2133 /* LDEnvironmentController+EventSource.m */; }; 83258A401F3244D0008C2133 /* LDUserBuilder+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 83258A3F1F3244D0008C2133 /* LDUserBuilder+Testable.m */; }; 83258A421F32721A008C2133 /* emptyConfig.json in Resources */ = {isa = PBXBuildFile; fileRef = 83258A411F32721A008C2133 /* emptyConfig.json */; }; 83258A441F329EFB008C2133 /* doubleConfigIsADouble-Pi.json in Resources */ = {isa = PBXBuildFile; fileRef = 83258A431F329EFB008C2133 /* doubleConfigIsADouble-Pi.json */; }; @@ -177,6 +180,39 @@ 833D08D01F3B97EB00BEED83 /* NSThread+MainExecutable.m in Sources */ = {isa = PBXBuildFile; fileRef = 833D08CA1F3B97EB00BEED83 /* NSThread+MainExecutable.m */; }; 833D08D11F3B97EB00BEED83 /* NSThread+MainExecutable.m in Sources */ = {isa = PBXBuildFile; fileRef = 833D08CA1F3B97EB00BEED83 /* NSThread+MainExecutable.m */; }; 833D08D21F3B97EB00BEED83 /* NSThread+MainExecutable.m in Sources */ = {isa = PBXBuildFile; fileRef = 833D08CA1F3B97EB00BEED83 /* NSThread+MainExecutable.m */; }; + 8340A42E2164F6D900418027 /* LDEnvironment.h in Headers */ = {isa = PBXBuildFile; fileRef = 8340A42C2164F6D900418027 /* LDEnvironment.h */; }; + 8340A42F2164F6D900418027 /* LDEnvironment.h in Headers */ = {isa = PBXBuildFile; fileRef = 8340A42C2164F6D900418027 /* LDEnvironment.h */; }; + 8340A4302164F6D900418027 /* LDEnvironment.h in Headers */ = {isa = PBXBuildFile; fileRef = 8340A42C2164F6D900418027 /* LDEnvironment.h */; }; + 8340A4312164F6D900418027 /* LDEnvironment.h in Headers */ = {isa = PBXBuildFile; fileRef = 8340A42C2164F6D900418027 /* LDEnvironment.h */; }; + 8340A4322164F6D900418027 /* LDEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = 8340A42D2164F6D900418027 /* LDEnvironment.m */; }; + 8340A4332164F6D900418027 /* LDEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = 8340A42D2164F6D900418027 /* LDEnvironment.m */; }; + 8340A4342164F6D900418027 /* LDEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = 8340A42D2164F6D900418027 /* LDEnvironment.m */; }; + 8340A4352164F6D900418027 /* LDEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = 8340A42D2164F6D900418027 /* LDEnvironment.m */; }; + 8340A4382165007A00418027 /* LDEnvironmentTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8340A4372165007A00418027 /* LDEnvironmentTest.m */; }; + 8342C82D218A50DF000BD8D8 /* LDConfig+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8342C82C218A50DF000BD8D8 /* LDConfig+Testable.m */; }; + 8342C830218A5EBC000BD8D8 /* LDEnvironmentMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 8342C82F218A5EBC000BD8D8 /* LDEnvironmentMock.m */; }; + 8342C833218B5594000BD8D8 /* NSString+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 8342C831218B5594000BD8D8 /* NSString+LaunchDarkly.h */; }; + 8342C834218B5594000BD8D8 /* NSString+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 8342C831218B5594000BD8D8 /* NSString+LaunchDarkly.h */; }; + 8342C835218B5594000BD8D8 /* NSString+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 8342C831218B5594000BD8D8 /* NSString+LaunchDarkly.h */; }; + 8342C836218B5594000BD8D8 /* NSString+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 8342C831218B5594000BD8D8 /* NSString+LaunchDarkly.h */; }; + 8342C837218B5594000BD8D8 /* NSString+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 8342C832218B5594000BD8D8 /* NSString+LaunchDarkly.m */; }; + 8342C838218B5594000BD8D8 /* NSString+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 8342C832218B5594000BD8D8 /* NSString+LaunchDarkly.m */; }; + 8342C839218B5594000BD8D8 /* NSString+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 8342C832218B5594000BD8D8 /* NSString+LaunchDarkly.m */; }; + 8342C83A218B5594000BD8D8 /* NSString+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 8342C832218B5594000BD8D8 /* NSString+LaunchDarkly.m */; }; + 8342C83C218B97DB000BD8D8 /* LDClientInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = 8342C83B218B97DB000BD8D8 /* LDClientInterface.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8342C83D218B97DB000BD8D8 /* LDClientInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = 8342C83B218B97DB000BD8D8 /* LDClientInterface.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8342C83E218B97DB000BD8D8 /* LDClientInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = 8342C83B218B97DB000BD8D8 /* LDClientInterface.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8342C83F218B97DB000BD8D8 /* LDClientInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = 8342C83B218B97DB000BD8D8 /* LDClientInterface.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 834541122171298000001C44 /* LDUserEnvironment.h in Headers */ = {isa = PBXBuildFile; fileRef = 834541102171298000001C44 /* LDUserEnvironment.h */; }; + 834541132171298000001C44 /* LDUserEnvironment.h in Headers */ = {isa = PBXBuildFile; fileRef = 834541102171298000001C44 /* LDUserEnvironment.h */; }; + 834541142171298000001C44 /* LDUserEnvironment.h in Headers */ = {isa = PBXBuildFile; fileRef = 834541102171298000001C44 /* LDUserEnvironment.h */; }; + 834541152171298000001C44 /* LDUserEnvironment.h in Headers */ = {isa = PBXBuildFile; fileRef = 834541102171298000001C44 /* LDUserEnvironment.h */; }; + 834541162171298000001C44 /* LDUserEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = 834541112171298000001C44 /* LDUserEnvironment.m */; }; + 834541172171298000001C44 /* LDUserEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = 834541112171298000001C44 /* LDUserEnvironment.m */; }; + 834541182171298000001C44 /* LDUserEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = 834541112171298000001C44 /* LDUserEnvironment.m */; }; + 834541192171298000001C44 /* LDUserEnvironment.m in Sources */ = {isa = PBXBuildFile; fileRef = 834541112171298000001C44 /* LDUserEnvironment.m */; }; + 8345411B2171318400001C44 /* LDUserEnvironmentTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8345411A2171318400001C44 /* LDUserEnvironmentTest.m */; }; + 8345411E21714F0500001C44 /* LDUserEnvironment+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8345411D21714F0500001C44 /* LDUserEnvironment+Testable.m */; }; 8349F51E1F19352300B1F3DB /* NSDictionary+StringKey_Matchable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8349F51D1F19352300B1F3DB /* NSDictionary+StringKey_Matchable.m */; }; 8358F25A1F4202A300ECE1AF /* LDConfig+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2591F4202A300ECE1AF /* LDConfig+Testable.m */; }; 83620B8520BDF36B00F1F28E /* NSNumber+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 83620B8320BDF36B00F1F28E /* NSNumber+LaunchDarkly.h */; }; @@ -215,8 +251,14 @@ 8369477E1F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyB.json in Resources */ = {isa = PBXBuildFile; fileRef = 836947771F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyB.json */; }; 8369477F1F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyC-keyDValueDiffers.json in Resources */ = {isa = PBXBuildFile; fileRef = 836947781F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyC-keyDValueDiffers.json */; }; 836947801F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyC.json in Resources */ = {isa = PBXBuildFile; fileRef = 836947791F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyC.json */; }; - 836947831F20125F0047697C /* ldClientManagerTestConfigA.json in Resources */ = {isa = PBXBuildFile; fileRef = 836947811F20125F0047697C /* ldClientManagerTestConfigA.json */; }; - 836947841F20125F0047697C /* ldClientManagerTestConfigB.json in Resources */ = {isa = PBXBuildFile; fileRef = 836947821F20125F0047697C /* ldClientManagerTestConfigB.json */; }; + 836947831F20125F0047697C /* ldEnvironmentControllerTestConfigA.json in Resources */ = {isa = PBXBuildFile; fileRef = 836947811F20125F0047697C /* ldEnvironmentControllerTestConfigA.json */; }; + 836947841F20125F0047697C /* ldEnvironmentControllerTestConfigB.json in Resources */ = {isa = PBXBuildFile; fileRef = 836947821F20125F0047697C /* ldEnvironmentControllerTestConfigB.json */; }; + 8371CE7B216BE9690011622A /* LDFlagConfigValue+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8371CE6F216BE9690011622A /* LDFlagConfigValue+Testable.m */; }; + 8371CE7C216BE9690011622A /* LDFlagConfigTracker+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8371CE70216BE9690011622A /* LDFlagConfigTracker+Testable.m */; }; + 8371CE7D216BE9690011622A /* LDFlagCounter+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8371CE71216BE9690011622A /* LDFlagCounter+Testable.m */; }; + 8371CE7E216BE9690011622A /* LDFlagValueCounter+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8371CE75216BE9690011622A /* LDFlagValueCounter+Testable.m */; }; + 8371CE7F216BE9690011622A /* LDFlagConfigModel+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8371CE79216BE9690011622A /* LDFlagConfigModel+Testable.m */; }; + 8371CE80216BE9690011622A /* LDEventTrackingContext+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8371CE7A216BE9690011622A /* LDEventTrackingContext+Testable.m */; }; 8375839A209CCE71004329DD /* LDEventTrackingContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 83758398209CCE71004329DD /* LDEventTrackingContext.h */; }; 8375839B209CCE71004329DD /* LDEventTrackingContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 83758398209CCE71004329DD /* LDEventTrackingContext.h */; }; 8375839C209CCE71004329DD /* LDEventTrackingContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 83758398209CCE71004329DD /* LDEventTrackingContext.h */; }; @@ -226,7 +268,6 @@ 837583A0209CCE71004329DD /* LDEventTrackingContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 83758399209CCE71004329DD /* LDEventTrackingContext.m */; }; 837583A1209CCE71004329DD /* LDEventTrackingContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 83758399209CCE71004329DD /* LDEventTrackingContext.m */; }; 837583A3209CD0A7004329DD /* LDEventTrackingContextTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 837583A2209CD0A7004329DD /* LDEventTrackingContextTest.m */; }; - 837583A7209CD187004329DD /* LDEventTrackingContext+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 837583A6209CD187004329DD /* LDEventTrackingContext+Testable.m */; }; 83889B141F8E93A100A4EF69 /* LDEvent+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 83889B131F8E93A100A4EF69 /* LDEvent+Testable.m */; }; 83889B171F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 83889B151F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.h */; }; 83889B181F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 83889B151F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.h */; }; @@ -236,9 +277,18 @@ 83889B1C1F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 83889B161F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.m */; }; 83889B1D1F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 83889B161F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.m */; }; 83889B1E1F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 83889B161F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.m */; }; + 83926B1C219F68F300D46140 /* LDURLCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 83926B1A219F68F300D46140 /* LDURLCache.h */; }; + 83926B1D219F68F300D46140 /* LDURLCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 83926B1A219F68F300D46140 /* LDURLCache.h */; }; + 83926B1E219F68F300D46140 /* LDURLCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 83926B1A219F68F300D46140 /* LDURLCache.h */; }; + 83926B1F219F68F300D46140 /* LDURLCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 83926B1A219F68F300D46140 /* LDURLCache.h */; }; + 83926B20219F68F300D46140 /* LDURLCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 83926B1B219F68F300D46140 /* LDURLCache.m */; }; + 83926B21219F68F300D46140 /* LDURLCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 83926B1B219F68F300D46140 /* LDURLCache.m */; }; + 83926B22219F68F300D46140 /* LDURLCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 83926B1B219F68F300D46140 /* LDURLCache.m */; }; + 83926B23219F68F300D46140 /* LDURLCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 83926B1B219F68F300D46140 /* LDURLCache.m */; }; + 83926B25219FA85100D46140 /* LDURLCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 83926B24219FA85100D46140 /* LDURLCacheTest.m */; }; 839956E820053081009707D1 /* LDUserModel+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 839956E720053081009707D1 /* LDUserModel+Testable.m */; }; - 839E0A16201F97E900DB8CD1 /* ldClientManagerTestPatchIsANumber.json in Resources */ = {isa = PBXBuildFile; fileRef = 839E0A15201F97E800DB8CD1 /* ldClientManagerTestPatchIsANumber.json */; }; - 839E0A18201FB8D900DB8CD1 /* ldClientManagerTestDeleteIsANumber.json in Resources */ = {isa = PBXBuildFile; fileRef = 839E0A17201FB8D900DB8CD1 /* ldClientManagerTestDeleteIsANumber.json */; }; + 839E0A16201F97E900DB8CD1 /* ldEnvironmentControllerTestPatchIsANumber.json in Resources */ = {isa = PBXBuildFile; fileRef = 839E0A15201F97E800DB8CD1 /* ldEnvironmentControllerTestPatchIsANumber.json */; }; + 839E0A18201FB8D900DB8CD1 /* ldEnvironmentControllerTestDeleteIsANumber.json in Resources */ = {isa = PBXBuildFile; fileRef = 839E0A17201FB8D900DB8CD1 /* ldEnvironmentControllerTestDeleteIsANumber.json */; }; 83B62E1720A249A200F2E656 /* NSDateFormatter+JsonHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B62E1520A249A200F2E656 /* NSDateFormatter+JsonHeader.h */; }; 83B62E1820A249A200F2E656 /* NSDateFormatter+JsonHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B62E1520A249A200F2E656 /* NSDateFormatter+JsonHeader.h */; }; 83B62E1920A249A200F2E656 /* NSDateFormatter+JsonHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B62E1520A249A200F2E656 /* NSDateFormatter+JsonHeader.h */; }; @@ -286,7 +336,6 @@ 83C886832087A786004AC82F /* LDFlagValueCounter.m in Sources */ = {isa = PBXBuildFile; fileRef = 83C8867C2087A786004AC82F /* LDFlagValueCounter.m */; }; 83C886842087A786004AC82F /* LDFlagValueCounter.m in Sources */ = {isa = PBXBuildFile; fileRef = 83C8867C2087A786004AC82F /* LDFlagValueCounter.m */; }; 83ECCC882087BF860086F879 /* LDFlagValueCounterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 83ECCC872087BF860086F879 /* LDFlagValueCounterTest.m */; }; - 83ECCC8B2087CA280086F879 /* LDFlagValueCounter+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 83ECCC8A2087CA280086F879 /* LDFlagValueCounter+Testable.m */; }; 83ECCC8E2087D67D0086F879 /* LDFlagCounter.h in Headers */ = {isa = PBXBuildFile; fileRef = 83ECCC8C2087D67D0086F879 /* LDFlagCounter.h */; }; 83ECCC8F2087D67D0086F879 /* LDFlagCounter.h in Headers */ = {isa = PBXBuildFile; fileRef = 83ECCC8C2087D67D0086F879 /* LDFlagCounter.h */; }; 83ECCC902087D67D0086F879 /* LDFlagCounter.h in Headers */ = {isa = PBXBuildFile; fileRef = 83ECCC8C2087D67D0086F879 /* LDFlagCounter.h */; }; @@ -296,9 +345,7 @@ 83ECCC942087D67D0086F879 /* LDFlagCounter.m in Sources */ = {isa = PBXBuildFile; fileRef = 83ECCC8D2087D67D0086F879 /* LDFlagCounter.m */; }; 83ECCC952087D67D0086F879 /* LDFlagCounter.m in Sources */ = {isa = PBXBuildFile; fileRef = 83ECCC8D2087D67D0086F879 /* LDFlagCounter.m */; }; 83ECCC982087DF040086F879 /* LDFlagCounterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 83ECCC972087DF040086F879 /* LDFlagCounterTest.m */; }; - 83ECCC9B2087EF4A0086F879 /* LDFlagConfigValue+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 83ECCC9A2087EF4A0086F879 /* LDFlagConfigValue+Testable.m */; }; 83ECCC9D208800F20086F879 /* doubleConfigIsADouble-e.json in Resources */ = {isa = PBXBuildFile; fileRef = 83ECCC9C208800F10086F879 /* doubleConfigIsADouble-e.json */; }; - 83ECCCA32088E4E90086F879 /* LDFlagCounter+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 83ECCCA22088E4E90086F879 /* LDFlagCounter+Testable.m */; }; 83ECCCA62088FBA80086F879 /* LDFlagConfigTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 83ECCCA42088FBA80086F879 /* LDFlagConfigTracker.h */; }; 83ECCCA72088FBA80086F879 /* LDFlagConfigTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 83ECCCA42088FBA80086F879 /* LDFlagConfigTracker.h */; }; 83ECCCA82088FBA80086F879 /* LDFlagConfigTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 83ECCCA42088FBA80086F879 /* LDFlagConfigTracker.h */; }; @@ -308,7 +355,6 @@ 83ECCCAC2088FBA80086F879 /* LDFlagConfigTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 83ECCCA52088FBA80086F879 /* LDFlagConfigTracker.m */; }; 83ECCCAD2088FBA80086F879 /* LDFlagConfigTracker.m in Sources */ = {isa = PBXBuildFile; fileRef = 83ECCCA52088FBA80086F879 /* LDFlagConfigTracker.m */; }; 83ECCCAF2088FF420086F879 /* LDFlagConfigTrackerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 83ECCCAE2088FF420086F879 /* LDFlagConfigTrackerTest.m */; }; - 83ECCCB220890A430086F879 /* LDFlagConfigTracker+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 83ECCCB120890A430086F879 /* LDFlagConfigTracker+Testable.m */; }; 83ECCCB520891E8E0086F879 /* NSDate+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 83ECCCB420891E8E0086F879 /* NSDate+Testable.m */; }; 83EF67811F979B4100403126 /* LDEvent+Unauthorized.h in Headers */ = {isa = PBXBuildFile; fileRef = 83EF677F1F979B4100403126 /* LDEvent+Unauthorized.h */; }; 83EF67821F979B4100403126 /* LDEvent+Unauthorized.h in Headers */ = {isa = PBXBuildFile; fileRef = 83EF677F1F979B4100403126 /* LDEvent+Unauthorized.h */; }; @@ -318,8 +364,25 @@ 83EF67861F979B4100403126 /* LDEvent+Unauthorized.m in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67801F979B4100403126 /* LDEvent+Unauthorized.m */; }; 83EF67871F979B4100403126 /* LDEvent+Unauthorized.m in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67801F979B4100403126 /* LDEvent+Unauthorized.m */; }; 83EF67881F979B4100403126 /* LDEvent+Unauthorized.m in Sources */ = {isa = PBXBuildFile; fileRef = 83EF67801F979B4100403126 /* LDEvent+Unauthorized.m */; }; - 83EF678D1F98FC9200403126 /* LDFlagConfigModel+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 83EF678C1F98FC9200403126 /* LDFlagConfigModel+Testable.m */; }; 83EF67901F99365600403126 /* LDClient+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 83EF678F1F99365600403126 /* LDClient+Testable.m */; }; + 83F3E40D21826CD200CDFC7D /* ClientDelegateMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F3E40C21826CD200CDFC7D /* ClientDelegateMock.m */; }; + 83F569CD21A3428200FF7A5C /* NSURLSession+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F569CB21A3428200FF7A5C /* NSURLSession+LaunchDarkly.h */; }; + 83F569CE21A3428200FF7A5C /* NSURLSession+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F569CB21A3428200FF7A5C /* NSURLSession+LaunchDarkly.h */; }; + 83F569CF21A3428200FF7A5C /* NSURLSession+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F569CB21A3428200FF7A5C /* NSURLSession+LaunchDarkly.h */; }; + 83F569D021A3428200FF7A5C /* NSURLSession+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F569CB21A3428200FF7A5C /* NSURLSession+LaunchDarkly.h */; }; + 83F569D121A3428200FF7A5C /* NSURLSession+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F569CC21A3428200FF7A5C /* NSURLSession+LaunchDarkly.m */; }; + 83F569D221A3428200FF7A5C /* NSURLSession+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F569CC21A3428200FF7A5C /* NSURLSession+LaunchDarkly.m */; }; + 83F569D321A3428200FF7A5C /* NSURLSession+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F569CC21A3428200FF7A5C /* NSURLSession+LaunchDarkly.m */; }; + 83F569D421A3428200FF7A5C /* NSURLSession+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F569CC21A3428200FF7A5C /* NSURLSession+LaunchDarkly.m */; }; + 83F569D621A3604700FF7A5C /* NSURLSession+LaunchDarklyTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F569D521A3604700FF7A5C /* NSURLSession+LaunchDarklyTest.m */; }; + 83F569D921A5E1B400FF7A5C /* LDConfig+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F569D721A5E1B400FF7A5C /* LDConfig+LaunchDarkly.h */; }; + 83F569DA21A5E1B400FF7A5C /* LDConfig+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F569D721A5E1B400FF7A5C /* LDConfig+LaunchDarkly.h */; }; + 83F569DB21A5E1B400FF7A5C /* LDConfig+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F569D721A5E1B400FF7A5C /* LDConfig+LaunchDarkly.h */; }; + 83F569DC21A5E1B400FF7A5C /* LDConfig+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F569D721A5E1B400FF7A5C /* LDConfig+LaunchDarkly.h */; }; + 83F569DD21A5E1B400FF7A5C /* LDConfig+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F569D821A5E1B400FF7A5C /* LDConfig+LaunchDarkly.m */; }; + 83F569DE21A5E1B400FF7A5C /* LDConfig+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F569D821A5E1B400FF7A5C /* LDConfig+LaunchDarkly.m */; }; + 83F569DF21A5E1B400FF7A5C /* LDConfig+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F569D821A5E1B400FF7A5C /* LDConfig+LaunchDarkly.m */; }; + 83F569E021A5E1B400FF7A5C /* LDConfig+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F569D821A5E1B400FF7A5C /* LDConfig+LaunchDarkly.m */; }; 83F5B4751F91560300174DF7 /* LDDataManager+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F5B4741F91560300174DF7 /* LDDataManager+Testable.m */; }; 83F5B4781F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F5B4761F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.h */; }; 83F5B4791F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F5B4761F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.h */; }; @@ -329,9 +392,9 @@ 83F5B47D1F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F5B4771F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.m */; }; 83F5B47E1F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F5B4771F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.m */; }; 83F5B47F1F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.m in Sources */ = {isa = PBXBuildFile; fileRef = 83F5B4771F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.m */; }; - 9D9381DB32C21A64201D531E /* Pods_DarklyTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD4E6D89FED62F00F62E7CFF /* Pods_DarklyTests.framework */; }; - A2A846DDAF45254E478C2F9F /* Pods_Darkly_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7E5AC66F7E94570F819695FC /* Pods_Darkly_tvOS.framework */; }; - DBEFC809C8ECA37472B7614F /* Pods_Darkly_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 686FE687663DF73D33CF4F91 /* Pods_Darkly_iOS.framework */; }; + DEE7C6708471EDFB2BF9D870 /* Pods_Darkly_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D14168C4F3210BD12DC485DD /* Pods_Darkly_iOS.framework */; }; + E94220DDD50D5A32436F7C4F /* Pods_Darkly_watchOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7DD90E1CF4043F3C256961D4 /* Pods_Darkly_watchOS.framework */; }; + F151BECE1BDE5EBEC4079B01 /* Pods_Darkly_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BD4C5E7B6CAA22D0DD7B4E51 /* Pods_Darkly_tvOS.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -345,11 +408,11 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 163B00D14362D3130CADF696 /* Pods-Darkly_tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_tvOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_tvOS/Pods-Darkly_tvOS.release.xcconfig"; sourceTree = ""; }; - 1EA7421D138B21221D7B5076 /* Pods-DarklyTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DarklyTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests.debug.xcconfig"; sourceTree = ""; }; - 276A54AE74C43EE452D1400D /* Pods-Darkly_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_iOS/Pods-Darkly_iOS.debug.xcconfig"; sourceTree = ""; }; - 4A1B843329DED272F522946F /* Pods-Darkly_watchOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_watchOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_watchOS/Pods-Darkly_watchOS.release.xcconfig"; sourceTree = ""; }; - 686FE687663DF73D33CF4F91 /* Pods_Darkly_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Darkly_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 13A3188F82A743ED134620FA /* Pods-Darkly_osx.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_osx.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_osx/Pods-Darkly_osx.debug.xcconfig"; sourceTree = ""; }; + 43D1AA6A655A94B08842C52D /* Pods-Darkly_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_iOS/Pods-Darkly_iOS.debug.xcconfig"; sourceTree = ""; }; + 56E0675A20537FFDFAD488FC /* Pods_DarklyTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DarklyTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5D56A157FE2544DC8977D3C2 /* Pods-Darkly_tvOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_tvOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_tvOS/Pods-Darkly_tvOS.release.xcconfig"; sourceTree = ""; }; + 5F9C63CF10818385D9F5E225 /* Pods-Darkly_watchOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_watchOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_watchOS/Pods-Darkly_watchOS.release.xcconfig"; sourceTree = ""; }; 690346BE1E6872EA00E45133 /* Darkly.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Darkly.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 690346C71E6872EA00E45133 /* DarklyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DarklyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 690346CE1E6872EA00E45133 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -358,8 +421,8 @@ 690346DA1E68990000E45133 /* DarklyConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DarklyConstants.m; sourceTree = ""; }; 690346DB1E68990000E45133 /* LDClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LDClient.h; sourceTree = ""; }; 690346DC1E68990000E45133 /* LDClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LDClient.m; sourceTree = ""; }; - 690346DD1E68990000E45133 /* LDClientManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LDClientManager.h; sourceTree = ""; }; - 690346DE1E68990000E45133 /* LDClientManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LDClientManager.m; sourceTree = ""; }; + 690346DD1E68990000E45133 /* LDEnvironmentController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LDEnvironmentController.h; sourceTree = ""; }; + 690346DE1E68990000E45133 /* LDEnvironmentController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LDEnvironmentController.m; sourceTree = ""; }; 690346DF1E68990000E45133 /* LDConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LDConfig.h; sourceTree = ""; }; 690346E01E68990000E45133 /* LDConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LDConfig.m; sourceTree = ""; }; 690346E11E68990000E45133 /* LDEventModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LDEventModel.h; sourceTree = ""; }; @@ -376,8 +439,8 @@ 690346EC1E68990000E45133 /* LDUserModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LDUserModel.m; sourceTree = ""; }; 690346ED1E68990000E45133 /* LDUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LDUtil.h; sourceTree = ""; }; 690346EE1E68990000E45133 /* LDUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LDUtil.m; sourceTree = ""; }; - 690346F21E68990000E45133 /* NSDictionary+JSON.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+JSON.h"; sourceTree = ""; }; - 690346F31E68990000E45133 /* NSDictionary+JSON.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+JSON.m"; sourceTree = ""; }; + 690346F21E68990000E45133 /* NSDictionary+LaunchDarkly.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+LaunchDarkly.h"; sourceTree = ""; }; + 690346F31E68990000E45133 /* NSDictionary+LaunchDarkly.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+LaunchDarkly.m"; sourceTree = ""; }; 6903470F1E68994500E45133 /* LDDataManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LDDataManager.h; sourceTree = ""; }; 690347101E68994500E45133 /* LDDataManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LDDataManager.m; sourceTree = ""; }; 690347131E689B9F00E45133 /* LDUserBuilderTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LDUserBuilderTest.m; sourceTree = ""; }; @@ -388,7 +451,7 @@ 690347191E689B9F00E45133 /* LDPollingManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LDPollingManagerTest.m; sourceTree = ""; }; 6903471A1E689B9F00E45133 /* LDClientTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LDClientTest.m; sourceTree = ""; }; 6903471B1E689B9F00E45133 /* LDUtilTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LDUtilTest.m; sourceTree = ""; }; - 6903471C1E689B9F00E45133 /* LDClientManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LDClientManagerTest.m; sourceTree = ""; }; + 6903471C1E689B9F00E45133 /* LDEnvironmentControllerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LDEnvironmentControllerTest.m; sourceTree = ""; }; 6903471E1E689B9F00E45133 /* featureFlags.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = featureFlags.json; sourceTree = ""; }; 6903471F1E689B9F00E45133 /* LDConfigTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LDConfigTest.m; sourceTree = ""; }; 690347201E689B9F00E45133 /* DarklyXCTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DarklyXCTestCase.h; sourceTree = ""; }; @@ -401,9 +464,7 @@ 69BAF40C1E9AAB4800747613 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Framework/Info.plist; sourceTree = SOURCE_ROOT; }; 69BD7E101E6C79550056D70F /* Darkly.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Darkly.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 69F3F67B1E6BF7C000079A09 /* Darkly.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Darkly.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 782AE9231AE7A6BAA755F6DC /* Pods_Darkly_osx.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Darkly_osx.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 7B3AFF080C28C6F90C108DDA /* Pods-Darkly_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_iOS/Pods-Darkly_iOS.release.xcconfig"; sourceTree = ""; }; - 7E5AC66F7E94570F819695FC /* Pods_Darkly_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Darkly_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7DD90E1CF4043F3C256961D4 /* Pods_Darkly_watchOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Darkly_watchOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8305EC6520221973002F20DB /* LDFlagConfigValue.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LDFlagConfigValue.h; sourceTree = ""; }; 8305EC6620221973002F20DB /* LDFlagConfigValue.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDFlagConfigValue.m; sourceTree = ""; }; 8305EC7B2022336C002F20DB /* LDFlagConfigValueTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDFlagConfigValueTest.m; sourceTree = ""; }; @@ -413,18 +474,39 @@ 830C2ACE20757CE9001D645D /* LDThrottlerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDThrottlerTest.m; sourceTree = ""; }; 830C2AD020768697001D645D /* LDThrottler+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDThrottler+Testable.h"; sourceTree = ""; }; 830C2AD120768697001D645D /* LDThrottler+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDThrottler+Testable.m"; sourceTree = ""; }; - 83258A3B1F323049008C2133 /* LDClientManager+EventSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDClientManager+EventSource.h"; sourceTree = ""; }; - 83258A3C1F323049008C2133 /* LDClientManager+EventSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "LDClientManager+EventSource.m"; sourceTree = ""; }; + 831CAE4E214B11050066360C /* LDEnvironmentController+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDEnvironmentController+Testable.h"; sourceTree = ""; }; + 831CAE4F214B11050066360C /* LDEnvironmentController+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDEnvironmentController+Testable.m"; sourceTree = ""; }; + 831CAE51214B13B30066360C /* LDRequestManager+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDRequestManager+Testable.h"; sourceTree = ""; }; + 831CAE52214B13B30066360C /* LDRequestManager+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDRequestManager+Testable.m"; sourceTree = ""; }; + 831CAE58214B25110066360C /* LDRequestManagerDelegateMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LDRequestManagerDelegateMock.h; sourceTree = ""; }; + 831CAE59214B25110066360C /* LDRequestManagerDelegateMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDRequestManagerDelegateMock.m; sourceTree = ""; }; + 83258A3B1F323049008C2133 /* LDEnvironmentController+EventSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDEnvironmentController+EventSource.h"; sourceTree = ""; }; + 83258A3C1F323049008C2133 /* LDEnvironmentController+EventSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "LDEnvironmentController+EventSource.m"; sourceTree = ""; }; 83258A3E1F3244D0008C2133 /* LDUserBuilder+Testable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDUserBuilder+Testable.h"; sourceTree = ""; }; 83258A3F1F3244D0008C2133 /* LDUserBuilder+Testable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "LDUserBuilder+Testable.m"; sourceTree = ""; }; 83258A411F32721A008C2133 /* emptyConfig.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = emptyConfig.json; sourceTree = ""; }; 83258A431F329EFB008C2133 /* doubleConfigIsADouble-Pi.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "doubleConfigIsADouble-Pi.json"; sourceTree = ""; }; 832C78811F296F3400E334A2 /* NSMutableDictionary+NullRemovable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMutableDictionary+NullRemovable.h"; sourceTree = ""; }; 832C78821F296F3400E334A2 /* NSMutableDictionary+NullRemovable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMutableDictionary+NullRemovable.m"; sourceTree = ""; }; - 832C788B1F2977B800E334A2 /* NSString+RemoveWhitespace.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSString+RemoveWhitespace.h"; path = "../../Darkly/NSString+RemoveWhitespace.h"; sourceTree = ""; }; - 832C788C1F2977B800E334A2 /* NSString+RemoveWhitespace.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSString+RemoveWhitespace.m"; path = "../../Darkly/NSString+RemoveWhitespace.m"; sourceTree = ""; }; + 832C788B1F2977B800E334A2 /* NSString+RemoveWhitespace.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+RemoveWhitespace.h"; sourceTree = ""; }; + 832C788C1F2977B800E334A2 /* NSString+RemoveWhitespace.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+RemoveWhitespace.m"; sourceTree = ""; }; 833D08C91F3B97EB00BEED83 /* NSThread+MainExecutable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSThread+MainExecutable.h"; sourceTree = ""; }; 833D08CA1F3B97EB00BEED83 /* NSThread+MainExecutable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSThread+MainExecutable.m"; sourceTree = ""; }; + 8340A42C2164F6D900418027 /* LDEnvironment.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LDEnvironment.h; sourceTree = ""; }; + 8340A42D2164F6D900418027 /* LDEnvironment.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDEnvironment.m; sourceTree = ""; }; + 8340A4372165007A00418027 /* LDEnvironmentTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDEnvironmentTest.m; sourceTree = ""; }; + 8342C82B218A50DF000BD8D8 /* LDConfig+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDConfig+Testable.h"; sourceTree = ""; }; + 8342C82C218A50DF000BD8D8 /* LDConfig+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDConfig+Testable.m"; sourceTree = ""; }; + 8342C82E218A5EBC000BD8D8 /* LDEnvironmentMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LDEnvironmentMock.h; sourceTree = ""; }; + 8342C82F218A5EBC000BD8D8 /* LDEnvironmentMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDEnvironmentMock.m; sourceTree = ""; }; + 8342C831218B5594000BD8D8 /* NSString+LaunchDarkly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSString+LaunchDarkly.h"; sourceTree = ""; }; + 8342C832218B5594000BD8D8 /* NSString+LaunchDarkly.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSString+LaunchDarkly.m"; sourceTree = ""; }; + 8342C83B218B97DB000BD8D8 /* LDClientInterface.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LDClientInterface.h; sourceTree = ""; }; + 834541102171298000001C44 /* LDUserEnvironment.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LDUserEnvironment.h; sourceTree = ""; }; + 834541112171298000001C44 /* LDUserEnvironment.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDUserEnvironment.m; sourceTree = ""; }; + 8345411A2171318400001C44 /* LDUserEnvironmentTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDUserEnvironmentTest.m; sourceTree = ""; }; + 8345411C21714F0500001C44 /* LDUserEnvironment+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDUserEnvironment+Testable.h"; sourceTree = ""; }; + 8345411D21714F0500001C44 /* LDUserEnvironment+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDUserEnvironment+Testable.m"; sourceTree = ""; }; 8349F51B1F1934A000B1F3DB /* NSDictionary+StringKey_Matchable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+StringKey_Matchable.h"; sourceTree = ""; }; 8349F51D1F19352300B1F3DB /* NSDictionary+StringKey_Matchable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+StringKey_Matchable.m"; sourceTree = ""; }; 8358F2581F4202A300ECE1AF /* LDConfig+Testable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDConfig+Testable.h"; sourceTree = ""; }; @@ -453,21 +535,34 @@ 836947771F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyB.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "dictionaryConfigIsADictionary-KeyB.json"; sourceTree = ""; }; 836947781F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyC-keyDValueDiffers.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "dictionaryConfigIsADictionary-KeyC-keyDValueDiffers.json"; sourceTree = ""; }; 836947791F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyC.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "dictionaryConfigIsADictionary-KeyC.json"; sourceTree = ""; }; - 836947811F20125F0047697C /* ldClientManagerTestConfigA.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ldClientManagerTestConfigA.json; sourceTree = ""; }; - 836947821F20125F0047697C /* ldClientManagerTestConfigB.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ldClientManagerTestConfigB.json; sourceTree = ""; }; + 836947811F20125F0047697C /* ldEnvironmentControllerTestConfigA.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ldEnvironmentControllerTestConfigA.json; sourceTree = ""; }; + 836947821F20125F0047697C /* ldEnvironmentControllerTestConfigB.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ldEnvironmentControllerTestConfigB.json; sourceTree = ""; }; + 8371CE6F216BE9690011622A /* LDFlagConfigValue+Testable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "LDFlagConfigValue+Testable.m"; sourceTree = ""; }; + 8371CE70216BE9690011622A /* LDFlagConfigTracker+Testable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "LDFlagConfigTracker+Testable.m"; sourceTree = ""; }; + 8371CE71216BE9690011622A /* LDFlagCounter+Testable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "LDFlagCounter+Testable.m"; sourceTree = ""; }; + 8371CE72216BE9690011622A /* LDFlagValueCounter+Testable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDFlagValueCounter+Testable.h"; sourceTree = ""; }; + 8371CE73216BE9690011622A /* LDFlagConfigModel+Testable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDFlagConfigModel+Testable.h"; sourceTree = ""; }; + 8371CE74216BE9690011622A /* LDEventTrackingContext+Testable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDEventTrackingContext+Testable.h"; sourceTree = ""; }; + 8371CE75216BE9690011622A /* LDFlagValueCounter+Testable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "LDFlagValueCounter+Testable.m"; sourceTree = ""; }; + 8371CE76216BE9690011622A /* LDFlagCounter+Testable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDFlagCounter+Testable.h"; sourceTree = ""; }; + 8371CE77216BE9690011622A /* LDFlagConfigTracker+Testable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDFlagConfigTracker+Testable.h"; sourceTree = ""; }; + 8371CE78216BE9690011622A /* LDFlagConfigValue+Testable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDFlagConfigValue+Testable.h"; sourceTree = ""; }; + 8371CE79216BE9690011622A /* LDFlagConfigModel+Testable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "LDFlagConfigModel+Testable.m"; sourceTree = ""; }; + 8371CE7A216BE9690011622A /* LDEventTrackingContext+Testable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "LDEventTrackingContext+Testable.m"; sourceTree = ""; }; 83758398209CCE71004329DD /* LDEventTrackingContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LDEventTrackingContext.h; sourceTree = ""; }; 83758399209CCE71004329DD /* LDEventTrackingContext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDEventTrackingContext.m; sourceTree = ""; }; 837583A2209CD0A7004329DD /* LDEventTrackingContextTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDEventTrackingContextTest.m; sourceTree = ""; }; - 837583A5209CD187004329DD /* LDEventTrackingContext+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDEventTrackingContext+Testable.h"; sourceTree = ""; }; - 837583A6209CD187004329DD /* LDEventTrackingContext+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDEventTrackingContext+Testable.m"; sourceTree = ""; }; 83889B121F8E93A100A4EF69 /* LDEvent+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDEvent+Testable.h"; sourceTree = ""; }; 83889B131F8E93A100A4EF69 /* LDEvent+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDEvent+Testable.m"; sourceTree = ""; }; 83889B151F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSURLResponse+LaunchDarkly.h"; sourceTree = ""; }; 83889B161F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSURLResponse+LaunchDarkly.m"; sourceTree = ""; }; + 83926B1A219F68F300D46140 /* LDURLCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LDURLCache.h; sourceTree = ""; }; + 83926B1B219F68F300D46140 /* LDURLCache.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDURLCache.m; sourceTree = ""; }; + 83926B24219FA85100D46140 /* LDURLCacheTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDURLCacheTest.m; sourceTree = ""; }; 839956E620053081009707D1 /* LDUserModel+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDUserModel+Testable.h"; sourceTree = ""; }; 839956E720053081009707D1 /* LDUserModel+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDUserModel+Testable.m"; sourceTree = ""; }; - 839E0A15201F97E800DB8CD1 /* ldClientManagerTestPatchIsANumber.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ldClientManagerTestPatchIsANumber.json; sourceTree = ""; }; - 839E0A17201FB8D900DB8CD1 /* ldClientManagerTestDeleteIsANumber.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ldClientManagerTestDeleteIsANumber.json; sourceTree = ""; }; + 839E0A15201F97E800DB8CD1 /* ldEnvironmentControllerTestPatchIsANumber.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ldEnvironmentControllerTestPatchIsANumber.json; sourceTree = ""; }; + 839E0A17201FB8D900DB8CD1 /* ldEnvironmentControllerTestDeleteIsANumber.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ldEnvironmentControllerTestDeleteIsANumber.json; sourceTree = ""; }; 83B62E1520A249A200F2E656 /* NSDateFormatter+JsonHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSDateFormatter+JsonHeader.h"; sourceTree = ""; }; 83B62E1620A249A200F2E656 /* NSDateFormatter+JsonHeader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSDateFormatter+JsonHeader.m"; sourceTree = ""; }; 83B62E1F20A2517500F2E656 /* NSDateFormatter+JsonHeaderTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSDateFormatter+JsonHeaderTest.m"; sourceTree = ""; }; @@ -497,41 +592,40 @@ 83C8867B2087A786004AC82F /* LDFlagValueCounter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LDFlagValueCounter.h; sourceTree = ""; }; 83C8867C2087A786004AC82F /* LDFlagValueCounter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDFlagValueCounter.m; sourceTree = ""; }; 83ECCC872087BF860086F879 /* LDFlagValueCounterTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDFlagValueCounterTest.m; sourceTree = ""; }; - 83ECCC892087CA280086F879 /* LDFlagValueCounter+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDFlagValueCounter+Testable.h"; sourceTree = ""; }; - 83ECCC8A2087CA280086F879 /* LDFlagValueCounter+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDFlagValueCounter+Testable.m"; sourceTree = ""; }; 83ECCC8C2087D67D0086F879 /* LDFlagCounter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LDFlagCounter.h; sourceTree = ""; }; 83ECCC8D2087D67D0086F879 /* LDFlagCounter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDFlagCounter.m; sourceTree = ""; }; 83ECCC972087DF040086F879 /* LDFlagCounterTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDFlagCounterTest.m; sourceTree = ""; }; - 83ECCC992087EF4A0086F879 /* LDFlagConfigValue+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDFlagConfigValue+Testable.h"; sourceTree = ""; }; - 83ECCC9A2087EF4A0086F879 /* LDFlagConfigValue+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDFlagConfigValue+Testable.m"; sourceTree = ""; }; 83ECCC9C208800F10086F879 /* doubleConfigIsADouble-e.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "doubleConfigIsADouble-e.json"; sourceTree = ""; }; - 83ECCCA12088E4E90086F879 /* LDFlagCounter+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDFlagCounter+Testable.h"; sourceTree = ""; }; - 83ECCCA22088E4E90086F879 /* LDFlagCounter+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDFlagCounter+Testable.m"; sourceTree = ""; }; 83ECCCA42088FBA80086F879 /* LDFlagConfigTracker.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LDFlagConfigTracker.h; sourceTree = ""; }; 83ECCCA52088FBA80086F879 /* LDFlagConfigTracker.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDFlagConfigTracker.m; sourceTree = ""; }; 83ECCCAE2088FF420086F879 /* LDFlagConfigTrackerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LDFlagConfigTrackerTest.m; sourceTree = ""; }; - 83ECCCB020890A430086F879 /* LDFlagConfigTracker+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDFlagConfigTracker+Testable.h"; sourceTree = ""; }; - 83ECCCB120890A430086F879 /* LDFlagConfigTracker+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDFlagConfigTracker+Testable.m"; sourceTree = ""; }; 83ECCCB320891E8E0086F879 /* NSDate+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSDate+Testable.h"; sourceTree = ""; }; 83ECCCB420891E8E0086F879 /* NSDate+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSDate+Testable.m"; sourceTree = ""; }; 83EF677F1F979B4100403126 /* LDEvent+Unauthorized.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDEvent+Unauthorized.h"; sourceTree = ""; }; 83EF67801F979B4100403126 /* LDEvent+Unauthorized.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDEvent+Unauthorized.m"; sourceTree = ""; }; - 83EF678B1F98FC9200403126 /* LDFlagConfigModel+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDFlagConfigModel+Testable.h"; sourceTree = ""; }; - 83EF678C1F98FC9200403126 /* LDFlagConfigModel+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDFlagConfigModel+Testable.m"; sourceTree = ""; }; 83EF678E1F99365600403126 /* LDClient+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDClient+Testable.h"; sourceTree = ""; }; 83EF678F1F99365600403126 /* LDClient+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDClient+Testable.m"; sourceTree = ""; }; + 83F3E40B21826CD200CDFC7D /* ClientDelegateMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ClientDelegateMock.h; sourceTree = ""; }; + 83F3E40C21826CD200CDFC7D /* ClientDelegateMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ClientDelegateMock.m; sourceTree = ""; }; + 83F569CB21A3428200FF7A5C /* NSURLSession+LaunchDarkly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSURLSession+LaunchDarkly.h"; sourceTree = ""; }; + 83F569CC21A3428200FF7A5C /* NSURLSession+LaunchDarkly.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSURLSession+LaunchDarkly.m"; sourceTree = ""; }; + 83F569D521A3604700FF7A5C /* NSURLSession+LaunchDarklyTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSURLSession+LaunchDarklyTest.m"; sourceTree = ""; }; + 83F569D721A5E1B400FF7A5C /* LDConfig+LaunchDarkly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDConfig+LaunchDarkly.h"; sourceTree = ""; }; + 83F569D821A5E1B400FF7A5C /* LDConfig+LaunchDarkly.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDConfig+LaunchDarkly.m"; sourceTree = ""; }; 83F5B4731F91560300174DF7 /* LDDataManager+Testable.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDDataManager+Testable.h"; sourceTree = ""; }; 83F5B4741F91560300174DF7 /* LDDataManager+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDDataManager+Testable.m"; sourceTree = ""; }; 83F5B4761F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSHTTPURLResponse+LaunchDarkly.h"; sourceTree = ""; }; 83F5B4771F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSHTTPURLResponse+LaunchDarkly.m"; sourceTree = ""; }; 83F832FE2152DD0200D8859A /* DarklyEventSource.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DarklyEventSource.framework; path = Carthage/Build/iOS/DarklyEventSource.framework; sourceTree = ""; }; - A4FE8A4EBC82F6EE1F30F34A /* Pods-Darkly_tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_tvOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_tvOS/Pods-Darkly_tvOS.debug.xcconfig"; sourceTree = ""; }; - AD004A636172098D40946666 /* Pods-Darkly_watchOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_watchOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_watchOS/Pods-Darkly_watchOS.debug.xcconfig"; sourceTree = ""; }; - BC617F04C8F0C71D0C8026AE /* Pods_Darkly_watchOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Darkly_watchOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D16369F49E52EF81AF483671 /* Pods-Darkly_osx.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_osx.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_osx/Pods-Darkly_osx.debug.xcconfig"; sourceTree = ""; }; - D1B3611B3CD24D9CF0D0CD8A /* Pods-Darkly_osx.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_osx.release.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_osx/Pods-Darkly_osx.release.xcconfig"; sourceTree = ""; }; - D5D9EB6AC5D1F6AB6C20AABE /* Pods-DarklyTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DarklyTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests.release.xcconfig"; sourceTree = ""; }; - FD4E6D89FED62F00F62E7CFF /* Pods_DarklyTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DarklyTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A4977FD0873245354DEC8287 /* Pods-Darkly_tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_tvOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_tvOS/Pods-Darkly_tvOS.debug.xcconfig"; sourceTree = ""; }; + BD4C5E7B6CAA22D0DD7B4E51 /* Pods_Darkly_tvOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Darkly_tvOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C87C01FFCC54A1DBF76B2D22 /* Pods-Darkly_osx.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_osx.release.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_osx/Pods-Darkly_osx.release.xcconfig"; sourceTree = ""; }; + CA2C32D80AC041361908CED8 /* Pods-Darkly_watchOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_watchOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_watchOS/Pods-Darkly_watchOS.debug.xcconfig"; sourceTree = ""; }; + CBE56EDC74DB21D2C90DCF85 /* Pods-DarklyTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DarklyTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests.debug.xcconfig"; sourceTree = ""; }; + D14168C4F3210BD12DC485DD /* Pods_Darkly_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Darkly_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D1A8F80D8034DD9416A6504C /* Pods-DarklyTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DarklyTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests.release.xcconfig"; sourceTree = ""; }; + E1A46AA351E15FC95FE112FF /* Pods_Darkly_osx.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Darkly_osx.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E33A186E75DDE1C8C9A0C445 /* Pods-Darkly_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Darkly_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Darkly_iOS/Pods-Darkly_iOS.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -539,7 +633,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DBEFC809C8ECA37472B7614F /* Pods_Darkly_iOS.framework in Frameworks */, + DEE7C6708471EDFB2BF9D870 /* Pods_Darkly_iOS.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -548,7 +642,7 @@ buildActionMask = 2147483647; files = ( 690346C81E6872EA00E45133 /* Darkly.framework in Frameworks */, - 9D9381DB32C21A64201D531E /* Pods_DarklyTests.framework in Frameworks */, + 591FBC28B8F35B8CA416933E /* Pods_DarklyTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -556,7 +650,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3B68CE91357AE8C38617A3CA /* Pods_Darkly_osx.framework in Frameworks */, + 0CCD4D65985E4AF05236C02D /* Pods_Darkly_osx.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -564,7 +658,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 76F29BB985D7A699111326A6 /* Pods_Darkly_watchOS.framework in Frameworks */, + E94220DDD50D5A32436F7C4F /* Pods_Darkly_watchOS.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -572,22 +666,39 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A2A846DDAF45254E478C2F9F /* Pods_Darkly_tvOS.framework in Frameworks */, + F151BECE1BDE5EBEC4079B01 /* Pods_Darkly_tvOS.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4B7BE8C3A131975E9E4C8699 /* Pods */ = { + isa = PBXGroup; + children = ( + CBE56EDC74DB21D2C90DCF85 /* Pods-DarklyTests.debug.xcconfig */, + D1A8F80D8034DD9416A6504C /* Pods-DarklyTests.release.xcconfig */, + 43D1AA6A655A94B08842C52D /* Pods-Darkly_iOS.debug.xcconfig */, + E33A186E75DDE1C8C9A0C445 /* Pods-Darkly_iOS.release.xcconfig */, + 13A3188F82A743ED134620FA /* Pods-Darkly_osx.debug.xcconfig */, + C87C01FFCC54A1DBF76B2D22 /* Pods-Darkly_osx.release.xcconfig */, + A4977FD0873245354DEC8287 /* Pods-Darkly_tvOS.debug.xcconfig */, + 5D56A157FE2544DC8977D3C2 /* Pods-Darkly_tvOS.release.xcconfig */, + CA2C32D80AC041361908CED8 /* Pods-Darkly_watchOS.debug.xcconfig */, + 5F9C63CF10818385D9F5E225 /* Pods-Darkly_watchOS.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; 58D80BF3C9396FD83B0FCA6C /* Frameworks */ = { isa = PBXGroup; children = ( 83F832FE2152DD0200D8859A /* DarklyEventSource.framework */, - FD4E6D89FED62F00F62E7CFF /* Pods_DarklyTests.framework */, - 686FE687663DF73D33CF4F91 /* Pods_Darkly_iOS.framework */, - 782AE9231AE7A6BAA755F6DC /* Pods_Darkly_osx.framework */, - 7E5AC66F7E94570F819695FC /* Pods_Darkly_tvOS.framework */, - BC617F04C8F0C71D0C8026AE /* Pods_Darkly_watchOS.framework */, + 56E0675A20537FFDFAD488FC /* Pods_DarklyTests.framework */, + D14168C4F3210BD12DC485DD /* Pods_Darkly_iOS.framework */, + E1A46AA351E15FC95FE112FF /* Pods_Darkly_osx.framework */, + BD4C5E7B6CAA22D0DD7B4E51 /* Pods_Darkly_tvOS.framework */, + 7DD90E1CF4043F3C256961D4 /* Pods_Darkly_watchOS.framework */, ); name = Frameworks; sourceTree = ""; @@ -599,8 +710,8 @@ 690346CB1E6872EA00E45133 /* DarklyTests */, 690346BF1E6872EA00E45133 /* Products */, 839D6D2D1FD1B6BF000BE6BD /* CarthageFrameworks */, - A225377FB9160BD2B21CB824 /* Pods */, 58D80BF3C9396FD83B0FCA6C /* Frameworks */, + 4B7BE8C3A131975E9E4C8699 /* Pods */, ); sourceTree = ""; }; @@ -619,35 +730,14 @@ 690346C01E6872EA00E45133 /* Darkly */ = { isa = PBXGroup; children = ( - 69BAF40B1E9AAB4800747613 /* Darkly.h */, - 6903470F1E68994500E45133 /* LDDataManager.h */, - 690347101E68994500E45133 /* LDDataManager.m */, - 690346D81E68990000E45133 /* Darkly-Prefix.pch */, - 690346D91E68990000E45133 /* DarklyConstants.h */, - 690346DA1E68990000E45133 /* DarklyConstants.m */, + 8342C83B218B97DB000BD8D8 /* LDClientInterface.h */, 690346DB1E68990000E45133 /* LDClient.h */, 690346DC1E68990000E45133 /* LDClient.m */, - 690346DD1E68990000E45133 /* LDClientManager.h */, - 690346DE1E68990000E45133 /* LDClientManager.m */, - 690346DF1E68990000E45133 /* LDConfig.h */, - 690346E01E68990000E45133 /* LDConfig.m */, - 690346E11E68990000E45133 /* LDEventModel.h */, - 690346E21E68990000E45133 /* LDEventModel.m */, - 690346E51E68990000E45133 /* LDPollingManager.h */, - 690346E61E68990000E45133 /* LDPollingManager.m */, - 690346E71E68990000E45133 /* LDRequestManager.h */, - 690346E81E68990000E45133 /* LDRequestManager.m */, - 830C2AC3207579AC001D645D /* LDThrottler.h */, - 830C2AC4207579AC001D645D /* LDThrottler.m */, - 83C886862087A7D4004AC82F /* LDFlagConfig */, - 690346E91E68990000E45133 /* LDUserBuilder.h */, - 690346EA1E68990000E45133 /* LDUserBuilder.m */, - 690346EB1E68990000E45133 /* LDUserModel.h */, - 690346EC1E68990000E45133 /* LDUserModel.m */, - 690346ED1E68990000E45133 /* LDUtil.h */, - 690346EE1E68990000E45133 /* LDUtil.m */, + 83F04192215517A500BEBA79 /* Services */, + 83F041902155169200BEBA79 /* DataModels */, + 83F041912155175E00BEBA79 /* Support */, + 83F041932155183600BEBA79 /* Utilities */, 833D08D31F3B9C4400BEED83 /* Categories */, - 69BAF40C1E9AAB4800747613 /* Info.plist */, ); path = Darkly; sourceTree = ""; @@ -655,17 +745,20 @@ 690346CB1E6872EA00E45133 /* DarklyTests */ = { isa = PBXGroup; children = ( + 831CAE57214B24E50066360C /* Mocks */, 690347151E689B9F00E45133 /* Models */, 6903471D1E689B9F00E45133 /* Fixtures */, 690347231E689B9F00E45133 /* Categories */, 690347131E689B9F00E45133 /* LDUserBuilderTest.m */, 690347141E689B9F00E45133 /* LDRequestManagerTest.m */, 690347191E689B9F00E45133 /* LDPollingManagerTest.m */, + 8340A4372165007A00418027 /* LDEnvironmentTest.m */, 6903471A1E689B9F00E45133 /* LDClientTest.m */, 6903471B1E689B9F00E45133 /* LDUtilTest.m */, - 6903471C1E689B9F00E45133 /* LDClientManagerTest.m */, + 6903471C1E689B9F00E45133 /* LDEnvironmentControllerTest.m */, 6903471F1E689B9F00E45133 /* LDConfigTest.m */, 830C2ACE20757CE9001D645D /* LDThrottlerTest.m */, + 83926B24219FA85100D46140 /* LDURLCacheTest.m */, 690347201E689B9F00E45133 /* DarklyXCTestCase.h */, 690347221E689B9F00E45133 /* DarklyXCTestCase.m */, 690346CE1E6872EA00E45133 /* Info.plist */, @@ -681,6 +774,7 @@ 83ECCC962087DE960086F879 /* LDFlagConfig */, 690347181E689B9F00E45133 /* LDUserModelTest.m */, 83B62E1F20A2517500F2E656 /* NSDateFormatter+JsonHeaderTest.m */, + 8345411A2171318400001C44 /* LDUserEnvironmentTest.m */, ); path = Models; sourceTree = ""; @@ -714,8 +808,8 @@ 836947781F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyC-keyDValueDiffers.json */, 836947791F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyC.json */, 8305EC89202243D6002F20DB /* nullConfigIsANull-null.json */, - 836947811F20125F0047697C /* ldClientManagerTestConfigA.json */, - 836947821F20125F0047697C /* ldClientManagerTestConfigB.json */, + 836947811F20125F0047697C /* ldEnvironmentControllerTestConfigA.json */, + 836947821F20125F0047697C /* ldEnvironmentControllerTestConfigB.json */, 83BE9386201A6AA400DD1ED9 /* ldFlagConfigModelTest.json */, 83BE9388201A781200DD1ED9 /* ldFlagConfigModelPatchVersion1Flag.json */, 83BE9395201A93AC00DD1ED9 /* ldFlagConfigModelPatchVersion2Flag.json */, @@ -723,8 +817,8 @@ 83BE938D201A863C00DD1ED9 /* ldFlagConfigModelPatchNewFlag.json */, 83BE9399201B6EBA00DD1ED9 /* ldFlagConfigModelDeleteVersion2Flag.json */, 83BE939B201B80F800DD1ED9 /* ldFlagConfigModelDeleteNewFlag.json */, - 839E0A15201F97E800DB8CD1 /* ldClientManagerTestPatchIsANumber.json */, - 839E0A17201FB8D900DB8CD1 /* ldClientManagerTestDeleteIsANumber.json */, + 839E0A15201F97E800DB8CD1 /* ldEnvironmentControllerTestPatchIsANumber.json */, + 839E0A17201FB8D900DB8CD1 /* ldEnvironmentControllerTestDeleteIsANumber.json */, ); path = Fixtures; sourceTree = ""; @@ -732,90 +826,108 @@ 690347231E689B9F00E45133 /* Categories */ = { isa = PBXGroup; children = ( - 83BE9392201A8DEE00DD1ED9 /* NSObject+Testable.h */, - 83BE9393201A8DEE00DD1ED9 /* NSObject+Testable.m */, + 831CAE56214B17630066360C /* LDCategories */, + 831CAE55214B17230066360C /* CocoaCategories */, + ); + path = Categories; + sourceTree = ""; + }; + 831CAE55214B17230066360C /* CocoaCategories */ = { + isa = PBXGroup; + children = ( 690347241E689B9F00E45133 /* NSArray+Testable.h */, 690347251E689B9F00E45133 /* NSArray+Testable.m */, + 83ECCCB320891E8E0086F879 /* NSDate+Testable.h */, + 83ECCCB420891E8E0086F879 /* NSDate+Testable.m */, + 83B62E2120A258D400F2E656 /* NSDateFormatter+JsonHeader+Testable.h */, + 83B62E2220A258D400F2E656 /* NSDateFormatter+JsonHeader+Testable.m */, 8349F51B1F1934A000B1F3DB /* NSDictionary+StringKey_Matchable.h */, 8349F51D1F19352300B1F3DB /* NSDictionary+StringKey_Matchable.m */, 83BE938F201A8AD100DD1ED9 /* NSDictionary+Testable.h */, 83BE9390201A8AD100DD1ED9 /* NSDictionary+Testable.m */, - 832C788B1F2977B800E334A2 /* NSString+RemoveWhitespace.h */, - 832C788C1F2977B800E334A2 /* NSString+RemoveWhitespace.m */, + 83B62E2420A275A100F2E656 /* NSHTTPURLResponse+LaunchDarkly+Testable.h */, 83BE938A201A797B00DD1ED9 /* NSJSONSerialization+Testable.h */, 83BE938B201A797B00DD1ED9 /* NSJSONSerialization+Testable.m */, - 839956E620053081009707D1 /* LDUserModel+Testable.h */, - 839956E720053081009707D1 /* LDUserModel+Testable.m */, - 83258A3B1F323049008C2133 /* LDClientManager+EventSource.h */, - 83258A3C1F323049008C2133 /* LDClientManager+EventSource.m */, - 83258A3E1F3244D0008C2133 /* LDUserBuilder+Testable.h */, - 83258A3F1F3244D0008C2133 /* LDUserBuilder+Testable.m */, + 83BE9392201A8DEE00DD1ED9 /* NSObject+Testable.h */, + 83BE9393201A8DEE00DD1ED9 /* NSObject+Testable.m */, + 832C788B1F2977B800E334A2 /* NSString+RemoveWhitespace.h */, + 832C788C1F2977B800E334A2 /* NSString+RemoveWhitespace.m */, + 83F569D521A3604700FF7A5C /* NSURLSession+LaunchDarklyTest.m */, + ); + path = CocoaCategories; + sourceTree = ""; + }; + 831CAE56214B17630066360C /* LDCategories */ = { + isa = PBXGroup; + children = ( + 83EF678E1F99365600403126 /* LDClient+Testable.h */, + 83EF678F1F99365600403126 /* LDClient+Testable.m */, + 83258A3B1F323049008C2133 /* LDEnvironmentController+EventSource.h */, + 83258A3C1F323049008C2133 /* LDEnvironmentController+EventSource.m */, 8358F2581F4202A300ECE1AF /* LDConfig+Testable.h */, 8358F2591F4202A300ECE1AF /* LDConfig+Testable.m */, - 83889B121F8E93A100A4EF69 /* LDEvent+Testable.h */, - 83889B131F8E93A100A4EF69 /* LDEvent+Testable.m */, 83F5B4731F91560300174DF7 /* LDDataManager+Testable.h */, 83F5B4741F91560300174DF7 /* LDDataManager+Testable.m */, - 83EF678E1F99365600403126 /* LDClient+Testable.h */, - 83EF678F1F99365600403126 /* LDClient+Testable.m */, - 837583A4209CD0E0004329DD /* LDFlagConfig */, - 830C2AD020768697001D645D /* LDThrottler+Testable.h */, - 830C2AD120768697001D645D /* LDThrottler+Testable.m */, + 831CAE4E214B11050066360C /* LDEnvironmentController+Testable.h */, + 831CAE4F214B11050066360C /* LDEnvironmentController+Testable.m */, + 83889B121F8E93A100A4EF69 /* LDEvent+Testable.h */, + 83889B131F8E93A100A4EF69 /* LDEvent+Testable.m */, 83B6FC29208127B2002DBA7B /* LDEventModel+Testable.h */, 83B6FC2A208127B2002DBA7B /* LDEventModel+Testable.m */, - 83ECCCB320891E8E0086F879 /* NSDate+Testable.h */, - 83ECCCB420891E8E0086F879 /* NSDate+Testable.m */, - 83B62E2120A258D400F2E656 /* NSDateFormatter+JsonHeader+Testable.h */, - 83B62E2220A258D400F2E656 /* NSDateFormatter+JsonHeader+Testable.m */, - 83B62E2420A275A100F2E656 /* NSHTTPURLResponse+LaunchDarkly+Testable.h */, + 8371CE6E216BE9690011622A /* LDFlagConfig */, + 831CAE51214B13B30066360C /* LDRequestManager+Testable.h */, + 831CAE52214B13B30066360C /* LDRequestManager+Testable.m */, + 830C2AD020768697001D645D /* LDThrottler+Testable.h */, + 830C2AD120768697001D645D /* LDThrottler+Testable.m */, + 83258A3E1F3244D0008C2133 /* LDUserBuilder+Testable.h */, + 83258A3F1F3244D0008C2133 /* LDUserBuilder+Testable.m */, + 8345411C21714F0500001C44 /* LDUserEnvironment+Testable.h */, + 8345411D21714F0500001C44 /* LDUserEnvironment+Testable.m */, + 839956E620053081009707D1 /* LDUserModel+Testable.h */, + 839956E720053081009707D1 /* LDUserModel+Testable.m */, ); - path = Categories; + path = LDCategories; + sourceTree = ""; + }; + 831CAE57214B24E50066360C /* Mocks */ = { + isa = PBXGroup; + children = ( + 83F3E40B21826CD200CDFC7D /* ClientDelegateMock.h */, + 83F3E40C21826CD200CDFC7D /* ClientDelegateMock.m */, + 8342C82B218A50DF000BD8D8 /* LDConfig+Testable.h */, + 8342C82C218A50DF000BD8D8 /* LDConfig+Testable.m */, + 8342C82E218A5EBC000BD8D8 /* LDEnvironmentMock.h */, + 8342C82F218A5EBC000BD8D8 /* LDEnvironmentMock.m */, + 831CAE58214B25110066360C /* LDRequestManagerDelegateMock.h */, + 831CAE59214B25110066360C /* LDRequestManagerDelegateMock.m */, + ); + path = Mocks; sourceTree = ""; }; 833D08D31F3B9C4400BEED83 /* Categories */ = { isa = PBXGroup; children = ( - 690346F21E68990000E45133 /* NSDictionary+JSON.h */, - 690346F31E68990000E45133 /* NSDictionary+JSON.m */, - 832C78811F296F3400E334A2 /* NSMutableDictionary+NullRemovable.h */, - 832C78821F296F3400E334A2 /* NSMutableDictionary+NullRemovable.m */, - 833D08C91F3B97EB00BEED83 /* NSThread+MainExecutable.h */, - 833D08CA1F3B97EB00BEED83 /* NSThread+MainExecutable.m */, - 83889B151F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.h */, - 83889B161F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.m */, - 83F5B4761F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.h */, - 83F5B4771F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.m */, - 83EF677F1F979B4100403126 /* LDEvent+Unauthorized.h */, - 83EF67801F979B4100403126 /* LDEvent+Unauthorized.m */, - 8365E5062028F39E00DE8E2B /* LDEvent+EventTypes.h */, - 8365E5072028F39E00DE8E2B /* LDEvent+EventTypes.m */, - 83B8C24D1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h */, - 83B8C24E1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m */, - 83B62E1520A249A200F2E656 /* NSDateFormatter+JsonHeader.h */, - 83B62E1620A249A200F2E656 /* NSDateFormatter+JsonHeader.m */, - 83B6FC1B207E60B6002DBA7B /* NSDate+ReferencedDate.h */, - 83B6FC1C207E60B6002DBA7B /* NSDate+ReferencedDate.m */, - 83620B8320BDF36B00F1F28E /* NSNumber+LaunchDarkly.h */, - 83620B8420BDF36B00F1F28E /* NSNumber+LaunchDarkly.m */, + 83F041942155188300BEBA79 /* LDCategories */, + 83F041952155189C00BEBA79 /* CocoaCategories */, ); name = Categories; sourceTree = ""; }; - 837583A4209CD0E0004329DD /* LDFlagConfig */ = { + 8371CE6E216BE9690011622A /* LDFlagConfig */ = { isa = PBXGroup; children = ( - 83EF678B1F98FC9200403126 /* LDFlagConfigModel+Testable.h */, - 83EF678C1F98FC9200403126 /* LDFlagConfigModel+Testable.m */, - 83ECCC992087EF4A0086F879 /* LDFlagConfigValue+Testable.h */, - 83ECCC9A2087EF4A0086F879 /* LDFlagConfigValue+Testable.m */, - 83ECCCB020890A430086F879 /* LDFlagConfigTracker+Testable.h */, - 83ECCCB120890A430086F879 /* LDFlagConfigTracker+Testable.m */, - 83ECCCA12088E4E90086F879 /* LDFlagCounter+Testable.h */, - 83ECCCA22088E4E90086F879 /* LDFlagCounter+Testable.m */, - 83ECCC892087CA280086F879 /* LDFlagValueCounter+Testable.h */, - 83ECCC8A2087CA280086F879 /* LDFlagValueCounter+Testable.m */, - 837583A5209CD187004329DD /* LDEventTrackingContext+Testable.h */, - 837583A6209CD187004329DD /* LDEventTrackingContext+Testable.m */, + 8371CE74216BE9690011622A /* LDEventTrackingContext+Testable.h */, + 8371CE7A216BE9690011622A /* LDEventTrackingContext+Testable.m */, + 8371CE73216BE9690011622A /* LDFlagConfigModel+Testable.h */, + 8371CE79216BE9690011622A /* LDFlagConfigModel+Testable.m */, + 8371CE77216BE9690011622A /* LDFlagConfigTracker+Testable.h */, + 8371CE70216BE9690011622A /* LDFlagConfigTracker+Testable.m */, + 8371CE78216BE9690011622A /* LDFlagConfigValue+Testable.h */, + 8371CE6F216BE9690011622A /* LDFlagConfigValue+Testable.m */, + 8371CE76216BE9690011622A /* LDFlagCounter+Testable.h */, + 8371CE71216BE9690011622A /* LDFlagCounter+Testable.m */, + 8371CE72216BE9690011622A /* LDFlagValueCounter+Testable.h */, + 8371CE75216BE9690011622A /* LDFlagValueCounter+Testable.m */, ); path = LDFlagConfig; sourceTree = ""; @@ -859,21 +971,106 @@ path = LDFlagConfig; sourceTree = ""; }; - A225377FB9160BD2B21CB824 /* Pods */ = { + 83F041902155169200BEBA79 /* DataModels */ = { isa = PBXGroup; children = ( - 1EA7421D138B21221D7B5076 /* Pods-DarklyTests.debug.xcconfig */, - D5D9EB6AC5D1F6AB6C20AABE /* Pods-DarklyTests.release.xcconfig */, - 276A54AE74C43EE452D1400D /* Pods-Darkly_iOS.debug.xcconfig */, - 7B3AFF080C28C6F90C108DDA /* Pods-Darkly_iOS.release.xcconfig */, - D16369F49E52EF81AF483671 /* Pods-Darkly_osx.debug.xcconfig */, - D1B3611B3CD24D9CF0D0CD8A /* Pods-Darkly_osx.release.xcconfig */, - A4FE8A4EBC82F6EE1F30F34A /* Pods-Darkly_tvOS.debug.xcconfig */, - 163B00D14362D3130CADF696 /* Pods-Darkly_tvOS.release.xcconfig */, - AD004A636172098D40946666 /* Pods-Darkly_watchOS.debug.xcconfig */, - 4A1B843329DED272F522946F /* Pods-Darkly_watchOS.release.xcconfig */, + 690346D91E68990000E45133 /* DarklyConstants.h */, + 690346DA1E68990000E45133 /* DarklyConstants.m */, + 690346DF1E68990000E45133 /* LDConfig.h */, + 690346E01E68990000E45133 /* LDConfig.m */, + 690346E11E68990000E45133 /* LDEventModel.h */, + 690346E21E68990000E45133 /* LDEventModel.m */, + 83C886862087A7D4004AC82F /* LDFlagConfig */, + 690346E91E68990000E45133 /* LDUserBuilder.h */, + 690346EA1E68990000E45133 /* LDUserBuilder.m */, + 834541102171298000001C44 /* LDUserEnvironment.h */, + 834541112171298000001C44 /* LDUserEnvironment.m */, + 690346EB1E68990000E45133 /* LDUserModel.h */, + 690346EC1E68990000E45133 /* LDUserModel.m */, ); - name = Pods; + path = DataModels; + sourceTree = ""; + }; + 83F041912155175E00BEBA79 /* Support */ = { + isa = PBXGroup; + children = ( + 69BAF40B1E9AAB4800747613 /* Darkly.h */, + 690346D81E68990000E45133 /* Darkly-Prefix.pch */, + 69BAF40C1E9AAB4800747613 /* Info.plist */, + ); + path = Support; + sourceTree = ""; + }; + 83F04192215517A500BEBA79 /* Services */ = { + isa = PBXGroup; + children = ( + 6903470F1E68994500E45133 /* LDDataManager.h */, + 690347101E68994500E45133 /* LDDataManager.m */, + 8340A42C2164F6D900418027 /* LDEnvironment.h */, + 8340A42D2164F6D900418027 /* LDEnvironment.m */, + 690346DD1E68990000E45133 /* LDEnvironmentController.h */, + 690346DE1E68990000E45133 /* LDEnvironmentController.m */, + 690346E51E68990000E45133 /* LDPollingManager.h */, + 690346E61E68990000E45133 /* LDPollingManager.m */, + 690346E71E68990000E45133 /* LDRequestManager.h */, + 690346E81E68990000E45133 /* LDRequestManager.m */, + 830C2AC3207579AC001D645D /* LDThrottler.h */, + 830C2AC4207579AC001D645D /* LDThrottler.m */, + 83926B1A219F68F300D46140 /* LDURLCache.h */, + 83926B1B219F68F300D46140 /* LDURLCache.m */, + ); + path = Services; + sourceTree = ""; + }; + 83F041932155183600BEBA79 /* Utilities */ = { + isa = PBXGroup; + children = ( + 690346ED1E68990000E45133 /* LDUtil.h */, + 690346EE1E68990000E45133 /* LDUtil.m */, + ); + path = Utilities; + sourceTree = ""; + }; + 83F041942155188300BEBA79 /* LDCategories */ = { + isa = PBXGroup; + children = ( + 83EF677F1F979B4100403126 /* LDEvent+Unauthorized.h */, + 83EF67801F979B4100403126 /* LDEvent+Unauthorized.m */, + 8365E5062028F39E00DE8E2B /* LDEvent+EventTypes.h */, + 8365E5072028F39E00DE8E2B /* LDEvent+EventTypes.m */, + 83F569D721A5E1B400FF7A5C /* LDConfig+LaunchDarkly.h */, + 83F569D821A5E1B400FF7A5C /* LDConfig+LaunchDarkly.m */, + ); + name = LDCategories; + sourceTree = ""; + }; + 83F041952155189C00BEBA79 /* CocoaCategories */ = { + isa = PBXGroup; + children = ( + 83B6FC1B207E60B6002DBA7B /* NSDate+ReferencedDate.h */, + 83B6FC1C207E60B6002DBA7B /* NSDate+ReferencedDate.m */, + 83B62E1520A249A200F2E656 /* NSDateFormatter+JsonHeader.h */, + 83B62E1620A249A200F2E656 /* NSDateFormatter+JsonHeader.m */, + 83B8C24D1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h */, + 83B8C24E1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m */, + 690346F21E68990000E45133 /* NSDictionary+LaunchDarkly.h */, + 690346F31E68990000E45133 /* NSDictionary+LaunchDarkly.m */, + 83F5B4761F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.h */, + 83F5B4771F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.m */, + 832C78811F296F3400E334A2 /* NSMutableDictionary+NullRemovable.h */, + 832C78821F296F3400E334A2 /* NSMutableDictionary+NullRemovable.m */, + 83620B8320BDF36B00F1F28E /* NSNumber+LaunchDarkly.h */, + 83620B8420BDF36B00F1F28E /* NSNumber+LaunchDarkly.m */, + 8342C831218B5594000BD8D8 /* NSString+LaunchDarkly.h */, + 8342C832218B5594000BD8D8 /* NSString+LaunchDarkly.m */, + 833D08C91F3B97EB00BEED83 /* NSThread+MainExecutable.h */, + 833D08CA1F3B97EB00BEED83 /* NSThread+MainExecutable.m */, + 83889B151F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.h */, + 83889B161F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.m */, + 83F569CB21A3428200FF7A5C /* NSURLSession+LaunchDarkly.h */, + 83F569CC21A3428200FF7A5C /* NSURLSession+LaunchDarkly.m */, + ); + name = CocoaCategories; sourceTree = ""; }; /* End PBXGroup section */ @@ -883,8 +1080,10 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 69BAF40D1E9AAB4800747613 /* Darkly.h in Headers */, 690346FD1E68990000E45133 /* LDEventModel.h in Headers */, - 690346F91E68990000E45133 /* LDClientManager.h in Headers */, + 83F569D921A5E1B400FF7A5C /* LDConfig+LaunchDarkly.h in Headers */, + 690346F91E68990000E45133 /* LDEnvironmentController.h in Headers */, 83B62E1720A249A200F2E656 /* NSDateFormatter+JsonHeader.h in Headers */, 690346FF1E68990000E45133 /* LDFlagConfigModel.h in Headers */, 83ECCC8E2087D67D0086F879 /* LDFlagCounter.h in Headers */, @@ -893,24 +1092,29 @@ 690346F51E68990000E45133 /* DarklyConstants.h in Headers */, 83620B8520BDF36B00F1F28E /* NSNumber+LaunchDarkly.h in Headers */, 690346FB1E68990000E45133 /* LDConfig.h in Headers */, + 8340A42E2164F6D900418027 /* LDEnvironment.h in Headers */, 690347011E68990000E45133 /* LDPollingManager.h in Headers */, + 83F569CD21A3428200FF7A5C /* NSURLSession+LaunchDarkly.h in Headers */, 690346F41E68990000E45133 /* Darkly-Prefix.pch in Headers */, 690347111E68994500E45133 /* LDDataManager.h in Headers */, 690347031E68990000E45133 /* LDRequestManager.h in Headers */, 690347051E68990000E45133 /* LDUserBuilder.h in Headers */, 8305EC6720221973002F20DB /* LDFlagConfigValue.h in Headers */, 83F5B4781F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.h in Headers */, - 6903470D1E68990000E45133 /* NSDictionary+JSON.h in Headers */, + 6903470D1E68990000E45133 /* NSDictionary+LaunchDarkly.h in Headers */, 83B6FC1D207E60B6002DBA7B /* NSDate+ReferencedDate.h in Headers */, 690347091E68990000E45133 /* LDUtil.h in Headers */, - 69BAF40D1E9AAB4800747613 /* Darkly.h in Headers */, + 834541122171298000001C44 /* LDUserEnvironment.h in Headers */, + 8342C83C218B97DB000BD8D8 /* LDClientInterface.h in Headers */, 830C2AC5207579AC001D645D /* LDThrottler.h in Headers */, 690346F71E68990000E45133 /* LDClient.h in Headers */, 833D08CB1F3B97EB00BEED83 /* NSThread+MainExecutable.h in Headers */, 83C8867D2087A786004AC82F /* LDFlagValueCounter.h in Headers */, 8375839A209CCE71004329DD /* LDEventTrackingContext.h in Headers */, + 83926B1C219F68F300D46140 /* LDURLCache.h in Headers */, 83ECCCA62088FBA80086F879 /* LDFlagConfigTracker.h in Headers */, 83889B171F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.h in Headers */, + 8342C833218B5594000BD8D8 /* NSString+LaunchDarkly.h in Headers */, 690347071E68990000E45133 /* LDUserModel.h in Headers */, 8365E5082028F39E00DE8E2B /* LDEvent+EventTypes.h in Headers */, 832C78831F296F3400E334A2 /* NSMutableDictionary+NullRemovable.h in Headers */, @@ -921,8 +1125,11 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 69071F801EA2A7CC00497F93 /* Darkly.h in Headers */, + 69A87E991E74712800B88B23 /* Darkly-Prefix.pch in Headers */, + 83F569DC21A5E1B400FF7A5C /* LDConfig+LaunchDarkly.h in Headers */, 69A87EA21E74712800B88B23 /* LDEventModel.h in Headers */, - 69A87E9E1E74712800B88B23 /* LDClientManager.h in Headers */, + 69A87E9E1E74712800B88B23 /* LDEnvironmentController.h in Headers */, 83B62E1A20A249A200F2E656 /* NSDateFormatter+JsonHeader.h in Headers */, 69A87EA41E74712800B88B23 /* LDFlagConfigModel.h in Headers */, 83ECCC912087D67D0086F879 /* LDFlagCounter.h in Headers */, @@ -931,24 +1138,28 @@ 69A87E9A1E74712800B88B23 /* DarklyConstants.h in Headers */, 83620B8820BDF36B00F1F28E /* NSNumber+LaunchDarkly.h in Headers */, 69A87EA01E74712800B88B23 /* LDConfig.h in Headers */, + 8340A4312164F6D900418027 /* LDEnvironment.h in Headers */, + 83F569D021A3428200FF7A5C /* NSURLSession+LaunchDarkly.h in Headers */, 69A87EA61E74712800B88B23 /* LDPollingManager.h in Headers */, - 69A87E991E74712800B88B23 /* Darkly-Prefix.pch in Headers */, 69A87E971E74712800B88B23 /* LDDataManager.h in Headers */, 69A87EA81E74712800B88B23 /* LDRequestManager.h in Headers */, - 69A87EB01E74712800B88B23 /* NSDictionary+JSON.h in Headers */, + 69A87EB01E74712800B88B23 /* NSDictionary+LaunchDarkly.h in Headers */, 8305EC6A20221973002F20DB /* LDFlagConfigValue.h in Headers */, 83F5B47B1F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.h in Headers */, 69A87EAA1E74712800B88B23 /* LDUserBuilder.h in Headers */, 83B6FC20207E60B6002DBA7B /* NSDate+ReferencedDate.h in Headers */, 69A87EAE1E74712800B88B23 /* LDUtil.h in Headers */, - 69071F801EA2A7CC00497F93 /* Darkly.h in Headers */, + 834541152171298000001C44 /* LDUserEnvironment.h in Headers */, + 8342C83F218B97DB000BD8D8 /* LDClientInterface.h in Headers */, 830C2AC8207579AC001D645D /* LDThrottler.h in Headers */, 69A87E9C1E74712800B88B23 /* LDClient.h in Headers */, 833D08CE1F3B97EB00BEED83 /* NSThread+MainExecutable.h in Headers */, 83C886802087A786004AC82F /* LDFlagValueCounter.h in Headers */, 8375839D209CCE71004329DD /* LDEventTrackingContext.h in Headers */, + 83926B1F219F68F300D46140 /* LDURLCache.h in Headers */, 83ECCCA92088FBA80086F879 /* LDFlagConfigTracker.h in Headers */, 83889B1A1F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.h in Headers */, + 8342C836218B5594000BD8D8 /* NSString+LaunchDarkly.h in Headers */, 69A87EAC1E74712800B88B23 /* LDUserModel.h in Headers */, 8365E50B2028F39E00DE8E2B /* LDEvent+EventTypes.h in Headers */, 832C78861F296F3400E334A2 /* NSMutableDictionary+NullRemovable.h in Headers */, @@ -959,8 +1170,12 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 69071F7F1EA2A7CB00497F93 /* Darkly.h in Headers */, + 69BD7E1A1E6C79910056D70F /* Darkly-Prefix.pch in Headers */, + 83F569DB21A5E1B400FF7A5C /* LDConfig+LaunchDarkly.h in Headers */, 69BD7E231E6C79910056D70F /* LDEventModel.h in Headers */, - 69BD7E1F1E6C79910056D70F /* LDClientManager.h in Headers */, + 69BD7E1F1E6C79910056D70F /* LDEnvironmentController.h in Headers */, + 69BD7E211E6C79910056D70F /* LDConfig.h in Headers */, 83B62E1920A249A200F2E656 /* NSDateFormatter+JsonHeader.h in Headers */, 69BD7E251E6C79910056D70F /* LDFlagConfigModel.h in Headers */, 83ECCC902087D67D0086F879 /* LDFlagCounter.h in Headers */, @@ -968,25 +1183,28 @@ 83B8C2511FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h in Headers */, 69BD7E1B1E6C79910056D70F /* DarklyConstants.h in Headers */, 83620B8720BDF36B00F1F28E /* NSNumber+LaunchDarkly.h in Headers */, - 69BD7E211E6C79910056D70F /* LDConfig.h in Headers */, + 8340A4302164F6D900418027 /* LDEnvironment.h in Headers */, + 83F569CF21A3428200FF7A5C /* NSURLSession+LaunchDarkly.h in Headers */, 69BD7E271E6C79910056D70F /* LDPollingManager.h in Headers */, - 69BD7E1A1E6C79910056D70F /* Darkly-Prefix.pch in Headers */, 69BD7E181E6C79910056D70F /* LDDataManager.h in Headers */, 69BD7E291E6C79910056D70F /* LDRequestManager.h in Headers */, - 69BD7E311E6C79910056D70F /* NSDictionary+JSON.h in Headers */, + 69BD7E311E6C79910056D70F /* NSDictionary+LaunchDarkly.h in Headers */, 8305EC6920221973002F20DB /* LDFlagConfigValue.h in Headers */, 83F5B47A1F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.h in Headers */, 69BD7E2B1E6C79910056D70F /* LDUserBuilder.h in Headers */, 83B6FC1F207E60B6002DBA7B /* NSDate+ReferencedDate.h in Headers */, 69BD7E2F1E6C79910056D70F /* LDUtil.h in Headers */, - 69071F7F1EA2A7CB00497F93 /* Darkly.h in Headers */, + 834541142171298000001C44 /* LDUserEnvironment.h in Headers */, + 8342C83E218B97DB000BD8D8 /* LDClientInterface.h in Headers */, 830C2AC7207579AC001D645D /* LDThrottler.h in Headers */, 69BD7E1D1E6C79910056D70F /* LDClient.h in Headers */, 833D08CD1F3B97EB00BEED83 /* NSThread+MainExecutable.h in Headers */, 83C8867F2087A786004AC82F /* LDFlagValueCounter.h in Headers */, 8375839C209CCE71004329DD /* LDEventTrackingContext.h in Headers */, + 83926B1E219F68F300D46140 /* LDURLCache.h in Headers */, 83ECCCA82088FBA80086F879 /* LDFlagConfigTracker.h in Headers */, 83889B191F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.h in Headers */, + 8342C835218B5594000BD8D8 /* NSString+LaunchDarkly.h in Headers */, 69BD7E2D1E6C79910056D70F /* LDUserModel.h in Headers */, 8365E50A2028F39E00DE8E2B /* LDEvent+EventTypes.h in Headers */, 832C78851F296F3400E334A2 /* NSMutableDictionary+NullRemovable.h in Headers */, @@ -997,17 +1215,21 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 69071F7E1EA2A7CA00497F93 /* Darkly.h in Headers */, 69F3F6AE1E6BF84B00079A09 /* Darkly-Prefix.pch in Headers */, + 83F569DA21A5E1B400FF7A5C /* LDConfig+LaunchDarkly.h in Headers */, 69F3F6A11E6BF82C00079A09 /* LDPollingManager.h in Headers */, 83B62E1820A249A200F2E656 /* NSDateFormatter+JsonHeader.h in Headers */, 69F3F6941E6BF80800079A09 /* LDDataManager.h in Headers */, 83ECCC8F2087D67D0086F879 /* LDFlagCounter.h in Headers */, 83EF67821F979B4100403126 /* LDEvent+Unauthorized.h in Headers */, 83B8C2501FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h in Headers */, - 69F3F6991E6BF82C00079A09 /* LDClientManager.h in Headers */, + 69F3F6991E6BF82C00079A09 /* LDEnvironmentController.h in Headers */, 83620B8620BDF36B00F1F28E /* NSNumber+LaunchDarkly.h in Headers */, 69F3F6A51E6BF82C00079A09 /* LDUserBuilder.h in Headers */, + 8340A42F2164F6D900418027 /* LDEnvironment.h in Headers */, 69F3F6A91E6BF82C00079A09 /* LDUtil.h in Headers */, + 83F569CE21A3428200FF7A5C /* NSURLSession+LaunchDarkly.h in Headers */, 69F3F69B1E6BF82C00079A09 /* LDConfig.h in Headers */, 69F3F69F1E6BF82C00079A09 /* LDFlagConfigModel.h in Headers */, 69F3F6A71E6BF82C00079A09 /* LDUserModel.h in Headers */, @@ -1016,15 +1238,18 @@ 83F5B4791F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.h in Headers */, 69F3F6971E6BF82C00079A09 /* LDClient.h in Headers */, 83B6FC1E207E60B6002DBA7B /* NSDate+ReferencedDate.h in Headers */, - 69F3F6AB1E6BF82C00079A09 /* NSDictionary+JSON.h in Headers */, - 69071F7E1EA2A7CA00497F93 /* Darkly.h in Headers */, + 69F3F6AB1E6BF82C00079A09 /* NSDictionary+LaunchDarkly.h in Headers */, + 834541132171298000001C44 /* LDUserEnvironment.h in Headers */, + 8342C83D218B97DB000BD8D8 /* LDClientInterface.h in Headers */, 830C2AC6207579AC001D645D /* LDThrottler.h in Headers */, 69F3F6A31E6BF82C00079A09 /* LDRequestManager.h in Headers */, 833D08CC1F3B97EB00BEED83 /* NSThread+MainExecutable.h in Headers */, 83C8867E2087A786004AC82F /* LDFlagValueCounter.h in Headers */, 8375839B209CCE71004329DD /* LDEventTrackingContext.h in Headers */, + 83926B1D219F68F300D46140 /* LDURLCache.h in Headers */, 83ECCCA72088FBA80086F879 /* LDFlagConfigTracker.h in Headers */, 83889B181F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.h in Headers */, + 8342C834218B5594000BD8D8 /* NSString+LaunchDarkly.h in Headers */, 69F3F69D1E6BF82C00079A09 /* LDEventModel.h in Headers */, 8365E5092028F39E00DE8E2B /* LDEvent+EventTypes.h in Headers */, 832C78841F296F3400E334A2 /* NSMutableDictionary+NullRemovable.h in Headers */, @@ -1038,12 +1263,12 @@ isa = PBXNativeTarget; buildConfigurationList = 690346D21E6872EA00E45133 /* Build configuration list for PBXNativeTarget "Darkly_iOS" */; buildPhases = ( - E010A2AD2C3E66F514C172E4 /* [CP] Check Pods Manifest.lock */, + 53B9F3263AF304D1562E57C4 /* [CP] Check Pods Manifest.lock */, 690346B91E6872EA00E45133 /* Sources */, 690346BA1E6872EA00E45133 /* Frameworks */, 690346BB1E6872EA00E45133 /* Headers */, 690346BC1E6872EA00E45133 /* Resources */, - EE2B21A9BA3FE4B2239A1D3B /* [CP] Copy Pods Resources */, + F23929CF983F23647BBABD5C /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1058,12 +1283,12 @@ isa = PBXNativeTarget; buildConfigurationList = 690346D51E6872EA00E45133 /* Build configuration list for PBXNativeTarget "DarklyTests" */; buildPhases = ( - 11B7F406C98C7F3355730C7F /* [CP] Check Pods Manifest.lock */, + 9A748F6347318A95F3695087 /* [CP] Check Pods Manifest.lock */, 690346C31E6872EA00E45133 /* Sources */, 690346C41E6872EA00E45133 /* Frameworks */, 690346C51E6872EA00E45133 /* Resources */, - 6C29AEC6C277E21FB0DE8E96 /* [CP] Embed Pods Frameworks */, - 95CC9B7589DAFEE6CD0A3C24 /* [CP] Copy Pods Resources */, + DEE5AE8A1FD07DC494C4EC44 /* [CP] Embed Pods Frameworks */, + 55BAA74FE498EA7D73CD6358 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1079,12 +1304,12 @@ isa = PBXNativeTarget; buildConfigurationList = 69A87E961E74458900B88B23 /* Build configuration list for PBXNativeTarget "Darkly_osx" */; buildPhases = ( - D02FB84C31D64A9CB8D31E85 /* [CP] Check Pods Manifest.lock */, + 08791DDAB07D63649F5351D7 /* [CP] Check Pods Manifest.lock */, 69A87E8A1E74458900B88B23 /* Sources */, 69A87E8B1E74458900B88B23 /* Frameworks */, 69A87E8C1E74458900B88B23 /* Headers */, 69A87E8D1E74458900B88B23 /* Resources */, - 535C211732CC946B27CD5713 /* [CP] Copy Pods Resources */, + AF0E8E6F2583D140F789954D /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1099,12 +1324,12 @@ isa = PBXNativeTarget; buildConfigurationList = 69BD7E151E6C79550056D70F /* Build configuration list for PBXNativeTarget "Darkly_watchOS" */; buildPhases = ( - D335A295E8A5F33F09B0CB4B /* [CP] Check Pods Manifest.lock */, + A45494B8B8C46D39050A53FA /* [CP] Check Pods Manifest.lock */, 69BD7E0B1E6C79550056D70F /* Sources */, 69BD7E0C1E6C79550056D70F /* Frameworks */, 69BD7E0D1E6C79550056D70F /* Headers */, 69BD7E0E1E6C79550056D70F /* Resources */, - 6D7FF556F8637A039B95CBD6 /* [CP] Copy Pods Resources */, + 11585F4977D283C149EC8A79 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1119,12 +1344,12 @@ isa = PBXNativeTarget; buildConfigurationList = 69F3F68C1E6BF7C100079A09 /* Build configuration list for PBXNativeTarget "Darkly_tvOS" */; buildPhases = ( - FB12D461D566D47116676F50 /* [CP] Check Pods Manifest.lock */, + 58A44A59227DB40D49084958 /* [CP] Check Pods Manifest.lock */, 69F3F6761E6BF7C000079A09 /* Sources */, 69F3F6771E6BF7C000079A09 /* Frameworks */, 69F3F6781E6BF7C000079A09 /* Headers */, 69F3F6791E6BF7C000079A09 /* Resources */, - FAE4293016E0636547DDB179 /* [CP] Copy Pods Resources */, + E9D5326186CD9B34C198D0E6 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -1205,7 +1430,7 @@ 83BE9387201A6AA400DD1ED9 /* ldFlagConfigModelTest.json in Resources */, 83ECCC9D208800F20086F879 /* doubleConfigIsADouble-e.json in Resources */, 836947801F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyC.json in Resources */, - 836947841F20125F0047697C /* ldClientManagerTestConfigB.json in Resources */, + 836947841F20125F0047697C /* ldEnvironmentControllerTestConfigB.json in Resources */, 83BE939C201B80F800DD1ED9 /* ldFlagConfigModelDeleteNewFlag.json in Resources */, 836947711F1FF45B0047697C /* arrayConfigIsAnArray-Empty.json in Resources */, 8369476A1F1FF0A00047697C /* stringConfigIsAStringA-someString.json in Resources */, @@ -1219,14 +1444,14 @@ 8369475C1F1FED400047697C /* boolConfigIsABool-false.json in Resources */, 83B8C2581FEC4C3B0082B8A9 /* featureFlags-excludeNulls.json in Resources */, 83BE9396201A93AC00DD1ED9 /* ldFlagConfigModelPatchVersion2Flag.json in Resources */, - 836947831F20125F0047697C /* ldClientManagerTestConfigA.json in Resources */, - 839E0A16201F97E900DB8CD1 /* ldClientManagerTestPatchIsANumber.json in Resources */, + 836947831F20125F0047697C /* ldEnvironmentControllerTestConfigA.json in Resources */, + 839E0A16201F97E900DB8CD1 /* ldEnvironmentControllerTestPatchIsANumber.json in Resources */, 836947721F1FF45B0047697C /* arrayConfigIsAnArrayA-123.json in Resources */, 8369477C1F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyA.json in Resources */, 8369475E1F1FED400047697C /* boolConfigIsABool2-true.json in Resources */, 83258A441F329EFB008C2133 /* doubleConfigIsADouble-Pi.json in Resources */, 836947641F1FEEB40047697C /* numberConfigIsANumber2-1.json in Resources */, - 839E0A18201FB8D900DB8CD1 /* ldClientManagerTestDeleteIsANumber.json in Resources */, + 839E0A18201FB8D900DB8CD1 /* ldEnvironmentControllerTestDeleteIsANumber.json in Resources */, 8369477B1F1FF80A0047697C /* dictionaryConfigIsADictionary-Empty.json in Resources */, 836947681F1FF0A00047697C /* stringConfigIsAString-someString.json in Resources */, 8369477E1F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyB.json in Resources */, @@ -1263,119 +1488,146 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 11B7F406C98C7F3355730C7F /* [CP] Check Pods Manifest.lock */ = { + 08791DDAB07D63649F5351D7 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-DarklyTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Darkly_osx-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 535C211732CC946B27CD5713 /* [CP] Copy Pods Resources */ = { + 11585F4977D283C149EC8A79 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( ); name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Darkly_osx/Pods-Darkly_osx-resources.sh\"\n"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Darkly_watchOS/Pods-Darkly_watchOS-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 6C29AEC6C277E21FB0DE8E96 /* [CP] Embed Pods Frameworks */ = { + 53B9F3263AF304D1562E57C4 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/DarklyEventSource-iOS/DarklyEventSource.framework", - "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", - "${BUILT_PRODUCTS_DIR}/OHHTTPStubs/OHHTTPStubs.framework", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DarklyEventSource.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OHHTTPStubs.framework", + "$(DERIVED_FILE_DIR)/Pods-Darkly_iOS-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 6D7FF556F8637A039B95CBD6 /* [CP] Copy Pods Resources */ = { + 55BAA74FE498EA7D73CD6358 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( ); name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Darkly_watchOS/Pods-Darkly_watchOS-resources.sh\"\n"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 95CC9B7589DAFEE6CD0A3C24 /* [CP] Copy Pods Resources */ = { + 58A44A59227DB40D49084958 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "[CP] Copy Pods Resources"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Darkly_tvOS-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-resources.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - D02FB84C31D64A9CB8D31E85 /* [CP] Check Pods Manifest.lock */ = { + 9A748F6347318A95F3695087 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Darkly_osx-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-DarklyTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - D335A295E8A5F33F09B0CB4B /* [CP] Check Pods Manifest.lock */ = { + A45494B8B8C46D39050A53FA /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Darkly_watchOS-checkManifestLockResult.txt", ); @@ -1384,47 +1636,63 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - E010A2AD2C3E66F514C172E4 /* [CP] Check Pods Manifest.lock */ = { + AF0E8E6F2583D140F789954D /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Darkly_iOS-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Darkly_osx/Pods-Darkly_osx-resources.sh\"\n"; showEnvVarsInLog = 0; }; - EE2B21A9BA3FE4B2239A1D3B /* [CP] Copy Pods Resources */ = { + DEE5AE8A1FD07DC494C4EC44 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", + "${BUILT_PRODUCTS_DIR}/OHHTTPStubs/OHHTTPStubs.framework", + "${BUILT_PRODUCTS_DIR}/DarklyEventSource-iOS/DarklyEventSource.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( ); - name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OHHTTPStubs.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DarklyEventSource.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Darkly_iOS/Pods-Darkly_iOS-resources.sh\"\n"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - FAE4293016E0636547DDB179 /* [CP] Copy Pods Resources */ = { + E9D5326186CD9B34C198D0E6 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( ); name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; @@ -1432,22 +1700,23 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Darkly_tvOS/Pods-Darkly_tvOS-resources.sh\"\n"; showEnvVarsInLog = 0; }; - FB12D461D566D47116676F50 /* [CP] Check Pods Manifest.lock */ = { + F23929CF983F23647BBABD5C /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Darkly_tvOS-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Darkly_iOS/Pods-Darkly_iOS-resources.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -1459,29 +1728,35 @@ files = ( 83889B1B1F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.m in Sources */, 8375839E209CCE71004329DD /* LDEventTrackingContext.m in Sources */, + 834541162171298000001C44 /* LDUserEnvironment.m in Sources */, 8365E50C2028F39E00DE8E2B /* LDEvent+EventTypes.m in Sources */, 690346FE1E68990000E45133 /* LDEventModel.m in Sources */, 83B62E1B20A249A200F2E656 /* NSDateFormatter+JsonHeader.m in Sources */, 690347121E68994500E45133 /* LDDataManager.m in Sources */, 832C78871F296F3400E334A2 /* NSMutableDictionary+NullRemovable.m in Sources */, 690347041E68990000E45133 /* LDRequestManager.m in Sources */, + 83926B20219F68F300D46140 /* LDURLCache.m in Sources */, 83ECCCAA2088FBA80086F879 /* LDFlagConfigTracker.m in Sources */, 833D08CF1F3B97EB00BEED83 /* NSThread+MainExecutable.m in Sources */, - 690346FA1E68990000E45133 /* LDClientManager.m in Sources */, + 690346FA1E68990000E45133 /* LDEnvironmentController.m in Sources */, 83F5B47C1F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.m in Sources */, 690346F81E68990000E45133 /* LDClient.m in Sources */, 690346F61E68990000E45133 /* DarklyConstants.m in Sources */, 83B8C2531FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m in Sources */, 8305EC6B20221973002F20DB /* LDFlagConfigValue.m in Sources */, 83EF67851F979B4100403126 /* LDEvent+Unauthorized.m in Sources */, - 6903470E1E68990000E45133 /* NSDictionary+JSON.m in Sources */, + 6903470E1E68990000E45133 /* NSDictionary+LaunchDarkly.m in Sources */, 830C2AC9207579AC001D645D /* LDThrottler.m in Sources */, 690347021E68990000E45133 /* LDPollingManager.m in Sources */, 690346FC1E68990000E45133 /* LDConfig.m in Sources */, + 83F569DD21A5E1B400FF7A5C /* LDConfig+LaunchDarkly.m in Sources */, 690347061E68990000E45133 /* LDUserBuilder.m in Sources */, + 8342C837218B5594000BD8D8 /* NSString+LaunchDarkly.m in Sources */, 83620B8920BDF36B00F1F28E /* NSNumber+LaunchDarkly.m in Sources */, 690347081E68990000E45133 /* LDUserModel.m in Sources */, 6903470A1E68990000E45133 /* LDUtil.m in Sources */, + 8340A4322164F6D900418027 /* LDEnvironment.m in Sources */, + 83F569D121A3428200FF7A5C /* NSURLSession+LaunchDarkly.m in Sources */, 83B6FC21207E60B6002DBA7B /* NSDate+ReferencedDate.m in Sources */, 83C886812087A786004AC82F /* LDFlagValueCounter.m in Sources */, 83ECCC922087D67D0086F879 /* LDFlagCounter.m in Sources */, @@ -1493,48 +1768,59 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 837583A7209CD187004329DD /* LDEventTrackingContext+Testable.m in Sources */, 83ECCCAF2088FF420086F879 /* LDFlagConfigTrackerTest.m in Sources */, - 83EF678D1F98FC9200403126 /* LDFlagConfigModel+Testable.m in Sources */, + 8371CE7E216BE9690011622A /* LDFlagValueCounter+Testable.m in Sources */, 8349F51E1F19352300B1F3DB /* NSDictionary+StringKey_Matchable.m in Sources */, 83B6FC2B208127B2002DBA7B /* LDEventModel+Testable.m in Sources */, 839956E820053081009707D1 /* LDUserModel+Testable.m in Sources */, + 8371CE80216BE9690011622A /* LDEventTrackingContext+Testable.m in Sources */, + 831CAE5A214B25110066360C /* LDRequestManagerDelegateMock.m in Sources */, 690347291E689B9F00E45133 /* LDEventModelTest.m in Sources */, 830C2ACF20757CE9001D645D /* LDThrottlerTest.m in Sources */, 690347301E689B9F00E45133 /* LDConfigTest.m in Sources */, 690347261E689B9F00E45133 /* LDUserBuilderTest.m in Sources */, 832C788D1F2977B800E334A2 /* NSString+RemoveWhitespace.m in Sources */, + 8345411B2171318400001C44 /* LDUserEnvironmentTest.m in Sources */, + 8371CE7D216BE9690011622A /* LDFlagCounter+Testable.m in Sources */, 83ECCC982087DF040086F879 /* LDFlagCounterTest.m in Sources */, 6903472A1E689B9F00E45133 /* LDUserModelTest.m in Sources */, - 83ECCCB220890A430086F879 /* LDFlagConfigTracker+Testable.m in Sources */, 6903472B1E689B9F00E45133 /* LDPollingManagerTest.m in Sources */, 83EF67901F99365600403126 /* LDClient+Testable.m in Sources */, - 83258A3D1F323049008C2133 /* LDClientManager+EventSource.m in Sources */, - 83ECCC9B2087EF4A0086F879 /* LDFlagConfigValue+Testable.m in Sources */, + 83258A3D1F323049008C2133 /* LDEnvironmentController+EventSource.m in Sources */, 837583A3209CD0A7004329DD /* LDEventTrackingContextTest.m in Sources */, 83B62E2320A258D400F2E656 /* NSDateFormatter+JsonHeader+Testable.m in Sources */, + 8340A4382165007A00418027 /* LDEnvironmentTest.m in Sources */, 830C2AD220768697001D645D /* LDThrottler+Testable.m in Sources */, 83BE938C201A797B00DD1ED9 /* NSJSONSerialization+Testable.m in Sources */, - 6903472E1E689B9F00E45133 /* LDClientManagerTest.m in Sources */, + 8371CE7B216BE9690011622A /* LDFlagConfigValue+Testable.m in Sources */, + 6903472E1E689B9F00E45133 /* LDEnvironmentControllerTest.m in Sources */, + 83926B25219FA85100D46140 /* LDURLCacheTest.m in Sources */, + 83F3E40D21826CD200CDFC7D /* ClientDelegateMock.m in Sources */, 69E5275E1E6E948F00E4B63B /* LDDataManagerTest.m in Sources */, 83BE9394201A8DEE00DD1ED9 /* NSObject+Testable.m in Sources */, 83ECCC882087BF860086F879 /* LDFlagValueCounterTest.m in Sources */, + 831CAE50214B11050066360C /* LDEnvironmentController+Testable.m in Sources */, 690347321E689B9F00E45133 /* DarklyXCTestCase.m in Sources */, 83889B141F8E93A100A4EF69 /* LDEvent+Testable.m in Sources */, 69B205D31EA92ECD00487CA3 /* LDRequestManagerTest.m in Sources */, 6903472D1E689B9F00E45133 /* LDUtilTest.m in Sources */, + 8371CE7F216BE9690011622A /* LDFlagConfigModel+Testable.m in Sources */, 83B62E2020A2517500F2E656 /* NSDateFormatter+JsonHeaderTest.m in Sources */, - 83ECCC8B2087CA280086F879 /* LDFlagValueCounter+Testable.m in Sources */, + 831CAE53214B13B30066360C /* LDRequestManager+Testable.m in Sources */, 690347311E689B9F00E45133 /* LDFlagConfigModelTest.m in Sources */, + 8342C82D218A50DF000BD8D8 /* LDConfig+Testable.m in Sources */, 8305EC7C2022336C002F20DB /* LDFlagConfigValueTest.m in Sources */, 83BE9391201A8AD100DD1ED9 /* NSDictionary+Testable.m in Sources */, + 8342C830218A5EBC000BD8D8 /* LDEnvironmentMock.m in Sources */, 83258A401F3244D0008C2133 /* LDUserBuilder+Testable.m in Sources */, 690347331E689B9F00E45133 /* NSArray+Testable.m in Sources */, + 8345411E21714F0500001C44 /* LDUserEnvironment+Testable.m in Sources */, 6903472C1E689B9F00E45133 /* LDClientTest.m in Sources */, 8358F25A1F4202A300ECE1AF /* LDConfig+Testable.m in Sources */, - 83ECCCA32088E4E90086F879 /* LDFlagCounter+Testable.m in Sources */, 83ECCCB520891E8E0086F879 /* NSDate+Testable.m in Sources */, + 8371CE7C216BE9690011622A /* LDFlagConfigTracker+Testable.m in Sources */, 83F5B4751F91560300174DF7 /* LDDataManager+Testable.m in Sources */, + 83F569D621A3604700FF7A5C /* NSURLSession+LaunchDarklyTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1544,29 +1830,35 @@ files = ( 83889B1E1F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.m in Sources */, 837583A1209CCE71004329DD /* LDEventTrackingContext.m in Sources */, + 834541192171298000001C44 /* LDUserEnvironment.m in Sources */, 8365E50F2028F39E00DE8E2B /* LDEvent+EventTypes.m in Sources */, 69A87EA31E74712800B88B23 /* LDEventModel.m in Sources */, 83B62E1E20A249A200F2E656 /* NSDateFormatter+JsonHeader.m in Sources */, 69A87E9D1E74712800B88B23 /* LDClient.m in Sources */, 832C788A1F296F3400E334A2 /* NSMutableDictionary+NullRemovable.m in Sources */, 69A87E9B1E74712800B88B23 /* DarklyConstants.m in Sources */, + 83926B23219F68F300D46140 /* LDURLCache.m in Sources */, 83ECCCAD2088FBA80086F879 /* LDFlagConfigTracker.m in Sources */, 833D08D21F3B97EB00BEED83 /* NSThread+MainExecutable.m in Sources */, 69A87EA11E74712800B88B23 /* LDConfig.m in Sources */, 83F5B47F1F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.m in Sources */, 69A87EA51E74712800B88B23 /* LDFlagConfigModel.m in Sources */, - 69A87EB11E74712800B88B23 /* NSDictionary+JSON.m in Sources */, + 69A87EB11E74712800B88B23 /* NSDictionary+LaunchDarkly.m in Sources */, 83B8C2561FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m in Sources */, 8305EC6E20221973002F20DB /* LDFlagConfigValue.m in Sources */, 83EF67881F979B4100403126 /* LDEvent+Unauthorized.m in Sources */, 69A87EAF1E74712800B88B23 /* LDUtil.m in Sources */, 830C2ACC207579AC001D645D /* LDThrottler.m in Sources */, 69A87EA71E74712800B88B23 /* LDPollingManager.m in Sources */, - 69A87E9F1E74712800B88B23 /* LDClientManager.m in Sources */, + 69A87E9F1E74712800B88B23 /* LDEnvironmentController.m in Sources */, + 83F569E021A5E1B400FF7A5C /* LDConfig+LaunchDarkly.m in Sources */, 69A87EAD1E74712800B88B23 /* LDUserModel.m in Sources */, + 8342C83A218B5594000BD8D8 /* NSString+LaunchDarkly.m in Sources */, 83620B8C20BDF36B00F1F28E /* NSNumber+LaunchDarkly.m in Sources */, 69A87E981E74712800B88B23 /* LDDataManager.m in Sources */, 69A87EAB1E74712800B88B23 /* LDUserBuilder.m in Sources */, + 8340A4352164F6D900418027 /* LDEnvironment.m in Sources */, + 83F569D421A3428200FF7A5C /* NSURLSession+LaunchDarkly.m in Sources */, 83B6FC24207E60B6002DBA7B /* NSDate+ReferencedDate.m in Sources */, 83C886842087A786004AC82F /* LDFlagValueCounter.m in Sources */, 83ECCC952087D67D0086F879 /* LDFlagCounter.m in Sources */, @@ -1580,29 +1872,35 @@ files = ( 83889B1D1F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.m in Sources */, 837583A0209CCE71004329DD /* LDEventTrackingContext.m in Sources */, + 834541182171298000001C44 /* LDUserEnvironment.m in Sources */, 8365E50E2028F39E00DE8E2B /* LDEvent+EventTypes.m in Sources */, 69BD7E241E6C79910056D70F /* LDEventModel.m in Sources */, 83B62E1D20A249A200F2E656 /* NSDateFormatter+JsonHeader.m in Sources */, 69BD7E1E1E6C79910056D70F /* LDClient.m in Sources */, 832C78891F296F3400E334A2 /* NSMutableDictionary+NullRemovable.m in Sources */, 69BD7E1C1E6C79910056D70F /* DarklyConstants.m in Sources */, + 83926B22219F68F300D46140 /* LDURLCache.m in Sources */, 83ECCCAC2088FBA80086F879 /* LDFlagConfigTracker.m in Sources */, 833D08D11F3B97EB00BEED83 /* NSThread+MainExecutable.m in Sources */, 69BD7E221E6C79910056D70F /* LDConfig.m in Sources */, 83F5B47E1F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.m in Sources */, 69BD7E261E6C79910056D70F /* LDFlagConfigModel.m in Sources */, - 69BD7E321E6C79910056D70F /* NSDictionary+JSON.m in Sources */, + 69BD7E321E6C79910056D70F /* NSDictionary+LaunchDarkly.m in Sources */, 83B8C2551FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m in Sources */, 8305EC6D20221973002F20DB /* LDFlagConfigValue.m in Sources */, 83EF67871F979B4100403126 /* LDEvent+Unauthorized.m in Sources */, 69BD7E301E6C79910056D70F /* LDUtil.m in Sources */, 830C2ACB207579AC001D645D /* LDThrottler.m in Sources */, 69BD7E281E6C79910056D70F /* LDPollingManager.m in Sources */, - 69BD7E201E6C79910056D70F /* LDClientManager.m in Sources */, + 69BD7E201E6C79910056D70F /* LDEnvironmentController.m in Sources */, + 83F569DF21A5E1B400FF7A5C /* LDConfig+LaunchDarkly.m in Sources */, 69BD7E2E1E6C79910056D70F /* LDUserModel.m in Sources */, + 8342C839218B5594000BD8D8 /* NSString+LaunchDarkly.m in Sources */, 83620B8B20BDF36B00F1F28E /* NSNumber+LaunchDarkly.m in Sources */, 69BD7E191E6C79910056D70F /* LDDataManager.m in Sources */, 69BD7E2C1E6C79910056D70F /* LDUserBuilder.m in Sources */, + 8340A4342164F6D900418027 /* LDEnvironment.m in Sources */, + 83F569D321A3428200FF7A5C /* NSURLSession+LaunchDarkly.m in Sources */, 83B6FC23207E60B6002DBA7B /* NSDate+ReferencedDate.m in Sources */, 83C886832087A786004AC82F /* LDFlagValueCounter.m in Sources */, 83ECCC942087D67D0086F879 /* LDFlagCounter.m in Sources */, @@ -1616,29 +1914,35 @@ files = ( 83889B1C1F8F28AB00A4EF69 /* NSURLResponse+LaunchDarkly.m in Sources */, 8375839F209CCE71004329DD /* LDEventTrackingContext.m in Sources */, + 834541172171298000001C44 /* LDUserEnvironment.m in Sources */, 8365E50D2028F39E00DE8E2B /* LDEvent+EventTypes.m in Sources */, 69F3F69E1E6BF82C00079A09 /* LDEventModel.m in Sources */, 83B62E1C20A249A200F2E656 /* NSDateFormatter+JsonHeader.m in Sources */, 69F3F6981E6BF82C00079A09 /* LDClient.m in Sources */, 832C78881F296F3400E334A2 /* NSMutableDictionary+NullRemovable.m in Sources */, 69F3F6961E6BF82100079A09 /* DarklyConstants.m in Sources */, + 83926B21219F68F300D46140 /* LDURLCache.m in Sources */, 83ECCCAB2088FBA80086F879 /* LDFlagConfigTracker.m in Sources */, 833D08D01F3B97EB00BEED83 /* NSThread+MainExecutable.m in Sources */, 69F3F69C1E6BF82C00079A09 /* LDConfig.m in Sources */, 83F5B47D1F95096A00174DF7 /* NSHTTPURLResponse+LaunchDarkly.m in Sources */, 69F3F6A01E6BF82C00079A09 /* LDFlagConfigModel.m in Sources */, - 69F3F6AC1E6BF82C00079A09 /* NSDictionary+JSON.m in Sources */, + 69F3F6AC1E6BF82C00079A09 /* NSDictionary+LaunchDarkly.m in Sources */, 83B8C2541FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m in Sources */, 8305EC6C20221973002F20DB /* LDFlagConfigValue.m in Sources */, 83EF67861F979B4100403126 /* LDEvent+Unauthorized.m in Sources */, 69F3F6AA1E6BF82C00079A09 /* LDUtil.m in Sources */, 830C2ACA207579AC001D645D /* LDThrottler.m in Sources */, 69F3F6A21E6BF82C00079A09 /* LDPollingManager.m in Sources */, - 69F3F69A1E6BF82C00079A09 /* LDClientManager.m in Sources */, + 69F3F69A1E6BF82C00079A09 /* LDEnvironmentController.m in Sources */, + 83F569DE21A5E1B400FF7A5C /* LDConfig+LaunchDarkly.m in Sources */, 69F3F6A81E6BF82C00079A09 /* LDUserModel.m in Sources */, + 8342C838218B5594000BD8D8 /* NSString+LaunchDarkly.m in Sources */, 83620B8A20BDF36B00F1F28E /* NSNumber+LaunchDarkly.m in Sources */, 69F3F6921E6BF7F600079A09 /* LDDataManager.m in Sources */, 69F3F6A61E6BF82C00079A09 /* LDUserBuilder.m in Sources */, + 8340A4332164F6D900418027 /* LDEnvironment.m in Sources */, + 83F569D221A3428200FF7A5C /* NSURLSession+LaunchDarkly.m in Sources */, 83B6FC22207E60B6002DBA7B /* NSDate+ReferencedDate.m in Sources */, 83C886822087A786004AC82F /* LDFlagValueCounter.m in Sources */, 83ECCC932087D67D0086F879 /* LDFlagCounter.m in Sources */, @@ -1777,7 +2081,7 @@ }; 690346D31E6872EA00E45133 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 276A54AE74C43EE452D1400D /* Pods-Darkly_iOS.debug.xcconfig */; + baseConfigurationReference = 43D1AA6A655A94B08842C52D /* Pods-Darkly_iOS.debug.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; @@ -1801,7 +2105,7 @@ }; 690346D41E6872EA00E45133 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7B3AFF080C28C6F90C108DDA /* Pods-Darkly_iOS.release.xcconfig */; + baseConfigurationReference = E33A186E75DDE1C8C9A0C445 /* Pods-Darkly_iOS.release.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; @@ -1825,7 +2129,7 @@ }; 690346D61E6872EA00E45133 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 1EA7421D138B21221D7B5076 /* Pods-DarklyTests.debug.xcconfig */; + baseConfigurationReference = CBE56EDC74DB21D2C90DCF85 /* Pods-DarklyTests.debug.xcconfig */; buildSettings = { INFOPLIST_FILE = DarklyTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; @@ -1836,7 +2140,7 @@ }; 690346D71E6872EA00E45133 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D5D9EB6AC5D1F6AB6C20AABE /* Pods-DarklyTests.release.xcconfig */; + baseConfigurationReference = D1A8F80D8034DD9416A6504C /* Pods-DarklyTests.release.xcconfig */; buildSettings = { INFOPLIST_FILE = DarklyTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; @@ -1847,7 +2151,7 @@ }; 69A87E941E74458900B88B23 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D16369F49E52EF81AF483671 /* Pods-Darkly_osx.debug.xcconfig */; + baseConfigurationReference = 13A3188F82A743ED134620FA /* Pods-Darkly_osx.debug.xcconfig */; buildSettings = { CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CODE_SIGN_IDENTITY = "-"; @@ -1872,7 +2176,7 @@ }; 69A87E951E74458900B88B23 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D1B3611B3CD24D9CF0D0CD8A /* Pods-Darkly_osx.release.xcconfig */; + baseConfigurationReference = C87C01FFCC54A1DBF76B2D22 /* Pods-Darkly_osx.release.xcconfig */; buildSettings = { CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CODE_SIGN_IDENTITY = "-"; @@ -1897,7 +2201,7 @@ }; 69BD7E161E6C79550056D70F /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = AD004A636172098D40946666 /* Pods-Darkly_watchOS.debug.xcconfig */; + baseConfigurationReference = CA2C32D80AC041361908CED8 /* Pods-Darkly_watchOS.debug.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; @@ -1922,7 +2226,7 @@ }; 69BD7E171E6C79550056D70F /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 4A1B843329DED272F522946F /* Pods-Darkly_watchOS.release.xcconfig */; + baseConfigurationReference = 5F9C63CF10818385D9F5E225 /* Pods-Darkly_watchOS.release.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; @@ -1947,7 +2251,7 @@ }; 69F3F68D1E6BF7C100079A09 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A4FE8A4EBC82F6EE1F30F34A /* Pods-Darkly_tvOS.debug.xcconfig */; + baseConfigurationReference = A4977FD0873245354DEC8287 /* Pods-Darkly_tvOS.debug.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; @@ -1972,7 +2276,7 @@ }; 69F3F68E1E6BF7C100079A09 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 163B00D14362D3130CADF696 /* Pods-Darkly_tvOS.release.xcconfig */; + baseConfigurationReference = 5D56A157FE2544DC8977D3C2 /* Pods-Darkly_tvOS.release.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; diff --git a/Darkly/Darkly.h b/Darkly/Darkly.h index d13b0f0b..9e3793ad 100644 --- a/Darkly/Darkly.h +++ b/Darkly/Darkly.h @@ -8,16 +8,8 @@ #import -#import "LDDataManager.h" #import "DarklyConstants.h" -#import "LDClient.h" -#import "LDClientManager.h" #import "LDConfig.h" -#import "LDEventModel.h" -#import "LDFlagConfigModel.h" -#import "LDPollingManager.h" -#import "LDRequestManager.h" #import "LDUserBuilder.h" -#import "LDUserModel.h" -#import "LDUtil.h" -#import "NSDictionary+JSON.h" +#import "LDClientInterface.h" +#import "LDClient.h" diff --git a/Darkly/DarklyConstants.h b/Darkly/DataModels/DarklyConstants.h similarity index 77% rename from Darkly/DarklyConstants.h rename to Darkly/DataModels/DarklyConstants.h index 68668a60..dfd1ce9a 100644 --- a/Darkly/DarklyConstants.h +++ b/Darkly/DataModels/DarklyConstants.h @@ -18,6 +18,7 @@ typedef enum { } DarklyLogLevel; extern NSString * const kClientVersion; +extern NSString * const kLDPrimaryEnvironmentName; extern NSString * const kBaseUrl; extern NSString * const kEventsUrl; extern NSString * const kStreamUrl; @@ -36,25 +37,36 @@ extern NSString * const kAppleTV; extern NSString * const kMacOS; extern NSString * const kUserDictionaryStorageKey; extern NSString * const kDeviceIdentifierKey; +extern NSString * const kHeaderMobileKey; +extern NSString * const kHTTPMethodReport; + extern NSString *const kLDUserUpdatedNotification; extern NSString *const kLDUserNoChangeNotification; -extern NSString *const kLDFlagConfigChangedNotification; +extern NSString *const kLDFeatureFlagsChangedNotification; extern NSString *const kLDServerConnectionUnavailableNotification; extern NSString *const kLDClientUnauthorizedNotification; +extern NSString *const kLDFlagConfigTimerFiredNotification; +extern NSString *const kLDEventTimerFiredNotification; extern NSString *const kLDBackgroundFetchInitiated; -extern NSString *const kHTTPMethodReport; +extern NSString *const kLDNotificationUserInfoKeyMobileKey; +extern NSString *const kLDNotificationUserInfoKeyFlagKeys; + extern int const kCapacity; extern int const kConnectionTimeout; extern int const kDefaultFlushInterval; -extern int const kMinimumFlushIntervalMillis; +extern int const kMinimumFlushInterval; extern int const kDefaultPollingInterval; extern int const kMinimumPollingInterval; extern int const kDefaultBackgroundFetchInterval; extern int const kMinimumBackgroundFetchInterval; extern int const kMillisInSecs; +extern NSInteger const kHTTPStatusCodeOk; +extern NSInteger const kHTTPStatusCodeNotModified; extern NSInteger const kHTTPStatusCodeBadRequest; extern NSInteger const kHTTPStatusCodeUnauthorized; extern NSInteger const kHTTPStatusCodeMethodNotAllowed; extern NSInteger const kHTTPStatusCodeNotImplemented; extern NSInteger const kErrorCodeUnauthorized; +extern NSUInteger const kNSURLCacheMemoryCapacity; +extern NSUInteger const kNSURLCacheDiskCapacity; extern NSTimeInterval const kMaxThrottlingDelayInterval; diff --git a/Darkly/DarklyConstants.m b/Darkly/DataModels/DarklyConstants.m similarity index 74% rename from Darkly/DarklyConstants.m rename to Darkly/DataModels/DarklyConstants.m index b1d292b7..d0716a85 100644 --- a/Darkly/DarklyConstants.m +++ b/Darkly/DataModels/DarklyConstants.m @@ -4,7 +4,8 @@ #import "DarklyConstants.h" -NSString * const kClientVersion = @"2.13.9"; +NSString * const kClientVersion = @"2.14.0"; +NSString * const kLDPrimaryEnvironmentName = @"LaunchDarkly.EnvironmentName.Primary"; NSString * const kBaseUrl = @"https://app.launchdarkly.com"; NSString * const kEventsUrl = @"https://mobile.launchdarkly.com"; NSString * const kStreamUrl = @"https://clientstream.launchdarkly.com"; @@ -23,17 +24,24 @@ NSString * const kMacOS = @"macOS"; NSString * const kUserDictionaryStorageKey = @"ldUserModelDictionary"; NSString * const kDeviceIdentifierKey = @"ldDeviceIdentifier"; +NSString * const kHeaderMobileKey = @"api_key "; +NSString * const kHTTPMethodReport = @"REPORT"; + NSString * const kLDUserUpdatedNotification = @"Darkly.UserUpdatedNotification"; NSString * const kLDUserNoChangeNotification = @"Darkly.UserNoChangeNotification"; NSString * const kLDBackgroundFetchInitiated = @"Darkly.BackgroundFetchInitiated"; -NSString * const kLDFlagConfigChangedNotification = @"Darkly.FlagConfigChangedNotification"; +NSString * const kLDFeatureFlagsChangedNotification = @"Darkly.FeatureFlagsChangedNotification"; NSString * const kLDServerConnectionUnavailableNotification = @"Darkly.ServerConnectionUnavailableNotification"; NSString * const kLDClientUnauthorizedNotification = @"Darkly.LDClientUnauthorizedNotification"; -NSString * const kHTTPMethodReport = @"REPORT"; +NSString * const kLDFlagConfigTimerFiredNotification = @"Darkly.FlagConfigTimerFiredNotification"; +NSString * const kLDEventTimerFiredNotification = @"Darkly.EventTimerFiredNotification"; +NSString * const kLDNotificationUserInfoKeyMobileKey = @"Darkly.Notification.UserInfo.MobileKey"; +NSString * const kLDNotificationUserInfoKeyFlagKeys = @"Darkly.Notification.UserInfo.FlagKeys"; + int const kCapacity = 100; int const kConnectionTimeout = 10; int const kDefaultFlushInterval = 30; -int const kMinimumFlushIntervalMillis = 0; +int const kMinimumFlushInterval = 0; int const kDefaultPollingInterval = 300; #if DEBUG int const kMinimumPollingInterval = 30; @@ -43,9 +51,14 @@ int const kDefaultBackgroundFetchInterval = 3600; int const kMinimumBackgroundFetchInterval = 900; int const kMillisInSecs = 1000; +NSInteger const kHTTPStatusCodeOk = 200; +NSInteger const kHTTPStatusCodeNotModified = 304; NSInteger const kHTTPStatusCodeBadRequest = 400; NSInteger const kHTTPStatusCodeUnauthorized = 401; NSInteger const kHTTPStatusCodeMethodNotAllowed = 405; NSInteger const kHTTPStatusCodeNotImplemented = 501; NSInteger const kErrorCodeUnauthorized = -kHTTPStatusCodeUnauthorized; +NSUInteger const kNSURLCacheMemoryCapacity = 512000; +NSUInteger const kNSURLCacheDiskCapacity = 0; + NSTimeInterval const kMaxThrottlingDelayInterval = 600.0; diff --git a/Darkly/LDConfig.h b/Darkly/DataModels/LDConfig.h similarity index 93% rename from Darkly/LDConfig.h rename to Darkly/DataModels/LDConfig.h index 9a615bc4..f7de1f45 100644 --- a/Darkly/LDConfig.h +++ b/Darkly/DataModels/LDConfig.h @@ -11,6 +11,15 @@ */ @property (nonatomic, readonly, nonnull) NSString* mobileKey; +/** + These are the names and mobile keys for secondary environments to use in the SDK. The + property must specify a 1:1 mapping of environment name to mobile key. Neither + kLDPrimaryEnvironmentName nor the value in mobileKey may appear in secondaryMobileKeys. + Neither the names nor mobile keys may be empty. If any of these conditions are not met + the SDK will throw an NSInvalidArgumentException. Optional. The default is nil. + */ +@property (nonatomic, strong, nullable) NSDictionary *secondaryMobileKeys; + /** The base URL of the LaunchDarkly service, should you need to override the default. @@ -113,7 +122,7 @@ */ - (instancetype _Nonnull)initWithMobileKey:(nonnull NSString *)mobileKey NS_DESIGNATED_INITIALIZER; - (BOOL)isFlagRetryStatusCode:(NSInteger)statusCode; - +-(NSString*)secondaryMobileKeysDescription; - (instancetype _Nonnull )init NS_UNAVAILABLE; @end diff --git a/Darkly/LDConfig.m b/Darkly/DataModels/LDConfig.m similarity index 57% rename from Darkly/LDConfig.m rename to Darkly/DataModels/LDConfig.m index 574a1313..6cf41e9a 100644 --- a/Darkly/LDConfig.m +++ b/Darkly/DataModels/LDConfig.m @@ -4,6 +4,9 @@ #import "LDConfig.h" #import "LDUtil.h" +#import "LDClient.h" +#import "NSString+LaunchDarkly.h" +#import "NSDictionary+LaunchDarkly.h" @interface LDConfig() @property (nonatomic, copy, nonnull) NSString* mobileKey; @@ -42,6 +45,35 @@ - (void)setMobileKey:(NSString *)mobileKey { DEBUG_LOG(@"Set LDConfig mobileKey: %@", mobileKey); } +- (void)setSecondaryMobileKeys:(NSDictionary *)secondaryMobileKeys { + if ([secondaryMobileKeys.allKeys containsObject:kLDPrimaryEnvironmentName]) { + NSException *invalidConfigException = + [NSException exceptionWithName:NSInvalidArgumentException reason:@"Illegal LDConfig secondaryMobileKeys: May not contain the primary environment name." userInfo:nil]; + @throw invalidConfigException; + } + if ([secondaryMobileKeys.allValues containsObject:self.mobileKey]) { + NSException *invalidConfigException = + [NSException exceptionWithName:NSInvalidArgumentException reason:@"Illegal LDConfig secondaryMobileKeys: May not contain the primary mobile key." userInfo:nil]; + @throw invalidConfigException; + } + if ([NSSet setWithArray:secondaryMobileKeys.allValues].count != secondaryMobileKeys.allValues.count) { + NSException *invalidConfigException = + [NSException exceptionWithName:NSInvalidArgumentException reason:@"Illegal LDConfig secondaryMobileKeys: mobile keys must all be unique" userInfo:nil]; + @throw invalidConfigException; + } + if ([secondaryMobileKeys.allKeys containsObject:@""]) { + NSException *invalidConfigException = + [NSException exceptionWithName:NSInvalidArgumentException reason:@"Illegal LDConfig secondaryMobileKeys: May not contain an empty environment name." userInfo:nil]; + @throw invalidConfigException; + } + if ([secondaryMobileKeys.allValues containsObject:@""]) { + NSException *invalidConfigException = + [NSException exceptionWithName:NSInvalidArgumentException reason:@"Illegal LDConfig secondaryMobileKeys: May not contain an empty mobile key." userInfo:nil]; + @throw invalidConfigException; + } + _secondaryMobileKeys = secondaryMobileKeys; +} + - (void)setBaseUrl:(NSString *)baseUrl { if (baseUrl) { DEBUG_LOG(@"Set LDConfig baseUrl: %@", baseUrl); @@ -136,6 +168,41 @@ - (BOOL)isFlagRetryStatusCode:(NSInteger)statusCode { return [self.flagRetryStatusCodes containsObject:@(statusCode)]; } +-(NSString*)description { + NSString *description = [NSString stringWithFormat:@"", description]; + return description; +} + +-(NSString*)secondaryMobileKeysDescription { + if (self.secondaryMobileKeys.count == 0) { + return @"{}"; + } + NSString *secondaryKeysDescription = @"{"; + NSString *separator = @""; + for (NSString *environmentName in self.secondaryMobileKeys.allKeys) { + secondaryKeysDescription = [NSString stringWithFormat:@"%@%@%@:%@", secondaryKeysDescription, separator, environmentName, self.secondaryMobileKeys[environmentName]]; + separator = @","; + } + secondaryKeysDescription = [NSString stringWithFormat:@"%@}", secondaryKeysDescription]; + return secondaryKeysDescription; +} @end #pragma clang diagnostic push diff --git a/Darkly/LDEventModel.h b/Darkly/DataModels/LDEventModel.h similarity index 100% rename from Darkly/LDEventModel.h rename to Darkly/DataModels/LDEventModel.h diff --git a/Darkly/LDEventModel.m b/Darkly/DataModels/LDEventModel.m similarity index 100% rename from Darkly/LDEventModel.m rename to Darkly/DataModels/LDEventModel.m diff --git a/Darkly/LDFlagConfig/LDEventTrackingContext.h b/Darkly/DataModels/LDFlagConfig/LDEventTrackingContext.h similarity index 100% rename from Darkly/LDFlagConfig/LDEventTrackingContext.h rename to Darkly/DataModels/LDFlagConfig/LDEventTrackingContext.h diff --git a/Darkly/LDFlagConfig/LDEventTrackingContext.m b/Darkly/DataModels/LDFlagConfig/LDEventTrackingContext.m similarity index 100% rename from Darkly/LDFlagConfig/LDEventTrackingContext.m rename to Darkly/DataModels/LDFlagConfig/LDEventTrackingContext.m diff --git a/Darkly/LDFlagConfig/LDFlagConfigModel.h b/Darkly/DataModels/LDFlagConfig/LDFlagConfigModel.h similarity index 81% rename from Darkly/LDFlagConfig/LDFlagConfigModel.h rename to Darkly/DataModels/LDFlagConfig/LDFlagConfigModel.h index 3ccfbd4c..e9b70811 100644 --- a/Darkly/LDFlagConfig/LDFlagConfigModel.h +++ b/Darkly/DataModels/LDFlagConfig/LDFlagConfigModel.h @@ -13,12 +13,13 @@ @interface LDFlagConfigModel : NSObject @property (nullable, nonatomic, strong) NSDictionary *featuresJsonDictionary; +@property (nullable, nonatomic, strong, readonly) NSDictionary *allFlagValues; -(nullable id)initWithDictionary:(nullable NSDictionary*)dictionary; -(nullable NSDictionary*)dictionaryValue; -(nullable NSDictionary*)dictionaryValueIncludeNulls:(BOOL)includeNulls; --(BOOL)doesFlagConfigValueExistForFlagKey:(nonnull NSString*)flagKey; +-(BOOL)containsFlagKey:(nonnull NSString*)flagKey; -(nullable LDFlagConfigValue*)flagConfigValueForFlagKey:(nonnull NSString*)flagKey; -(nullable id)flagValueForFlagKey:(nonnull NSString*)flagKey; -(NSInteger)flagModelVersionForFlagKey:(nonnull NSString*)flagKey; @@ -27,9 +28,11 @@ -(void)deleteFromDictionary:(nullable NSDictionary*)eventDictionary; -(BOOL)isEqualToConfig:(nullable LDFlagConfigModel*)otherConfig; +-(NSArray*)differingFlagKeysFromConfig:(nullable LDFlagConfigModel*)otherConfig; -(BOOL)hasFeaturesEqualToDictionary:(nullable NSDictionary*)otherDictionary; -(void)updateEventTrackingContextFromConfig:(nullable LDFlagConfigModel*)otherConfig; +-(LDFlagConfigModel*)copy; -(nonnull NSString*)description; @end diff --git a/Darkly/LDFlagConfig/LDFlagConfigModel.m b/Darkly/DataModels/LDFlagConfig/LDFlagConfigModel.m similarity index 77% rename from Darkly/LDFlagConfig/LDFlagConfigModel.m rename to Darkly/DataModels/LDFlagConfig/LDFlagConfigModel.m index ad3acfb8..7366e195 100644 --- a/Darkly/LDFlagConfig/LDFlagConfigModel.m +++ b/Darkly/DataModels/LDFlagConfig/LDFlagConfigModel.m @@ -10,6 +10,7 @@ #import "LDFlagConfigValue.h" #import "LDUtil.h" #import "NSMutableDictionary+NullRemovable.h" +#import "NSDictionary+LaunchDarkly.h" NSString * const kFeaturesJsonDictionaryKey = @"featuresJsonDictionary"; NSString * const kLDFlagConfigModelKeyKey = @"key"; @@ -78,7 +79,17 @@ -(NSDictionary*)dictionaryValueIncludeNulls:(BOOL)includeNulls { return [NSDictionary dictionaryWithDictionary:[flagConfigDictionaryValues copy]]; } --(BOOL)doesFlagConfigValueExistForFlagKey:(NSString*)flagKey { +-(NSDictionary*)allFlagValues { + return [self.featuresJsonDictionary compactMapUsingBlock:^id(id originalValue) { + if (originalValue == nil) { return nil; } + if (![originalValue isKindOfClass:[LDFlagConfigValue class]]) { return nil; } + LDFlagConfigValue *flagConfigValue = originalValue; + if (flagConfigValue.value == nil || [flagConfigValue.value isKindOfClass:[NSNull class]]) { return nil; } + return flagConfigValue.value; + }]; +} + +-(BOOL)containsFlagKey:(NSString*)flagKey { if (!self.featuresJsonDictionary) { return NO; } return [[self.featuresJsonDictionary allKeys] containsObject: flagKey]; @@ -131,10 +142,10 @@ -(void)deleteFromDictionary:(NSDictionary*)eventDictionary { id flagVersionObject = eventDictionary[kLDFlagConfigValueKeyVersion]; if (!flagVersionObject || ![flagVersionObject isKindOfClass:[NSNumber class]]) { return; } NSInteger flagVersion = [(NSNumber*)flagVersionObject integerValue]; - if ([self doesFlagConfigValueExistForFlagKey:flagKey] && flagVersion <= [self flagModelVersionForFlagKey:flagKey]) { return; } + if ([self containsFlagKey:flagKey] && flagVersion <= [self flagModelVersionForFlagKey:flagKey]) { return; } NSMutableDictionary *updatedFlagConfig = [NSMutableDictionary dictionaryWithDictionary:self.featuresJsonDictionary]; - updatedFlagConfig[flagKey] = nil; + [updatedFlagConfig removeObjectForKey:flagKey]; self.featuresJsonDictionary = [updatedFlagConfig copy]; } @@ -143,12 +154,41 @@ -(BOOL)isEqualToConfig:(LDFlagConfigModel *)otherConfig { return [self.featuresJsonDictionary isEqualToDictionary:otherConfig.featuresJsonDictionary]; } +-(NSArray*)differingFlagKeysFromConfig:(nullable LDFlagConfigModel*)otherConfig { + NSSet *allKeys = [[NSSet setWithArray:self.featuresJsonDictionary.allKeys] setByAddingObjectsFromArray:otherConfig.featuresJsonDictionary.allKeys]; + NSMutableArray *differingFlagKeys = [NSMutableArray arrayWithCapacity:allKeys.count]; + for (NSString *flagKey in allKeys) { + if (![self containsFlagKey:flagKey] || ![otherConfig containsFlagKey:flagKey]) { + [differingFlagKeys addObject:flagKey]; + continue; + } + id value = [self flagValueForFlagKey:flagKey]; + id otherValue = [otherConfig flagValueForFlagKey:flagKey]; + if ([value isEqual:otherValue]) { + continue; + } + if (value == nil && otherValue == nil) { + continue; + } + [differingFlagKeys addObject:flagKey]; + } + if (differingFlagKeys.count == 0) { + return nil; + } + + return [differingFlagKeys copy]; +} + -(BOOL)hasFeaturesEqualToDictionary:(NSDictionary*)otherDictionary { NSArray *flagKeys = self.featuresJsonDictionary.allKeys; - if (flagKeys.count != otherDictionary.allKeys.count) { return NO; } + if (flagKeys.count != otherDictionary.allKeys.count) { + return NO; + } for (NSString *flagKey in flagKeys) { LDFlagConfigValue *flagConfigValue = self.featuresJsonDictionary[flagKey]; - if (!otherDictionary[flagKey] || ![otherDictionary[flagKey] isKindOfClass:[NSDictionary class]]) { return NO; } + if (!otherDictionary[flagKey] || ![otherDictionary[flagKey] isKindOfClass:[NSDictionary class]]) { + return NO; + } NSDictionary *otherFlagConfigValueDictionary = otherDictionary[flagKey]; if (![flagConfigValue hasPropertiesMatchingDictionary:otherFlagConfigValueDictionary]) { @@ -172,6 +212,10 @@ -(void)updateEventTrackingContextFromConfig:(LDFlagConfigModel*)otherConfig { } } +-(LDFlagConfigModel*)copy { + return [[LDFlagConfigModel alloc] initWithDictionary:[self dictionaryValueIncludeNulls:YES]]; +} + -(NSString*)description { return [NSString stringWithFormat:@"", self, [self.featuresJsonDictionary description]]; } diff --git a/Darkly/LDFlagConfig/LDFlagConfigTracker.h b/Darkly/DataModels/LDFlagConfig/LDFlagConfigTracker.h similarity index 100% rename from Darkly/LDFlagConfig/LDFlagConfigTracker.h rename to Darkly/DataModels/LDFlagConfig/LDFlagConfigTracker.h diff --git a/Darkly/LDFlagConfig/LDFlagConfigTracker.m b/Darkly/DataModels/LDFlagConfig/LDFlagConfigTracker.m similarity index 100% rename from Darkly/LDFlagConfig/LDFlagConfigTracker.m rename to Darkly/DataModels/LDFlagConfig/LDFlagConfigTracker.m diff --git a/Darkly/LDFlagConfig/LDFlagConfigValue.h b/Darkly/DataModels/LDFlagConfig/LDFlagConfigValue.h similarity index 100% rename from Darkly/LDFlagConfig/LDFlagConfigValue.h rename to Darkly/DataModels/LDFlagConfig/LDFlagConfigValue.h diff --git a/Darkly/LDFlagConfig/LDFlagConfigValue.m b/Darkly/DataModels/LDFlagConfig/LDFlagConfigValue.m similarity index 100% rename from Darkly/LDFlagConfig/LDFlagConfigValue.m rename to Darkly/DataModels/LDFlagConfig/LDFlagConfigValue.m diff --git a/Darkly/LDFlagConfig/LDFlagCounter.h b/Darkly/DataModels/LDFlagConfig/LDFlagCounter.h similarity index 100% rename from Darkly/LDFlagConfig/LDFlagCounter.h rename to Darkly/DataModels/LDFlagConfig/LDFlagCounter.h diff --git a/Darkly/LDFlagConfig/LDFlagCounter.m b/Darkly/DataModels/LDFlagConfig/LDFlagCounter.m similarity index 100% rename from Darkly/LDFlagConfig/LDFlagCounter.m rename to Darkly/DataModels/LDFlagConfig/LDFlagCounter.m diff --git a/Darkly/LDFlagConfig/LDFlagValueCounter.h b/Darkly/DataModels/LDFlagConfig/LDFlagValueCounter.h similarity index 100% rename from Darkly/LDFlagConfig/LDFlagValueCounter.h rename to Darkly/DataModels/LDFlagConfig/LDFlagValueCounter.h diff --git a/Darkly/LDFlagConfig/LDFlagValueCounter.m b/Darkly/DataModels/LDFlagConfig/LDFlagValueCounter.m similarity index 100% rename from Darkly/LDFlagConfig/LDFlagValueCounter.m rename to Darkly/DataModels/LDFlagConfig/LDFlagValueCounter.m diff --git a/Darkly/LDUserBuilder.h b/Darkly/DataModels/LDUserBuilder.h similarity index 98% rename from Darkly/LDUserBuilder.h rename to Darkly/DataModels/LDUserBuilder.h index ff0a4a62..4c8ec1a9 100644 --- a/Darkly/LDUserBuilder.h +++ b/Darkly/DataModels/LDUserBuilder.h @@ -2,8 +2,8 @@ // Copyright © 2015 Catamorphic Co. All rights reserved. // - -#import "LDUserModel.h" +@import Foundation; +@class LDUserModel; @interface LDUserBuilder : NSObject @@ -109,9 +109,7 @@ -(nonnull LDUserModel *)build; -+ (nonnull LDUserModel *)compareNewBuilder:(nonnull LDUserBuilder *)iBuilder withUser:(nonnull LDUserModel *)iUser; + (nonnull LDUserBuilder *)currentBuilder:(nonnull LDUserModel *)iUser; - + (nonnull LDUserBuilder *)retrieveCurrentBuilder:(nonnull LDUserModel *)iUser __deprecated_msg("Use `currentBuilder:` instead"); /** diff --git a/Darkly/LDUserBuilder.m b/Darkly/DataModels/LDUserBuilder.m similarity index 69% rename from Darkly/LDUserBuilder.m rename to Darkly/DataModels/LDUserBuilder.m index 9511beb9..274d561f 100644 --- a/Darkly/LDUserBuilder.m +++ b/Darkly/DataModels/LDUserBuilder.m @@ -4,8 +4,10 @@ #import "LDUserBuilder.h" +#import "LDUserModel.h" #import "LDUtil.h" #import "LDDataManager.h" +#import "DarklyConstants.h" @implementation LDUserBuilder @@ -141,79 +143,40 @@ - (void)customArray:(NSString *)inputKey value:(NSArray *)value } } -- (LDUserModel *)build { - DEBUG_LOGX(@"LDUserBuilder build method called"); - LDUserModel *user = nil; +-(LDUserModel*)build { + LDUserModel *user = [[LDUserModel alloc] init]; + user.key = self.key.length > 0 ? self.key : [LDUserBuilder uniqueKey]; + user.anonymous = self.key.length > 0 ? self.isAnonymous : YES; + user.ip = self.ip; + user.country = self.country; + user.name = self.name; + user.firstName = self.firstName; + user.lastName = self.lastName; + user.email = self.email; + user.avatar = self.avatar; + user.custom = self.customDictionary; + user.privateAttributes = self.privateAttributes; - if (self.key) { - user = [[LDDataManager sharedManager] findUserWithkey:self.key]; - if(!user) { - user = [[LDUserModel alloc] init]; - } - DEBUG_LOG(@"LDUserBuilder building User with key: %@", self.key); - DEBUG_LOG(@"LDUserBuilder building User with anonymous: %d", self.isAnonymous); - [user key:self.key]; - user.anonymous = self.isAnonymous; - } else { - NSString *uniqueKey; + return user; +} + ++(NSString*)uniqueKey { + NSString *uniqueKey; #if TARGET_OS_IOS || TARGET_OS_TV - uniqueKey = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; + uniqueKey = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; #else - if ([[NSUserDefaults standardUserDefaults] valueForKey:kDeviceIdentifierKey]) { - uniqueKey = [[NSUserDefaults standardUserDefaults] valueForKey:kDeviceIdentifierKey]; - } - else{ - uniqueKey = [[NSUUID UUID] UUIDString]; - [[NSUserDefaults standardUserDefaults] setValue:uniqueKey forKey:kDeviceIdentifierKey]; - [[NSUserDefaults standardUserDefaults] synchronize]; - } - -#endif - DEBUG_LOG(@"LDUserBuilder building User with key: %@", uniqueKey); - - user = [[LDUserModel alloc] init]; - [user key:uniqueKey]; - user.anonymous = YES; - } - if (self.ip) { - DEBUG_LOG(@"LDUserBuilder building User with ip: %@", self.ip); - user.ip = self.ip; + if ([[NSUserDefaults standardUserDefaults] valueForKey:kDeviceIdentifierKey]) { + uniqueKey = [[NSUserDefaults standardUserDefaults] valueForKey:kDeviceIdentifierKey]; } - if (self.country) { - DEBUG_LOG(@"LDUserBuilder building User with country: %@", self.country); - user.country = self.country; - } - if (self.name) { - DEBUG_LOG(@"LDUserBuilder building User with name: %@", self.name); - user.name = self.name; - } - if (self.firstName) { - DEBUG_LOG(@"LDUserBuilder building User with firstName: %@", self.firstName); - user.firstName = self.firstName; - } - if (self.lastName) { - DEBUG_LOG(@"LDUserBuilder building User with lastName: %@", self.lastName); - user.lastName = self.lastName; - } - if (self.email) { - DEBUG_LOG(@"LDUserBuilder building User with email: %@", self.email); - user.email = self.email; - } - if (self.avatar) { - DEBUG_LOG(@"LDUserBuilder building User with avatar: %@", self.avatar); - user.avatar = self.avatar; - } - if (self.customDictionary && self.customDictionary.count) { - DEBUG_LOG(@"LDUserBuilder building User with custom: %@", self.customDictionary); - user.custom = self.customDictionary; - } - if (self.privateAttributes) { - DEBUG_LOG(@"LDUserBuilder building User with private attributes: %@", [self.privateAttributes description]); - user.privateAttributes = self.privateAttributes; + else{ + uniqueKey = [[NSUUID UUID] UUIDString]; + [[NSUserDefaults standardUserDefaults] setValue:uniqueKey forKey:kDeviceIdentifierKey]; + [[NSUserDefaults standardUserDefaults] synchronize]; } - [[LDDataManager sharedManager] saveUser:user]; - return user; +#endif + + return uniqueKey; } - (LDUserBuilder *)withKey:(NSString *)inputKey diff --git a/Darkly/DataModels/LDUserEnvironment.h b/Darkly/DataModels/LDUserEnvironment.h new file mode 100644 index 00000000..49749efe --- /dev/null +++ b/Darkly/DataModels/LDUserEnvironment.h @@ -0,0 +1,32 @@ +// +// LDUserEnvironment.h +// Darkly +// +// Created by Mark Pokorny on 10/12/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import + +@class LDUserModel; + +//Wrapper data object used for caching environment based users. Conceptually, an object containing a collection of feature flags for a single user keyed on mobile-key +@interface LDUserEnvironment: NSObject +@property (nonatomic, strong, readonly, nonnull) NSString *userKey; +@property (nonatomic, strong, readonly, nullable) NSDate *lastUpdated; + +//Each LDUserModel passed in through environments must match the userKey in order to be included. Any that do not match will not be included. ++(nullable instancetype)userEnvironmentForUserWithKey:(nonnull NSString*)userKey environments:(nullable NSDictionary*)environments; +-(nullable instancetype)initForUserWithKey:(nonnull NSString*)userKey environments:(nonnull NSDictionary*)environments; + +-(nullable instancetype)initWithCoder:(NSCoder*)coder; +-(void)encodeWithCoder:(NSCoder*)coder; + +-(nullable instancetype)initWithDictionary:(nullable NSDictionary*)dictionary; +-(nullable NSDictionary*)dictionaryValue; + +-(nullable LDUserModel*)userForMobileKey:(nonnull NSString*)mobileKey; +-(void)setUser:(nonnull LDUserModel*)user mobileKey:(nonnull NSString*)mobileKey; +-(void)removeUserForMobileKey:(nonnull NSString*)mobileKey; + +@end diff --git a/Darkly/DataModels/LDUserEnvironment.m b/Darkly/DataModels/LDUserEnvironment.m new file mode 100644 index 00000000..c61e8c7b --- /dev/null +++ b/Darkly/DataModels/LDUserEnvironment.m @@ -0,0 +1,147 @@ +// +// LDUserEnvironment.m +// Darkly +// +// Created by Mark Pokorny on 10/12/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDUserEnvironment.h" +#import "LDUserModel.h" +#import "NSDictionary+LaunchDarkly.h" + +NSString *const kUserEnvironmentKeyUserKey = @"userKey"; +NSString *const kUserEnvironmentKeyEnvironments = @"environments"; + +@interface LDUserEnvironment () +@property (nonatomic, strong) NSString *userKey; +@property (nonatomic, strong) NSDictionary *users; // +@end + +@implementation LDUserEnvironment + +//Each LDUserModel passed in through environments must match the userKey in order to be included. Any that do not match will be excluded. ++(instancetype)userEnvironmentForUserWithKey:(NSString*)userKey environments:(NSDictionary*)environments { + return [[LDUserEnvironment alloc] initForUserWithKey:userKey environments:environments]; +} + +-(instancetype)initForUserWithKey:(NSString*)userKey environments:(NSDictionary*)environments { + if (!(self = [super init])) { return nil; } + if (userKey.length == 0) { + return nil; + } + //Filter out users with keys that don't match userKey + NSDictionary *matchingEnvironments = [environments compactMapUsingBlock:^id(id _Nonnull originalValue) { + if (![originalValue isKindOfClass:[LDUserModel class]]) { + return nil; + } + LDUserModel *user = originalValue; + if (![user.key isEqualToString:userKey]) { + return nil; + } + return user; + }]; + + self.userKey = userKey; + self.users = matchingEnvironments ?: [NSDictionary dictionary]; + + return self; +} + +-(instancetype)initWithCoder:(NSCoder*)coder { + self = [self init]; + + self.userKey = [coder decodeObjectForKey:kUserEnvironmentKeyUserKey]; + if (self.userKey.length == 0) { + return nil; + } + self.users = [coder decodeObjectForKey:kUserEnvironmentKeyEnvironments] ?: [NSDictionary dictionary]; + + return self; +} + +-(void)encodeWithCoder:(NSCoder*)coder { + [coder encodeObject:self.userKey forKey:kUserEnvironmentKeyUserKey]; + [coder encodeObject:self.users forKey:kUserEnvironmentKeyEnvironments]; +} + +-(instancetype)initWithDictionary:(NSDictionary*)dictionary { + NSString *userKey = dictionary[kUserEnvironmentKeyUserKey]; + if (userKey.length == 0) { + return nil; + } + self = [self init]; + self.userKey = userKey; + self.users = [dictionary[kUserEnvironmentKeyEnvironments] compactMapUsingBlock:^id(id _Nonnull originalValue) { + if (![originalValue isKindOfClass:[NSDictionary class]]) { + return nil; + } + NSDictionary *userDictionary = originalValue; + return [[LDUserModel alloc] initWithDictionary:userDictionary]; + }]; + + return self; +} + +-(NSDictionary*)dictionaryValue { + if (self.userKey.length == 0) { + return nil; + } + NSDictionary *usersDictionary = [self.users compactMapUsingBlock:^id(id _Nonnull originalValue) { + if (![originalValue isKindOfClass:[LDUserModel class]]) { + return nil; + } + LDUserModel *originalUser = originalValue; + return [originalUser dictionaryValueWithPrivateAttributesAndFlagConfig:YES]; + }]; + return @{kUserEnvironmentKeyUserKey:self.userKey, kUserEnvironmentKeyEnvironments:usersDictionary ?: [NSDictionary dictionary]}; +} + +-(NSDate*)lastUpdated { + if (self.users.count == 0) { + return nil; + } + NSArray *usersUpdatedAt = [self.users compactMapUsingBlock:^id(id _Nonnull originalValue) { + if (![originalValue isKindOfClass:[LDUserModel class]]) { + return nil; + } + LDUserModel *originalUser = originalValue; + return originalUser.updatedAt; + }].allValues; + if (usersUpdatedAt.count == 0) { + return nil; + } + NSArray *sortedUsersUpdatedAt = [usersUpdatedAt sortedArrayUsingComparator:^NSComparisonResult(NSDate * _Nonnull date1, NSDate * _Nonnull date2) { + return [date1 compare:date2]; + }]; + return sortedUsersUpdatedAt.lastObject; +} + +-(LDUserModel*)userForMobileKey:(NSString*)mobileKey { + return self.users[mobileKey]; +} + +-(void)setUser:(LDUserModel*)user mobileKey:(NSString*)mobileKey { + if (mobileKey.length == 0 || user == nil || ![user.key isEqualToString:self.userKey]) { + return; + } + NSMutableDictionary *updatedUsers = [NSMutableDictionary dictionaryWithDictionary:self.users]; + updatedUsers[mobileKey] = user; + self.users = [updatedUsers copy]; +} + +-(void)removeUserForMobileKey:(NSString*)mobileKey { + if (mobileKey.length == 0) { + return; + } + NSMutableDictionary *updatedUsers = [NSMutableDictionary dictionaryWithDictionary:self.users]; + [updatedUsers removeObjectForKey:mobileKey]; + self.users = [updatedUsers copy]; +} + +-(NSString*)description { + NSString *mobileKeys = [self.users.allKeys componentsJoinedByString:@","]; + return [NSString stringWithFormat:@"<%@ %p: userKey:%@, mobileKeys:%@>", NSStringFromClass([self class]), self, self.userKey, mobileKeys]; +} + +@end diff --git a/Darkly/LDUserModel.h b/Darkly/DataModels/LDUserModel.h similarity index 98% rename from Darkly/LDUserModel.h rename to Darkly/DataModels/LDUserModel.h index 80007aba..f3e7b7dd 100644 --- a/Darkly/LDUserModel.h +++ b/Darkly/DataModels/LDUserModel.h @@ -48,4 +48,6 @@ extern NSString * __nonnull const kUserAttributeCustom; -(void)resetTracker; +-(LDUserModel*)copy; + @end diff --git a/Darkly/LDUserModel.m b/Darkly/DataModels/LDUserModel.m similarity index 95% rename from Darkly/LDUserModel.m rename to Darkly/DataModels/LDUserModel.m index b5b33153..b17a7ae1 100644 --- a/Darkly/LDUserModel.m +++ b/Darkly/DataModels/LDUserModel.m @@ -210,4 +210,13 @@ -(NSString*) description { return @[kUserAttributeIp, kUserAttributeCountry, kUserAttributeName, kUserAttributeFirstName, kUserAttributeLastName, kUserAttributeEmail, kUserAttributeAvatar, kUserAttributeCustom]; } +-(LDUserModel*)copy { + LDUserModel *copiedUser = [[LDUserModel alloc] initWithDictionary:[self dictionaryValueWithPrivateAttributesAndFlagConfig:NO]]; //omit the flag config because it excludes null items + if (self.privateAttributes != nil) { + copiedUser.privateAttributes = [NSArray arrayWithArray:self.privateAttributes]; //Private attributes are not placed into the dictionaryValue + } + copiedUser.flagConfig = [self.flagConfig copy]; + return copiedUser; +} + @end diff --git a/Darkly/LDClient.h b/Darkly/LDClient.h index fe792d89..eef7054d 100644 --- a/Darkly/LDClient.h +++ b/Darkly/LDClient.h @@ -4,11 +4,9 @@ #import "LDConfig.h" #import "LDUserBuilder.h" -#import "LDUtil.h" - -@class LDConfig; -@class LDUserBuilder; +#import "LDClientInterface.h" +@class LDUserModel; @protocol ClientDelegate @optional @@ -18,15 +16,19 @@ -(void)serverConnectionUnavailable; @end -@interface LDClient : NSObject +@interface LDClient : NSObject @property (nonatomic, assign, readonly) BOOL isOnline; -@property(nonatomic, strong, readonly) LDUserModel *ldUser; -@property(nonatomic, strong, readonly) LDConfig *ldConfig; +@property (nonatomic, strong, readonly) LDUserModel *ldUser; +@property (nonatomic, strong, readonly) LDConfig *ldConfig; +@property (nonatomic, copy, readonly) NSString *environmentName; @property (nonatomic, weak) id delegate; +@property (nonatomic, strong, readonly) NSDictionary *allFlags; + (LDClient *)sharedInstance; +#pragma mark - SDK Control + /** * Start the client with a valid configuration and user. * @@ -35,6 +37,7 @@ * @return whether the client was able to be started. */ - (BOOL)start:(LDConfigBuilder *)inputConfigBuilder userBuilder:(LDUserBuilder *)inputUserBuilder __deprecated_msg("Use start:withUserBuilder: instead"); + /** * Start the client with a valid configuration and user. * @@ -43,6 +46,40 @@ * @return whether the client was able to be started. */ - (BOOL)start:(LDConfig *)inputConfig withUserBuilder:(LDUserBuilder *)inputUserBuilder; + +/** + * Set the client to online/offline mode. When online events will be synced to server. (Default) + * + * @param goOnline Desired online/offline mode for the client + */ +- (void)setOnline:(BOOL)goOnline; + +/** + * Set the client to online/offline mode. When online events will be synced to server. (Default) + * + * @param goOnline Desired online/offline mode for the client + * @param completion Completion block called when setOnline completes + */ +- (void)setOnline:(BOOL)goOnline completion:(void(^)(void))completion; + +/** + * Sync all events to the server. Events are synced to the server on a + * regular basis, however this will force all stored events from the client + * to be synced immediately to the server. + * + * @return whether events were able to be flushed. + */ +- (BOOL)flush; + +/** + * Stop the client. + * + * @return whether the client was able to be stopped. + */ +- (BOOL)stopClient; + +#pragma mark - Variation + /** * Retrieve a feature flag value. If the configuration for this feature * flag is retrieved from the server that value is returned, otherwise @@ -53,6 +90,7 @@ * @return the feature flag value */ - (BOOL)boolVariation:(NSString *)featureKey fallback:(BOOL)fallback; + /** * Retrieve a feature flag value. If the configuration for this feature * flag is retrieved from the server that value is returned, otherwise @@ -63,6 +101,7 @@ * @return the feature flag value */ - (NSNumber*)numberVariation:(NSString *)featureKey fallback:(NSNumber*)fallback; + /** * Retrieve a feature flag value. If the configuration for this feature * flag is retrieved from the server that value is returned, otherwise @@ -73,6 +112,7 @@ * @return the feature flag value */ - (double)doubleVariation:(NSString *)featureKey fallback:(double)fallback; + /** * Retrieve a feature flag value. If the configuration for this feature * flag is retrieved from the server that value is returned, otherwise @@ -83,6 +123,7 @@ * @return the feature flag value */ - (NSString*)stringVariation:(NSString *)featureKey fallback:(NSString*)fallback; + /** * Retrieve a feature flag value. If the configuration for this feature * flag is retrieved from the server that value is returned, otherwise @@ -93,6 +134,7 @@ * @return the feature flag value */ - (NSArray*)arrayVariation:(NSString *)featureKey fallback:(NSArray*)fallback; + /** * Retrieve a feature flag value. If the configuration for this feature * flag is retrieved from the server that value is returned, otherwise @@ -103,6 +145,9 @@ * @return the feature flag value */ - (NSDictionary*)dictionaryVariation:(NSString *)featureKey fallback:(NSDictionary*)fallback; + +#pragma mark - Event + /** * Track a custom event. * @@ -111,6 +156,9 @@ * @return whether the event was successfully recorded */ - (BOOL)track:(NSString *)eventName data:(NSDictionary *)dataDictionary; + +#pragma mark - User + /** * Update the user after the client has started. This will override * user information passed in via the start method. @@ -119,38 +167,27 @@ * @return whether the user was successfully updated */ - (BOOL)updateUser:(LDUserBuilder *)builder; + /** * Retrieve the current user. * * @return the current user. */ - (LDUserBuilder *)currentUserBuilder; + +#pragma mark - Multiple Environments + /** - * Set the client to online/offline mode. When online events will be synced to server. (Default) - * - * @param goOnline Desired online/offline mode for the client - */ -- (void)setOnline:(BOOL)goOnline; -/** - * Set the client to online/offline mode. When online events will be synced to server. (Default) - * - * @param goOnline Desired online/offline mode for the client - * @param completion Completion block called when setOnline completes - */ -- (void)setOnline:(BOOL)goOnline completion:(void(^)(void))completion; -/** - * Sync all events to the server. Events are synced to the server on a - * regular basis, however this will force all stored events from the client - * to be synced immediately to the server. + * Class method that returns the LDClientInterface object for the environment referenced by the + * name parameter. The SDK must be started, otherwise the method returns nil even if the name + * appears in secondaryMobileKeys. If the name doesn't match any of the names (map keys) in + * secondaryMobileKeys, or the kLDPrimaryEnvironmentName, the method throws an + * NSIllegalArgumentException. * - * @return whether events were able to be flushed. - */ -- (BOOL)flush; -/** - * Stop the client. + * @param name name associated with a mobile key in secondaryMobileKeys * - * @return whether the client was able to be stopped. + * @return the LDClientInterface object associated with name in secondaryMobileKeys */ -- (BOOL)stopClient; ++(id)environmentForMobileKeyNamed:(NSString*)name; @end diff --git a/Darkly/LDClient.m b/Darkly/LDClient.m index 8aa96a28..653ccff1 100644 --- a/Darkly/LDClient.m +++ b/Darkly/LDClient.m @@ -4,9 +4,9 @@ #import "LDClient.h" -#import "LDClientManager.h" +#import "LDUserModel.h" +#import "LDEnvironment.h" #import "LDUtil.h" -#import "LDDataManager.h" #import "LDPollingManager.h" #import "DarklyConstants.h" #import "NSThread+MainExecutable.h" @@ -14,14 +14,17 @@ #import "LDFlagConfigModel.h" #import "LDFlagConfigValue.h" #import "LDFlagConfigTracker.h" +#import "NSURLSession+LaunchDarkly.h" @interface LDClient() @property (nonatomic, assign) BOOL isOnline; -@property(nonatomic, strong) LDUserModel *ldUser; -@property(nonatomic, strong) LDConfig *ldConfig; +@property (nonatomic, strong) LDUserModel *ldUser; +@property (nonatomic, strong) LDConfig *ldConfig; @property (nonatomic, assign) BOOL clientStarted; @property (nonatomic, strong) LDThrottler *throttler; @property (nonatomic, assign) BOOL willGoOnlineAfterDelay; +@property (nonatomic, strong) LDEnvironment *primaryEnvironment; +@property (nonatomic, strong) NSMutableDictionary *secondaryEnvironments; // @end @implementation LDClient @@ -33,30 +36,25 @@ +(LDClient *)sharedInstance dispatch_once(&onceToken, ^{ sharedLDClient = [[self alloc] init]; sharedLDClient.throttler = [[LDThrottler alloc] initWithMaxDelayInterval:kMaxThrottlingDelayInterval]; - [[NSNotificationCenter defaultCenter] addObserver: sharedLDClient - selector:@selector(userUpdated) - name: kLDUserUpdatedNotification object: nil]; - [[NSNotificationCenter defaultCenter] addObserver: sharedLDClient - selector:@selector(userUnchanged) - name: kLDUserNoChangeNotification object: nil]; - [[NSNotificationCenter defaultCenter] addObserver: sharedLDClient - selector:@selector(serverUnavailable) - name:kLDServerConnectionUnavailableNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver: sharedLDClient - selector:@selector(configFlagUpdated:) - name:kLDFlagConfigChangedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver: sharedLDClient - selector:@selector(handleClientUnauthorizedNotification) - name:kLDClientUnauthorizedNotification object:nil]; }); return sharedLDClient; } --(void)setLdUser:(LDUserModel*)user { - _ldUser = user; - [[LDDataManager sharedManager] createIdentifyEventWithUser:_ldUser config:self.ldConfig]; +-(NSString*)environmentName { + return kLDPrimaryEnvironmentName; } +-(void)setDelegate:(id)delegate { + _delegate = delegate; + self.primaryEnvironment.delegate = delegate; +} + +-(void)dealloc { + self.delegate = nil; +} + +#pragma mark - SDK Control + -(BOOL)start:(LDConfigBuilder *)inputConfigBuilder userBuilder:(LDUserBuilder *)inputUserBuilder { return [self start:[inputConfigBuilder build] withUserBuilder:inputUserBuilder]; } @@ -72,313 +70,253 @@ - (BOOL)start:(LDConfig *)inputConfig withUserBuilder:(LDUserBuilder *)inputUser return NO; } self.ldConfig = inputConfig; - [LDUtil setLogLevel:[self.ldConfig debugEnabled] ? DarklyLogLevelDebug : DarklyLogLevelCriticalOnly]; - + [NSURLSession setSharedLDSessionForConfig:self.ldConfig]; + self.clientStarted = YES; DEBUG_LOGX(@"LDClient started"); inputUserBuilder = inputUserBuilder ?: [[LDUserBuilder alloc] init]; self.ldUser = [inputUserBuilder build]; - + + self.primaryEnvironment = [LDEnvironment environmentForMobileKey:self.ldConfig.mobileKey config:self.ldConfig user:self.ldUser]; + self.primaryEnvironment.delegate = self.delegate; + [self.primaryEnvironment start]; + if (self.ldConfig.secondaryMobileKeys.count > 0) { + self.secondaryEnvironments = [NSMutableDictionary dictionaryWithCapacity:self.ldConfig.secondaryMobileKeys.count]; + } + for (NSString *secondaryKey in self.ldConfig.secondaryMobileKeys.allValues) { + LDEnvironment *secondaryEnvironment = [LDEnvironment environmentForMobileKey:secondaryKey config:self.ldConfig user:self.ldUser]; + [secondaryEnvironment start]; + self.secondaryEnvironments[secondaryKey] = secondaryEnvironment; + } [self setOnline:YES]; return YES; } -- (BOOL)updateUser:(LDUserBuilder *)builder { - DEBUG_LOGX(@"LDClient updateUser method called"); +-(void)setOnline:(BOOL)goOnline { + [self setOnline:goOnline completion:nil]; +} + +-(void)setOnline:(BOOL)goOnline completion:(void(^)(void))completion { if (!self.clientStarted) { - DEBUG_LOGX(@"LDClient aborted updateUser: client not started"); - return NO; + DEBUG_LOGX(@"LDClient not started yet!"); + if (completion) { + completion(); + } + return; } - if (!builder) { - DEBUG_LOGX(@"LDClient aborted updateUser: LDUserBuilder is nil"); - return NO; + self.willGoOnlineAfterDelay = goOnline; + if (goOnline == self.isOnline && [self environmentsMatchOnline:goOnline]) { + DEBUG_LOG(@"LDClient setOnline:%@ aborted. LDClient is already %@", goOnline ? @"YES" : @"NO", goOnline ? @"online" : @"offline"); + if (completion) { + completion(); + } + return; } - if (builder.key.length > 0 && [builder.key isEqualToString:self.ldUser.key]) { - self.ldUser = [LDUserBuilder compareNewBuilder:builder withUser:self.ldUser]; - [[LDDataManager sharedManager] saveUser:self.ldUser]; - } else { - self.ldUser = [builder build]; + if (!goOnline) { + DEBUG_LOGX(@"LDClient setOnline:NO called"); + [self _setOnline:NO completion:completion]; + return; } + [self.throttler runThrottled:^{ + if (!self.willGoOnlineAfterDelay) { + DEBUG_LOGX(@"LDClient setOnline:YES aborted. Client last received an offline request when the throttling timer expired."); + if (completion) { + completion(); + } + return; + } + DEBUG_LOGX(@"LDClient setOnline:YES called"); + [self _setOnline:YES completion:completion]; + }]; +} - [[LDClientManager sharedInstance] updateUser]; +-(BOOL)environmentsMatchOnline:(BOOL)online { + if (self.primaryEnvironment.isOnline != online) { + return NO; + } + for (LDEnvironment *environment in self.secondaryEnvironments.allValues) { + if (environment.isOnline != online) { + return NO; + } + } return YES; } -- (LDUserBuilder *)currentUserBuilder { - DEBUG_LOGX(@"LDClient currentUserBuilder method called"); - if (self.clientStarted) { - return [LDUserBuilder currentBuilder:self.ldUser]; - } else { - DEBUG_LOGX(@"LDClient not started yet!"); - return nil; +-(void)_setOnline:(BOOL)isOnline completion:(void(^)(void))completion { + self.isOnline = isOnline; + self.primaryEnvironment.online = isOnline; + for (LDEnvironment *secondaryEnvironment in self.secondaryEnvironments.allValues) { + secondaryEnvironment.online = isOnline; + } + if (completion) { + completion(); } } -- (BOOL)boolVariation:(NSString *)flagKey fallback:(BOOL)fallback{ - DEBUG_LOG(@"LDClient boolVariation method called for flagKey=%@ and fallback=%d", flagKey, fallback); - if (![flagKey isKindOfClass:[NSString class]]) { - NSLog(@"flagKey should be an NSString. Returning fallback value"); - return fallback; - } +- (BOOL)flush { + DEBUG_LOGX(@"LDClient flush method called"); if (!self.clientStarted) { DEBUG_LOGX(@"LDClient not started yet!"); - return fallback; + return NO; + } + BOOL result = [self.primaryEnvironment flush]; + for (LDEnvironment *secondaryEnvironment in self.secondaryEnvironments.allValues) { + result = result && [secondaryEnvironment flush]; } - LDFlagConfigValue *flagConfigValue = [self.ldUser.flagConfig flagConfigValueForFlagKey:flagKey]; - BOOL returnValue = flagConfigValue.value && [flagConfigValue.value isKindOfClass:[NSNumber class]] ? [flagConfigValue.value boolValue] : fallback; + return result; +} + +- (BOOL)stopClient { + DEBUG_LOGX(@"LDClient stop method called"); + if (!self.clientStarted) { + DEBUG_LOGX(@"LDClient not started yet!"); + return NO; + } - [[LDDataManager sharedManager] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:@(returnValue) - flagConfigValue:flagConfigValue - defaultFlagValue:@(fallback) - user:self.ldUser - config:self.ldConfig]; - return returnValue; + [self setOnline:NO]; + [self.primaryEnvironment stop]; + self.primaryEnvironment = nil; + for (LDEnvironment *secondaryEnvironment in self.secondaryEnvironments.allValues) { + [secondaryEnvironment stop]; + } + self.clientStarted = NO; + [self.secondaryEnvironments removeAllObjects]; + return YES; } -- (NSNumber*)numberVariation:(NSString *)flagKey fallback:(NSNumber*)fallback{ - DEBUG_LOG(@"LDClient numberVariation method called for flagKey=%@ and fallback=%@", flagKey, fallback); - if (![flagKey isKindOfClass:[NSString class]]) { - NSLog(@"flagKey should be an NSString. Returning fallback value"); +#pragma mark - Variation + +- (BOOL)boolVariation:(NSString *)flagKey fallback:(BOOL)fallback{ + if (!self.clientStarted) { + DEBUG_LOG(@"LDClient %@ flagKey:%@ fallback:%@ aborted. Client not started.", NSStringFromSelector(_cmd), flagKey, @(fallback)); return fallback; } + + DEBUG_LOG(@"LDClient %@ called flagKey:%@ fallback:%@", NSStringFromSelector(_cmd), flagKey, @(fallback)); + return [self.primaryEnvironment boolVariation:flagKey fallback:fallback]; +} + +- (NSNumber*)numberVariation:(NSString *)flagKey fallback:(NSNumber*)fallback{ if (!self.clientStarted) { - DEBUG_LOGX(@"LDClient not started yet!"); + DEBUG_LOG(@"LDClient %@ flagKey:%@ fallback:%@ aborted. Client not started.", NSStringFromSelector(_cmd), flagKey, fallback); return fallback; } - LDFlagConfigValue *flagConfigValue = [self.ldUser.flagConfig flagConfigValueForFlagKey:flagKey]; - NSNumber *returnValue = flagConfigValue.value && [flagConfigValue.value isKindOfClass:[NSNumber class]] ? flagConfigValue.value : fallback; - - [[LDDataManager sharedManager] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:returnValue - flagConfigValue:flagConfigValue - defaultFlagValue:fallback - user:self.ldUser - config:self.ldConfig]; - return returnValue; + DEBUG_LOG(@"LDClient %@ called flagKey:%@ fallback:%@", NSStringFromSelector(_cmd), flagKey, fallback); + return [self.primaryEnvironment numberVariation:flagKey fallback:fallback]; } - (double)doubleVariation:(NSString *)flagKey fallback:(double)fallback { - DEBUG_LOG(@"LDClient doubleVariation method called for flagKey=%@ and fallback=%f", flagKey, fallback); - if (![flagKey isKindOfClass:[NSString class]]) { - NSLog(@"flagKey should be an NSString. Returning fallback value"); - return fallback; - } if (!self.clientStarted) { - DEBUG_LOGX(@"LDClient not started yet!"); + DEBUG_LOG(@"LDClient %@ flagKey:%@ fallback:%@ aborted. Client not started.", NSStringFromSelector(_cmd), flagKey, @(fallback)); return fallback; } - LDFlagConfigValue *flagConfigValue = [self.ldUser.flagConfig flagConfigValueForFlagKey:flagKey]; - double returnValue = flagConfigValue.value && [flagConfigValue.value isKindOfClass:[NSNumber class]] ? [flagConfigValue.value doubleValue] : fallback; - - [[LDDataManager sharedManager] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:@(returnValue) - flagConfigValue:flagConfigValue - defaultFlagValue:@(fallback) - user:self.ldUser - config:self.ldConfig]; - return returnValue; + DEBUG_LOG(@"LDClient %@ called flagKey:%@ fallback:%@", NSStringFromSelector(_cmd), flagKey, @(fallback)); + return [self.primaryEnvironment doubleVariation:flagKey fallback:fallback]; } - (NSString*)stringVariation:(NSString *)flagKey fallback:(NSString*)fallback{ - DEBUG_LOG(@"LDClient stringVariation method called for flagKey=%@ and fallback=%@", flagKey, fallback); - if (![flagKey isKindOfClass:[NSString class]]) { - NSLog(@"flagKey should be an NSString. Returning fallback value"); - return fallback; - } if (!self.clientStarted) { - DEBUG_LOGX(@"LDClient not started yet!"); + DEBUG_LOG(@"LDClient %@ flagKey:%@ fallback:%@ aborted. Client not started.", NSStringFromSelector(_cmd), flagKey, fallback); return fallback; } - LDFlagConfigValue *flagConfigValue = [self.ldUser.flagConfig flagConfigValueForFlagKey:flagKey]; - NSString *returnValue = flagConfigValue.value && [flagConfigValue.value isKindOfClass:[NSString class]] ? flagConfigValue.value : fallback; - - [[LDDataManager sharedManager] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:returnValue - flagConfigValue:flagConfigValue - defaultFlagValue:fallback - user:self.ldUser - config:self.ldConfig]; - return returnValue; + DEBUG_LOG(@"LDClient %@ called flagKey:%@ fallback:%@", NSStringFromSelector(_cmd), flagKey, fallback); + return [self.primaryEnvironment stringVariation:flagKey fallback:fallback]; } - (NSArray*)arrayVariation:(NSString *)flagKey fallback:(NSArray*)fallback{ - DEBUG_LOG(@"LDClient arrayVariation method called for flagKey=%@ and fallback=%@", flagKey, fallback); - if (![flagKey isKindOfClass:[NSString class]]) { - NSLog(@"flagKey should be an NSString. Returning fallback value"); - return fallback; - } if (!self.clientStarted) { - DEBUG_LOGX(@"LDClient not started yet!"); + DEBUG_LOG(@"LDClient %@ flagKey:%@ fallback:%@ aborted. Client not started.", NSStringFromSelector(_cmd), flagKey, fallback); return fallback; } - LDFlagConfigValue *flagConfigValue = [self.ldUser.flagConfig flagConfigValueForFlagKey:flagKey]; - NSArray *returnValue = flagConfigValue.value && [flagConfigValue.value isKindOfClass:[NSArray class]] ? flagConfigValue.value : fallback; - - [[LDDataManager sharedManager] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:returnValue - flagConfigValue:flagConfigValue - defaultFlagValue:fallback - user:self.ldUser - config:self.ldConfig]; - return returnValue; + DEBUG_LOG(@"LDClient %@ called flagKey:%@ fallback:%@", NSStringFromSelector(_cmd), flagKey, fallback); + return [self.primaryEnvironment arrayVariation:flagKey fallback:fallback]; } - (NSDictionary*)dictionaryVariation:(NSString *)flagKey fallback:(NSDictionary*)fallback{ - DEBUG_LOG(@"LDClient dictionaryVariation method called for flagKey=%@ and fallback=%@", flagKey, fallback); - if (![flagKey isKindOfClass:[NSString class]]) { - NSLog(@"flagKey should be an NSString. Returning fallback value"); - return fallback; - } if (!self.clientStarted) { - DEBUG_LOGX(@"LDClient not started yet!"); + DEBUG_LOG(@"LDClient %@ flagKey:%@ fallback:%@ aborted. Client not started.", NSStringFromSelector(_cmd), flagKey, fallback); return fallback; } - LDFlagConfigValue *flagConfigValue = [self.ldUser.flagConfig flagConfigValueForFlagKey:flagKey]; - NSDictionary *returnValue = flagConfigValue.value && [flagConfigValue.value isKindOfClass:[NSDictionary class]] ? flagConfigValue.value : fallback; - - [[LDDataManager sharedManager] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:returnValue - flagConfigValue:flagConfigValue - defaultFlagValue:fallback - user:self.ldUser - config:self.ldConfig]; - return returnValue; + DEBUG_LOG(@"LDClient %@ called flagKey:%@ fallback:%@", NSStringFromSelector(_cmd), flagKey, fallback); + return [self.primaryEnvironment dictionaryVariation:flagKey fallback:fallback]; } -- (BOOL)track:(NSString *)eventName data:(NSDictionary *)dataDictionary -{ - DEBUG_LOG(@"LDClient track method called for event=%@ and data=%@", eventName, dataDictionary); - if (self.clientStarted) { - [[LDDataManager sharedManager] createCustomEventWithKey:eventName customData: dataDictionary user:self.ldUser config:self.ldConfig]; - return YES; - } else { +-(NSDictionary*)allFlags { + if (!self.clientStarted) { DEBUG_LOGX(@"LDClient not started yet!"); - return NO; + return nil; } + return self.primaryEnvironment.allFlags; } --(void)setOnline:(BOOL)goOnline { - [self setOnline:goOnline completion:nil]; -} +#pragma mark - Event Tracking --(void)setOnline:(BOOL)goOnline completion:(void(^)(void))completion { +-(BOOL)track:(NSString *)eventName data:(NSDictionary *)dataDictionary { + DEBUG_LOG(@"LDClient track method called for event=%@ and data=%@", eventName, dataDictionary); if (!self.clientStarted) { DEBUG_LOGX(@"LDClient not started yet!"); - if (completion) { - completion(); - } - return; - } - self.willGoOnlineAfterDelay = goOnline; - if (goOnline == self.isOnline) { - DEBUG_LOG(@"LDClient setOnline:%@ aborted. LDClient is already %@", goOnline ? @"YES" : @"NO", goOnline ? @"online" : @"offline"); - if (completion) { - completion(); - } - return; - } - - if (!goOnline) { - DEBUG_LOGX(@"LDClient setOnline:NO called"); - [self _setOnline:NO completion:completion]; - return; + return NO; } - [self.throttler runThrottled:^{ - if (!self.willGoOnlineAfterDelay) { - DEBUG_LOGX(@"LDClient setOnline:YES aborted. Client last received an offline request when the throttling timer expired."); - if (completion) { - completion(); - } - return; - } - DEBUG_LOGX(@"LDClient setOnline:YES called"); - [self _setOnline:YES completion:completion]; - }]; -} --(void)_setOnline:(BOOL)isOnline completion:(void(^)(void))completion { - self.isOnline = isOnline; - [[LDClientManager sharedInstance] setOnline:isOnline]; - if (completion) { - completion(); - } + return [self.primaryEnvironment track:eventName data:dataDictionary]; } -- (BOOL)flush { - DEBUG_LOGX(@"LDClient flush method called"); - if (self.clientStarted) { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager flushEvents]; - return YES; - } else { - DEBUG_LOGX(@"LDClient not started yet!"); - return NO; - } -} +#pragma mark - User -- (BOOL)stopClient { - DEBUG_LOGX(@"LDClient stop method called"); +- (BOOL)updateUser:(LDUserBuilder *)builder { + DEBUG_LOGX(@"LDClient updateUser method called"); if (!self.clientStarted) { - DEBUG_LOGX(@"LDClient not started yet!"); + DEBUG_LOGX(@"LDClient aborted updateUser: client not started"); + return NO; + } + if (!builder) { + DEBUG_LOGX(@"LDClient aborted updateUser: LDUserBuilder is nil"); return NO; } - [self setOnline:NO]; - self.clientStarted = NO; - return YES; -} - -// Notification handler for ClientManager user updated --(void)userUpdated { - if (![self.delegate respondsToSelector:@selector(userDidUpdate)]) { return; } - [NSThread performOnMainThread:^{ - [self.delegate userDidUpdate]; - }]; -} + self.ldUser = [builder build]; + [self.primaryEnvironment updateUser:self.ldUser]; + for (LDEnvironment *secondaryEnvironment in self.secondaryEnvironments.allValues) { + [secondaryEnvironment updateUser:self.ldUser]; + } -// Notification handler for ClientManager user unchanged --(void)userUnchanged { - if (![self.delegate respondsToSelector:@selector(userUnchanged)]) { return; } - [NSThread performOnMainThread:^{ - [self.delegate userUnchanged]; - }]; + return YES; } -// Notification handler for ClientManager server connection failed --(void)serverUnavailable { - if (![self.delegate respondsToSelector:@selector(serverConnectionUnavailable)]) { return; } - [NSThread performOnMainThread:^{ - [self.delegate serverConnectionUnavailable]; - }]; +- (LDUserBuilder *)currentUserBuilder { + DEBUG_LOGX(@"LDClient currentUserBuilder method called"); + if (self.clientStarted) { + return [LDUserBuilder currentBuilder:self.ldUser]; + } else { + DEBUG_LOGX(@"LDClient not started yet!"); + return nil; + } } -// Notification handler for DataManager config flag update --(void)configFlagUpdated:(NSNotification *)notification { - if (![self.delegate respondsToSelector:@selector(featureFlagDidUpdate:)]) { return; } - [NSThread performOnMainThread:^{ - [self.delegate featureFlagDidUpdate:[notification.userInfo objectForKey:@"flagkey"]]; - }]; -} +#pragma mark - Multiple Environments -//Notification handler for Client Unauthorized notification --(void)handleClientUnauthorizedNotification { - [NSThread performOnMainThread:^{ - DEBUG_LOGX(@"LDClient received Client Unauthorized notification. Taking LDClient offline."); - [self setOnline:NO]; - }]; -} ++(id)environmentForMobileKeyNamed:(NSString*)name { + if (![LDClient sharedInstance].clientStarted) { + return nil; + } + if (![name isEqualToString:kLDPrimaryEnvironmentName] && ![[LDClient sharedInstance].ldConfig.secondaryMobileKeys.allKeys containsObject:name]) { + NSException *missingKeyNameException = + [NSException exceptionWithName:NSInvalidArgumentException reason:@"Environment key name does not appear in LDConfig.secondaryMobileKeys." userInfo:nil]; + @throw missingKeyNameException; --(void)dealloc { - self.delegate = nil; + } + if ([name isEqualToString:kLDPrimaryEnvironmentName]) { + return [LDClient sharedInstance].primaryEnvironment; + } + return [LDClient sharedInstance].secondaryEnvironments[[LDClient sharedInstance].ldConfig.secondaryMobileKeys[name]]; } @end diff --git a/Darkly/LDClientInterface.h b/Darkly/LDClientInterface.h new file mode 100644 index 00000000..cebe6037 --- /dev/null +++ b/Darkly/LDClientInterface.h @@ -0,0 +1,99 @@ +// +// LDClientInterface.h +// Darkly +// +// Created by Mark Pokorny on 11/1/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import + +@protocol ClientDelegate; +@class LDUserBuilder; + +@protocol LDClientInterface + +@property (nonatomic, copy, readonly) NSString *environmentName; ///The name associated with the environment in LDConfig. +@property (nonatomic, weak) id delegate; +@property (nonatomic, strong, readonly) NSDictionary *allFlags; ///Dictionary of for all feature flags in the environment + +#pragma mark - Variation + +/** + * Retrieve a feature flag value. If the configuration for this feature + * flag is retrieved from the server that value is returned, otherwise + * the fallback is returned. + * + * @param featureKey Key of feature flag + * @param fallback Fallback value for feature flag + * @return the feature flag value + */ +- (BOOL)boolVariation:(NSString *)featureKey fallback:(BOOL)fallback; + +/** + * Retrieve a feature flag value. If the configuration for this feature + * flag is retrieved from the server that value is returned, otherwise + * the fallback is returned. + * + * @param featureKey Key of feature flag + * @param fallback Fallback value for feature flag + * @return the feature flag value + */ +- (NSNumber*)numberVariation:(NSString *)featureKey fallback:(NSNumber*)fallback; + +/** + * Retrieve a feature flag value. If the configuration for this feature + * flag is retrieved from the server that value is returned, otherwise + * the fallback is returned. + * + * @param featureKey Key of feature flag + * @param fallback Fallback value for feature flag + * @return the feature flag value + */ +- (double)doubleVariation:(NSString *)featureKey fallback:(double)fallback; + +/** + * Retrieve a feature flag value. If the configuration for this feature + * flag is retrieved from the server that value is returned, otherwise + * the fallback is returned. + * + * @param featureKey Key of feature flag + * @param fallback Fallback value for feature flag + * @return the feature flag value + */ +- (NSString*)stringVariation:(NSString *)featureKey fallback:(NSString*)fallback; + +/** + * Retrieve a feature flag value. If the configuration for this feature + * flag is retrieved from the server that value is returned, otherwise + * the fallback is returned. + * + * @param featureKey Key of feature flag + * @param fallback Fallback value for feature flag + * @return the feature flag value + */ +- (NSArray*)arrayVariation:(NSString *)featureKey fallback:(NSArray*)fallback; + +/** + * Retrieve a feature flag value. If the configuration for this feature + * flag is retrieved from the server that value is returned, otherwise + * the fallback is returned. + * + * @param featureKey Key of feature flag + * @param fallback Fallback value for feature flag + * @return the feature flag value + */ +- (NSDictionary*)dictionaryVariation:(NSString *)featureKey fallback:(NSDictionary*)fallback; + +#pragma mark - Event + +/** + * Track a custom event. + * + * @param eventName Name of the custom event + * @param dataDictionary Data to be attached to custom event + * @return whether the event was successfully recorded + */ +- (BOOL)track:(NSString *)eventName data:(NSDictionary *)dataDictionary; + +@end diff --git a/Darkly/LDClientManager.h b/Darkly/LDClientManager.h deleted file mode 100644 index a5cdad2e..00000000 --- a/Darkly/LDClientManager.h +++ /dev/null @@ -1,39 +0,0 @@ -// -// Copyright © 2015 Catamorphic Co. All rights reserved. -// - - -#import "LDRequestManager.h" -#if TARGET_OS_OSX -#import -#else -#import -#endif - -#if TARGET_OS_WATCH -@interface LDClientManager : NSObject { -} -#elif TARGET_OS_OSX -@interface LDClientManager : NSObject { -} -#else -@interface LDClientManager : NSObject { -} -#endif - -@property (nonatomic, assign, getter=isOnline) BOOL online; - -+(LDClientManager *)sharedInstance; - -- (void)syncWithServerForEvents; -- (void)syncWithServerForConfig; -- (void)processedEvents:(BOOL)success jsonEventArray:(NSArray*)jsonEventArray responseDate:(NSDate*)responseDate; -- (void)processedConfig:(BOOL)success jsonConfigDictionary:(NSDictionary *)jsonConfigDictionary; -- (void)startPolling; -- (void)stopPolling; -- (void)updateUser; -- (void)willEnterBackground; -- (void)willEnterForeground; -- (void)flushEvents; - -@end diff --git a/Darkly/LDClientManager.m b/Darkly/LDClientManager.m deleted file mode 100644 index 1a9c2ec4..00000000 --- a/Darkly/LDClientManager.m +++ /dev/null @@ -1,404 +0,0 @@ -// -// Copyright © 2015 Catamorphic Co. All rights reserved. -// - -#import "LDClientManager.h" -#import "LDClient.h" -#import "LDPollingManager.h" -#import "LDDataManager.h" -#import "LDUtil.h" -#import "LDUserModel.h" -#import "LDEventModel.h" -#import "LDFlagConfigModel.h" -#import "NSDictionary+JSON.h" -#import -#import "LDEvent+Unauthorized.h" -#import "LDEvent+EventTypes.h" - -NSString * const kLDClientManagerStreamMethod = @"meval"; - -@interface LDClientManager() - -@property(nonatomic, strong, readonly) LDEventSource *eventSource; -@property(nonatomic, strong) NSDate *backgroundTime; - -@end - -@implementation LDClientManager { - BOOL _online; -} - -@synthesize eventSource; - -+(LDClientManager *)sharedInstance { - static LDClientManager *sharedApiManager = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedApiManager = [[self alloc] init]; -#if TARGET_OS_IOS || TARGET_OS_TV - [[NSNotificationCenter defaultCenter] addObserver:sharedApiManager selector:@selector(willEnterForeground) name:UIApplicationDidBecomeActiveNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:sharedApiManager selector:@selector(willEnterBackground) name:UIApplicationWillResignActiveNotification object:nil]; -#elif TARGET_OS_OSX - [[NSNotificationCenter defaultCenter] addObserver:sharedApiManager selector:@selector(willEnterForeground) name:NSApplicationDidBecomeActiveNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:sharedApiManager selector:@selector(willEnterBackground) name:NSApplicationWillResignActiveNotification object:nil]; -#endif - [[NSNotificationCenter defaultCenter] addObserver:sharedApiManager selector:@selector(backgroundFetchInitiated) name:kLDBackgroundFetchInitiated object:nil]; - - }); - return sharedApiManager; -} - -- (void)setOnline:(BOOL)online { - _online = online; - _online ? [self startPolling] : [self stopPolling]; -} - -- (BOOL)isOnline { - return _online; -} - -- (void)startPolling { - if (!self.isOnline) { - DEBUG_LOGX(@"ClientManager startPolling aborted - manager is offline"); - return; - } - - LDPollingManager *pollingMgr = [LDPollingManager sharedInstance]; - LDConfig *config = [[LDClient sharedInstance] ldConfig]; - - [pollingMgr startEventPolling]; - - if ([config streaming]) { - [self configureEventSource]; - } - else{ - [self syncWithServerForConfig]; - [pollingMgr startConfigPolling]; - } -} - - -- (void)stopPolling { - DEBUG_LOGX(@"ClientManager stopPolling method called"); - LDPollingManager *pollingMgr = [LDPollingManager sharedInstance]; - - [pollingMgr stopEventPolling]; - - if ([[[LDClient sharedInstance] ldConfig] streaming]) { - [self stopEventSource]; - } - else{ - [pollingMgr stopConfigPolling]; - } - - [self flushEvents]; -} - --(void)updateUser { - if (!self.isOnline) { - DEBUG_LOGX(@"ClientManager updateUser aborted - manager is offline"); - return; - } - if (self.eventSource) { - [self stopEventSource]; - } - [[LDPollingManager sharedInstance] stopConfigPolling]; - - if ([[[LDClient sharedInstance] ldConfig] streaming]) { - [self configureEventSource]; - } else { - [self syncWithServerForConfig]; - [[LDPollingManager sharedInstance] startConfigPolling]; - } -} - -- (void)willEnterBackground { - DEBUG_LOGX(@"ClientManager entering background"); - LDPollingManager *pollingMgr = [LDPollingManager sharedInstance]; - - [pollingMgr suspendEventPolling]; - - if ([[[LDClient sharedInstance] ldConfig] streaming]) { - [self stopEventSource]; - } - else{ - [pollingMgr suspendConfigPolling]; - } - - [self flushEvents]; - - self.backgroundTime = [NSDate date]; - -} - -- (void)willEnterForeground { - DEBUG_LOGX(@"ClientManager entering foreground"); - LDPollingManager *pollingMgr = [LDPollingManager sharedInstance]; - [pollingMgr resumeEventPolling]; - - LDClient *client = [LDClient sharedInstance]; - - if ([[client ldConfig] streaming]) { - [self configureEventSource]; - } - else{ - [pollingMgr resumeConfigPolling]; - } -} - -- (void)configureEventSource { - @synchronized (self) { - if (!self.isOnline) { - DEBUG_LOGX(@"ClientManager configureEventSource aborted - manager is offline"); - return; - } - - if (eventSource) { - DEBUG_LOGX(@"ClientManager aborting event source creation - event source running"); - return; - } - - eventSource = [self eventSourceForUser:[LDClient sharedInstance].ldUser config:[LDClient sharedInstance].ldConfig httpHeaders:[self httpHeadersForEventSource]]; - - __weak typeof(self) weakSelf = self; - [eventSource onMessage:^(LDEvent *event) { - __strong typeof(weakSelf) strongSelf = weakSelf; - [strongSelf handlePingEvent:event]; - [strongSelf handlePutEvent:event]; - [strongSelf handlePatchEvent:event]; - [strongSelf handleDeleteEvent:event]; - }]; - - [eventSource onError:^(LDEvent *event) { - [[NSNotificationCenter defaultCenter] postNotificationName:kLDServerConnectionUnavailableNotification object:nil]; - if (![event isUnauthorizedEvent]) { return; } - [[NSNotificationCenter defaultCenter] postNotificationName:kLDClientUnauthorizedNotification object:nil]; - }]; - } -} - -- (LDEventSource*)eventSourceForUser:(LDUserModel*)user config:(LDConfig*)config httpHeaders:(NSDictionary*)httpHeaders { - LDEventSource *eventSource; - if (config.useReport) { - eventSource = [LDEventSource eventSourceWithURL:[self eventSourceUrlForUser:user config:config] - httpHeaders:httpHeaders - connectMethod:kHTTPMethodReport - connectBody:[[[user dictionaryValueWithPrivateAttributesAndFlagConfig:NO] jsonString] dataUsingEncoding:NSUTF8StringEncoding]]; - } else { - eventSource = [LDEventSource eventSourceWithURL:[self eventSourceUrlForUser:user config:config] httpHeaders:httpHeaders connectMethod:nil connectBody:nil]; - } - return eventSource; -} - -- (NSURL*)eventSourceUrlForUser:(LDUserModel *)user config:(LDConfig*)config { - NSString *eventStreamUrl = [config.streamUrl stringByAppendingPathComponent:kLDClientManagerStreamMethod]; - if (!config.useReport) { - NSString *encodedUser = [LDUtil base64UrlEncodeString:[[user dictionaryValueWithPrivateAttributesAndFlagConfig:NO] jsonString]]; - eventStreamUrl = [eventStreamUrl stringByAppendingPathComponent:encodedUser]; - } - return [NSURL URLWithString:eventStreamUrl]; -} - -- (void)handlePingEvent:(LDEvent*)event { - if (![event.event isEqualToString:kLDEventTypePing]) { return; } - [self syncWithServerForConfig]; -} - -- (void)handlePutEvent:(LDEvent*)event { - if (![event.event isEqualToString:kLDEventTypePut]) { return; } - if (event.data.length == 0) { - DEBUG_LOGX(@"ClientManager aborted handlePutEvent - event contains no data"); - [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserNoChangeNotification object:nil]; - return; - } - NSDictionary *newConfigDictionary = [NSJSONSerialization JSONObjectWithData:[event.data dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil]; - if (!newConfigDictionary) { - DEBUG_LOGX(@"ClientManager aborted handlePutEvent - event contains json data could not be read"); - [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserNoChangeNotification object:nil]; - return; - } - - LDFlagConfigModel *newConfig = [[LDFlagConfigModel alloc] initWithDictionary:newConfigDictionary]; - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - - NSString *updateResultNotificationName = [user.flagConfig isEqualToConfig:newConfig] ? kLDUserNoChangeNotification : kLDUserUpdatedNotification; - user.flagConfig = newConfig; - [[LDDataManager sharedManager] saveUser:user]; - [[NSNotificationCenter defaultCenter] postNotificationName:updateResultNotificationName object:nil]; - DEBUG_LOG(@"ClientManager posted %@ following user config update from SSE put event", updateResultNotificationName); -} - -- (void)handlePatchEvent:(LDEvent*)event { - if (![event.event isEqualToString:kLDEventTypePatch]) { return; } - if (event.data.length == 0) { - DEBUG_LOGX(@"ClientManager aborted handlePatchEvent - event contains no data"); - [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserNoChangeNotification object:nil]; - return; - } - NSDictionary *patchDictionary = [NSJSONSerialization JSONObjectWithData:[event.data dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil]; - if (!patchDictionary) { - DEBUG_LOGX(@"ClientManager aborted handlePatchEvent - event json data could not be read"); - [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserNoChangeNotification object:nil]; - return; - } - - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - NSDictionary *originalFlagConfig = user.flagConfig.featuresJsonDictionary; - - [user.flagConfig addOrReplaceFromDictionary:patchDictionary]; - - if ([user.flagConfig hasFeaturesEqualToDictionary:originalFlagConfig]) { - DEBUG_LOGX(@"ClientManager handlePatchEvent resulted in no change to the flag config"); - [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserNoChangeNotification object:nil]; - return; - } - - [[LDDataManager sharedManager] saveUser:user]; - [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserUpdatedNotification object:nil]; - DEBUG_LOGX(@"ClientManager posted Darkly.UserUpdatedNotification following user config update from SSE patch event"); -} - -- (void)handleDeleteEvent:(LDEvent*)event { - if (![event.event isEqualToString:kLDEventTypeDelete]) { return; } - if (event.data.length == 0) { - DEBUG_LOGX(@"ClientManager aborted handleDeleteEvent - event contains no data"); - [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserNoChangeNotification object:nil]; - return; - } - NSDictionary *deleteDictionary = [NSJSONSerialization JSONObjectWithData:[event.data dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil]; - if (!deleteDictionary) { - DEBUG_LOGX(@"ClientManager aborted handleDeleteEvent - event json data could not be read"); - [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserNoChangeNotification object:nil]; - return; - } - - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - NSDictionary *originalFlagConfig = user.flagConfig.featuresJsonDictionary; - - [user.flagConfig deleteFromDictionary:deleteDictionary]; - - if ([user.flagConfig hasFeaturesEqualToDictionary:originalFlagConfig]) { - DEBUG_LOGX(@"ClientManager handleDeleteEvent resulted in no change to the flag config"); - [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserNoChangeNotification object:nil]; - return; - } - - [[LDDataManager sharedManager] saveUser:user]; - [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserUpdatedNotification object:nil]; - DEBUG_LOGX(@"ClientManager posted Darkly.UserUpdatedNotification following user config update from SSE delete event"); -} - -- (void)postClientUnauthorizedNotification { - [[NSNotificationCenter defaultCenter] postNotificationName:kLDClientUnauthorizedNotification object:nil]; -} - -- (void)stopEventSource { - @synchronized (self) { - DEBUG_LOGX(@"ClientManager stopping event source."); - [eventSource close]; - eventSource = nil; - } -} - -- (void)backgroundFetchInitiated { - NSTimeInterval time = [[NSDate date] timeIntervalSinceDate:self.backgroundTime]; - LDConfig *config = [[LDClient sharedInstance] ldConfig]; - if (time >= [config.backgroundFetchInterval doubleValue]) { - [self syncWithServerForConfig]; - } -} - --(void)syncWithServerForEvents { - if (!self.isOnline) { - DEBUG_LOGX(@"ClientManager is in offline mode so won't sync events with server"); - return; - } - - DEBUG_LOGX(@"ClientManager syncing events with server"); - - [[LDDataManager sharedManager] createSummaryEventWithTracker:[LDClient sharedInstance].ldUser.flagConfigTracker config:[LDClient sharedInstance].ldConfig]; - - [[LDDataManager sharedManager] allEventDictionaries:^(NSArray *eventDictionaries) { - [[LDClient sharedInstance].ldUser resetTracker]; - if (eventDictionaries) { - [[LDRequestManager sharedInstance] performEventRequest:eventDictionaries]; - } else { - DEBUG_LOGX(@"ClientManager has no events so won't sync events with server"); - } - }]; -} - --(void)syncWithServerForConfig { - if (!self.isOnline) { - DEBUG_LOGX(@"ClientManager is in offline mode so won't sync config with server"); - return; - } - - if (![LDClient sharedInstance].ldUser) { - DEBUG_LOGX(@"ClientManager has no user so won't sync config with server"); - return; - } - - [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; -} - -- (void)flushEvents { - if (!self.isOnline) { - DEBUG_LOGX(@"ClientManager flushEvents aborted - manager is offline"); - return; - } - [self syncWithServerForEvents]; -} - -- (void)processedEvents:(BOOL)success jsonEventArray:(NSArray *)jsonEventArray responseDate:(NSDate*)responseDate { - // If Success - if (success) { - DEBUG_LOGX(@"ClientManager processedEvents method called after receiving successful response from server"); - // Audit cached events versus processed Events and only keep difference - if (jsonEventArray) { - [[LDDataManager sharedManager] deleteProcessedEvents:jsonEventArray]; - } - [LDDataManager sharedManager].lastEventResponseDate = responseDate; - } -} - -- (void)processedConfig:(BOOL)success jsonConfigDictionary:(NSDictionary *)jsonConfigDictionary { - if (!success) { - DEBUG_LOGX(@"ClientManager processedConfig method called after receiving failure response from server"); - [[NSNotificationCenter defaultCenter] postNotificationName: kLDServerConnectionUnavailableNotification - object: nil]; - return; - } - - DEBUG_LOGX(@"ClientManager processedConfig method called after receiving successful response from server"); - - LDFlagConfigModel *newConfig = [[LDFlagConfigModel alloc] initWithDictionary:jsonConfigDictionary]; - if (!newConfig || [[LDClient sharedInstance].ldUser.flagConfig isEqualToConfig:newConfig]) { - [[LDClient sharedInstance].ldUser.flagConfig updateEventTrackingContextFromConfig:newConfig]; - //Notify interested clients and bail out if no new config, or the new config equals the existing config - [[NSNotificationCenter defaultCenter] postNotificationName: kLDUserNoChangeNotification - object: nil]; - DEBUG_LOGX(@"ClientManager posted Darkly.UserNoChangeNotification following user config update"); - return; - } - - LDUserModel *user = [LDClient sharedInstance].ldUser; - user.flagConfig = newConfig; - [[LDDataManager sharedManager] saveUser:user]; // Save context - - [[NSNotificationCenter defaultCenter] postNotificationName: kLDUserUpdatedNotification - object: nil]; - DEBUG_LOGX(@"ClientManager posted Darkly.UserUpdatedNotification following user config update"); -} - -- (NSDictionary *)httpHeadersForEventSource { - NSMutableDictionary *headers = [[NSMutableDictionary alloc] init]; - - NSString *authKey = [kHeaderMobileKey stringByAppendingString:[[[LDClient sharedInstance] ldConfig] mobileKey]]; - - [headers setObject:authKey forKey:@"Authorization"]; - [headers setObject:[@"iOS/" stringByAppendingString:kClientVersion] forKey:@"User-Agent"]; - return headers; -} - -@end diff --git a/Darkly/LDConfig+LaunchDarkly.h b/Darkly/LDConfig+LaunchDarkly.h new file mode 100644 index 00000000..3d62c8a0 --- /dev/null +++ b/Darkly/LDConfig+LaunchDarkly.h @@ -0,0 +1,17 @@ +// +// LDConfig+LaunchDarkly.h +// Darkly +// +// Created by Mark Pokorny on 11/21/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDConfig.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface LDConfig (LaunchDarkly) +@property (nonatomic, strong, readonly) NSArray *mobileKeys; +@end + +NS_ASSUME_NONNULL_END diff --git a/Darkly/LDConfig+LaunchDarkly.m b/Darkly/LDConfig+LaunchDarkly.m new file mode 100644 index 00000000..ca413901 --- /dev/null +++ b/Darkly/LDConfig+LaunchDarkly.m @@ -0,0 +1,17 @@ +// +// LDConfig+LaunchDarkly.m +// Darkly +// +// Created by Mark Pokorny on 11/21/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDConfig+LaunchDarkly.h" + +@implementation LDConfig (LaunchDarkly) +-(NSArray*)mobileKeys { + NSMutableArray *keys = [NSMutableArray arrayWithArray:self.secondaryMobileKeys.allValues]; + [keys insertObject:self.mobileKey atIndex:0]; + return [keys copy]; +} +@end diff --git a/Darkly/LDDataManager.h b/Darkly/LDDataManager.h deleted file mode 100644 index f2557fca..00000000 --- a/Darkly/LDDataManager.h +++ /dev/null @@ -1,49 +0,0 @@ -// -// Copyright © 2015 Catamorphic Co. All rights reserved. -// - -#import -#import "LDUserModel.h" - -@class LDFlagConfigValue; -@class LDFlagConfigTracker; - -extern int const kUserCacheSize; - -@interface LDDataManager : NSObject - -@property (nonatomic, strong) NSDate *lastEventResponseDate; - -+(LDDataManager *)sharedManager; - --(void) allEventDictionaries:(void (^)(NSArray *eventDictionaries))completion; --(NSMutableDictionary*)retrieveUserDictionary; --(NSMutableArray*)retrieveEventsArray; --(LDUserModel*)findUserWithkey: (NSString *)key; --(void)createFlagEvaluationEventsWithFlagKey:(NSString*)flagKey - reportedFlagValue:(id)reportedFlagValue - flagConfigValue:(LDFlagConfigValue*)flagConfigValue - defaultFlagValue:(id)defaultFlagValue - user:(LDUserModel*)user - config:(LDConfig*)config; --(void)createFeatureEventWithFlagKey:(NSString*)flagKey - reportedFlagValue:(id)reportedFlagValue - flagConfigValue:(LDFlagConfigValue*)flagConfigValue - defaultFlagValue:(id)defaultFlagValue - user:(LDUserModel*)user - config:(LDConfig*)config; --(void)createCustomEventWithKey:(NSString*)eventKey customData:(NSDictionary*)customData user:(LDUserModel*)user config:(LDConfig*)config; --(void)createIdentifyEventWithUser:(LDUserModel*)user config:(LDConfig*)config; --(void)createSummaryEventWithTracker:(LDFlagConfigTracker*)tracker config:(LDConfig*)config; --(void)createDebugEventWithFlagKey:(NSString *)flagKey - reportedFlagValue:(id)reportedFlagValue - flagConfigValue:(LDFlagConfigValue*)flagConfigValue - defaultFlagValue:(id)defaultFlagValue - user:(LDUserModel*)user - config:(LDConfig*)config; --(void)saveUser: (LDUserModel *) user; --(void)saveUserDeprecated:(LDUserModel *)user __deprecated_msg("Use saveUser: instead"); --(void)deleteProcessedEvents: (NSArray *) processedJsonArray; --(void)flushEventsDictionary; - -@end diff --git a/Darkly/LDDataManager.m b/Darkly/LDDataManager.m deleted file mode 100644 index 7883299f..00000000 --- a/Darkly/LDDataManager.m +++ /dev/null @@ -1,297 +0,0 @@ -// -// Copyright © 2015 Catamorphic Co. All rights reserved. -// - - -#import "LDDataManager.h" -#import "LDEventModel.h" -#import "LDUtil.h" -#import "LDFlagConfigModel.h" -#import "LDFlagConfigValue.h" -#import "LDEventTrackingContext.h" -#import "LDFlagConfigTracker.h" -#import "NSDate+ReferencedDate.h" -#import "NSThread+MainExecutable.h" - -int const kUserCacheSize = 5; - -static NSString * const kFlagKey = @"flagkey"; - -@interface LDDataManager() - -@property (strong, atomic) NSMutableArray *eventsArray; - -@end - -@implementation LDDataManager - -dispatch_queue_t saveUserQueue; -dispatch_queue_t eventsQueue; - -+ (id)sharedManager { - static LDDataManager *sharedDataManager = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedDataManager = [[self alloc] init]; - sharedDataManager.eventsArray = [[NSMutableArray alloc] init]; - saveUserQueue = dispatch_queue_create("com.launchdarkly.dataManager.saveUserQueue", DISPATCH_QUEUE_SERIAL); - eventsQueue = dispatch_queue_create("com.launchdarkly.dataManager.eventQueue", DISPATCH_QUEUE_SERIAL); - }); - return sharedDataManager; -} - -#pragma mark - users --(void) purgeOldUser: (NSMutableDictionary *)dictionary { - if (dictionary && [dictionary count] >= kUserCacheSize) { - - NSArray *sortedKeys = [dictionary keysSortedByValueUsingComparator: ^(LDUserModel *user1, LDUserModel *user2) { - return [user1.updatedAt compare:user2.updatedAt]; - }]; - - [dictionary removeObjectForKey:sortedKeys.firstObject]; - } -} - --(void) saveUser: (LDUserModel *) user { - [self saveUser:user asDict:YES completion:nil]; -} - --(void) saveUserDeprecated:(LDUserModel *)user { - [self saveUser:user asDict:NO completion:nil]; -} - --(void) saveUser:(LDUserModel *)user asDict:(BOOL)asDict completion:(void (^)(void))completion { - LDUserModel *userCopy = [[LDUserModel alloc] initWithDictionary:[user dictionaryValueWithPrivateAttributesAndFlagConfig:YES]]; //Preserve the user while waiting to save on the saveQueue - dispatch_async(saveUserQueue, ^{ - NSMutableDictionary *userDictionary = [self retrieveUserDictionary]; - if (userDictionary) { - LDUserModel *resultUser = [userDictionary objectForKey:userCopy.key]; - if (resultUser) { - // User is found - [self compareConfigForUser:resultUser withNewUser:userCopy]; - userCopy.updatedAt = [NSDate date]; - userDictionary[userCopy.key] = userCopy; - } else { - // User is not found so need to create and purge old users - [self compareConfigForUser:nil withNewUser:userCopy]; - [self purgeOldUser: userDictionary]; - userCopy.updatedAt = [NSDate date]; - userDictionary[userCopy.key] = userCopy; - } - } else { - // No Dictionary exists so create - [self compareConfigForUser:nil withNewUser:userCopy]; - userDictionary = [[NSMutableDictionary alloc] init]; - userDictionary[userCopy.key] = userCopy; - } - userDictionary[userCopy.key] = userCopy; - if (asDict) { - [self storeUserDictionary:userDictionary]; - } - else{ - [self deprecatedStoreUserDictionary:userDictionary]; - } - DEBUG_LOG(@"LDDataManager saved user:%@ %@", userCopy.key, userCopy); - if (completion != nil) { - [NSThread performOnMainThread:^{ - completion(); - }]; - } - }); -} - --(LDUserModel *)findUserWithkey: (NSString *)key { - LDUserModel *resultUser = nil; - NSDictionary *userDictionary = [self retrieveUserDictionary]; - if (userDictionary) { - resultUser = [userDictionary objectForKey:key]; - if (resultUser) { - DEBUG_LOG(@"LDDataManager found cached user:%@ %@", resultUser.key, resultUser); - resultUser.updatedAt = [NSDate date]; - } - } - return resultUser; -} - -- (void)compareConfigForUser:(LDUserModel *)user withNewUser:(LDUserModel *)newUser { - for (NSString *key in [newUser.flagConfig dictionaryValueIncludeNulls:NO]) { - if(user == nil || ![[newUser.flagConfig flagValueForFlagKey:key] isEqual:[user.flagConfig flagValueForFlagKey:key]]) { - [[NSNotificationCenter defaultCenter] postNotificationName:kLDFlagConfigChangedNotification object:nil userInfo:[NSDictionary dictionaryWithObject:key forKey:kFlagKey]]; - } - } -} - -- (void)storeUserDictionary:(NSDictionary *)userDictionary { - NSMutableDictionary *archiveDictionary = [[NSMutableDictionary alloc] init]; - for (NSString *key in userDictionary) { - if (![[userDictionary objectForKey:key] isKindOfClass:[LDUserModel class]]) { continue; } - [archiveDictionary setObject:[[userDictionary objectForKey:key] dictionaryValueWithPrivateAttributesAndFlagConfig:YES] forKey:key]; - } - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - [defaults setObject:archiveDictionary forKey:kUserDictionaryStorageKey]; - [defaults synchronize]; -} - -- (void)deprecatedStoreUserDictionary:(NSDictionary *)userDictionary { - NSMutableDictionary *archiveDictionary = [[NSMutableDictionary alloc] init]; - for (NSString *key in userDictionary) { - NSData *userEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:(LDUserModel *)[userDictionary objectForKey:key]]; - [archiveDictionary setObject:userEncodedObject forKey:key]; - } - - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - [defaults setObject:archiveDictionary forKey:kUserDictionaryStorageKey]; - [defaults synchronize]; -} - -- (NSMutableDictionary *)retrieveUserDictionary { - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - NSDictionary *encodedDictionary = [defaults objectForKey:kUserDictionaryStorageKey]; - NSMutableDictionary *retrievalDictionary = [[NSMutableDictionary alloc] initWithDictionary:encodedDictionary]; - for (NSString *key in encodedDictionary) { - LDUserModel *decodedUser; - if ([[encodedDictionary objectForKey:key] isKindOfClass:[NSData class]]) { - decodedUser = [NSKeyedUnarchiver unarchiveObjectWithData:(NSData *)[encodedDictionary objectForKey:key]]; - } - else{ - decodedUser = [[LDUserModel alloc] initWithDictionary:[encodedDictionary objectForKey:key]]; - } - if (decodedUser == nil) { - continue; - } - [retrievalDictionary setObject:decodedUser forKey:key]; - } - return retrievalDictionary; -} - -#pragma mark - events - --(void)createFlagEvaluationEventsWithFlagKey:(NSString*)flagKey - reportedFlagValue:(id)reportedFlagValue - flagConfigValue:(LDFlagConfigValue*)flagConfigValue - defaultFlagValue:(id)defaultFlagValue - user:(LDUserModel*)user - config:(LDConfig*)config { - [self createFeatureEventWithFlagKey:flagKey reportedFlagValue:reportedFlagValue flagConfigValue:flagConfigValue defaultFlagValue:defaultFlagValue user:user config:config]; - [self createDebugEventWithFlagKey:flagKey reportedFlagValue:reportedFlagValue flagConfigValue:flagConfigValue defaultFlagValue:defaultFlagValue user:user config:config]; - [user.flagConfigTracker logRequestForFlagKey:flagKey reportedFlagValue:reportedFlagValue flagConfigValue:flagConfigValue defaultValue:defaultFlagValue]; -} - --(void)createFeatureEventWithFlagKey:(NSString*)flagKey - reportedFlagValue:(id)reportedFlagValue - flagConfigValue:(LDFlagConfigValue*)flagConfigValue - defaultFlagValue:(id)defaultFlagValue - user:(LDUserModel*)user - config:(LDConfig*)config { - if (!flagConfigValue.eventTrackingContext || (flagConfigValue.eventTrackingContext && !flagConfigValue.eventTrackingContext.trackEvents)) { - DEBUG_LOG(@"Tracking is off. Discarding feature event %@", flagKey); - return; - } - DEBUG_LOG(@"Creating feature event for feature:%@ with flagConfigValue:%@ and fallback:%@", flagKey, flagConfigValue, defaultFlagValue); - [self addEventDictionary:[[LDEventModel featureEventWithFlagKey:flagKey - reportedFlagValue:reportedFlagValue - flagConfigValue:flagConfigValue - defaultFlagValue:defaultFlagValue - user:user - inlineUser:config.inlineUserInEvents] - dictionaryValueUsingConfig:config]]; -} - --(void)createCustomEventWithKey:(NSString *)eventKey customData:(NSDictionary *)customData user:(LDUserModel*)user config:(LDConfig*)config { - DEBUG_LOG(@"Creating custom event for custom key:%@ and customData:%@", eventKey, customData); - [self addEventDictionary:[[LDEventModel customEventWithKey:eventKey customData:customData userValue:user inlineUser:config.inlineUserInEvents] dictionaryValueUsingConfig:config]]; -} - --(void)createIdentifyEventWithUser:(LDUserModel*)user config:(LDConfig*)config { - DEBUG_LOG(@"Creating identify event for user key:%@", user.key); - [self addEventDictionary:[[LDEventModel identifyEventWithUser:user] dictionaryValueUsingConfig:config]]; -} - --(void)createSummaryEventWithTracker:(LDFlagConfigTracker*)tracker config:(LDConfig*)config { - if (!tracker.hasTrackedEvents) { - DEBUG_LOGX(@"Tracker has no events to report. Discarding summary event."); - return; - } - LDEventModel *summaryEvent = [LDEventModel summaryEventWithTracker:tracker]; - if (summaryEvent == nil) { - DEBUG_LOGX(@"Failed to create summary event. Aborting."); - return; - } - DEBUG_LOGX(@"Creating summary event"); - [self addEventDictionary:[summaryEvent dictionaryValueUsingConfig:config]]; -} - --(void)createDebugEventWithFlagKey:(NSString *)flagKey - reportedFlagValue:(id)reportedFlagValue - flagConfigValue:(LDFlagConfigValue*)flagConfigValue - defaultFlagValue:(id)defaultFlagValue - user:(LDUserModel*)user - config:(LDConfig*)config { - if (![self shouldCreateDebugEventForContext:flagConfigValue.eventTrackingContext lastEventResponseDate:self.lastEventResponseDate]) { - DEBUG_LOG(@"LDDataManager createDebugEventWithFlagKey aborting, debug events are turned off. Discarding debug event %@", flagKey); - return; - } - DEBUG_LOG(@"Creating debug event for feature:%@ with flagConfigValue:%@ and fallback:%@", flagKey, flagConfigValue, defaultFlagValue); - [self addEventDictionary:[[LDEventModel debugEventWithFlagKey:flagKey - reportedFlagValue:reportedFlagValue - flagConfigValue:flagConfigValue - defaultFlagValue:defaultFlagValue - user:user] - dictionaryValueUsingConfig:config]]; -} - --(BOOL)shouldCreateDebugEventForContext:(LDEventTrackingContext*)eventTrackingContext lastEventResponseDate:(NSDate*)lastEventResponseDate { - if (!eventTrackingContext || !eventTrackingContext.debugEventsUntilDate) { return NO; } - if ([lastEventResponseDate isLaterThan:eventTrackingContext.debugEventsUntilDate]) { return NO; } - if ([[NSDate date] isLaterThan:eventTrackingContext.debugEventsUntilDate]) { return NO; } - - return YES; -} - --(void)addEventDictionary:(NSDictionary*)eventDictionary { - if (!eventDictionary || eventDictionary.allKeys.count == 0) { - DEBUG_LOGX(@"LDDataManager addEventDictionary aborting. Event dictionary is missing or empty."); - return; - } - dispatch_async(eventsQueue, ^{ - if([self isAtEventCapacity:self.eventsArray]) { - DEBUG_LOG(@"Events have surpassed capacity. Discarding event %@", eventDictionary[@"key"]); - return; - } - [self.eventsArray addObject:eventDictionary]; - }); -} - --(BOOL)isAtEventCapacity:(NSArray *)currentArray { - LDConfig *ldConfig = [[LDClient sharedInstance] ldConfig]; - return ldConfig.capacity && currentArray && [currentArray count] >= [ldConfig.capacity integerValue]; -} - --(void) deleteProcessedEvents: (NSArray *) processedJsonArray { - // Loop through processedEvents - dispatch_async(eventsQueue, ^{ - NSInteger count = MIN([processedJsonArray count], [self.eventsArray count]); - [self.eventsArray removeObjectsInRange:NSMakeRange(0, count)]; - }); -} - --(void) allEventDictionaries:(void (^)(NSArray *))completion { - dispatch_async(eventsQueue, ^{ - NSMutableArray *eventDictionaries = [self retrieveEventsArray]; - if (eventDictionaries && [eventDictionaries count]) { - completion(eventDictionaries); - } else { - completion(nil); - } - }); -} - --(void)flushEventsDictionary { - [self.eventsArray removeAllObjects]; -} - -- (NSMutableArray *)retrieveEventsArray { - return [[NSMutableArray alloc] initWithArray:self.eventsArray]; -} - -@end diff --git a/Darkly/LDEvent+EventTypes.h b/Darkly/LDEvent+EventTypes.h index 4b87b5e0..16bb392c 100644 --- a/Darkly/LDEvent+EventTypes.h +++ b/Darkly/LDEvent+EventTypes.h @@ -2,7 +2,7 @@ // LDEvent+EventTypes.h // Darkly // -// Created by Mark Pokorny on 2/5/18. +// Created by Mark Pokorny on 2/5/18. +JMJ // Copyright © 2018 LaunchDarkly. All rights reserved. // diff --git a/Darkly/LDPollingManager.h b/Darkly/LDPollingManager.h deleted file mode 100644 index 1b47b127..00000000 --- a/Darkly/LDPollingManager.h +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright © 2015 Catamorphic Co. All rights reserved. -// - -#import - -typedef enum { - POLL_STOPPED = 0, - POLL_PAUSED = 1, - POLL_RUNNING = 2, - POLL_SUSPENDED = 3, - -} PollingState; - -@interface LDPollingManager : NSObject -{ -@protected - PollingState configPollingState; - PollingState eventPollingState; -} - - -+ (id)sharedInstance; -@property (atomic, assign) PollingState configPollingState; -@property (atomic, assign) PollingState eventPollingState; - -@property (strong, nonatomic) dispatch_source_t configTimer; -@property (nonatomic) NSTimeInterval configPollingIntervalMillis; -@property (strong, nonatomic) dispatch_source_t eventTimer; -@property (nonatomic) NSTimeInterval eventPollingIntervalMillis; - -- (void) startConfigPolling; -- (void) pauseConfigPolling; -- (void) suspendConfigPolling; -- (void) resumeConfigPolling; -- (void) stopConfigPolling; -- (PollingState)configPollingState; - -// event polling is passed in from the LDClient object. can be modified... -- (void) startEventPolling; -- (void) pauseEventPolling; -- (void) suspendEventPolling; -- (void) resumeEventPolling; -- (void) stopEventPolling; -- (PollingState)eventPollingState; - -@end diff --git a/Darkly/LDPollingManager.m b/Darkly/LDPollingManager.m deleted file mode 100644 index 00cf8224..00000000 --- a/Darkly/LDPollingManager.m +++ /dev/null @@ -1,297 +0,0 @@ -// -// Copyright © 2015 Catamorphic Co. All rights reserved. -// - - -#import "LDPollingManager.h" -#import "LDClientManager.h" -#import "LDUtil.h" -#import "DarklyConstants.h" - -@implementation LDPollingManager - -@synthesize eventTimer; -@synthesize eventPollingIntervalMillis; -@synthesize eventPollingState; -@synthesize configTimer; -@synthesize configPollingIntervalMillis; -@synthesize configPollingState; - -static id sharedInstance = nil; - -+ (instancetype)sharedInstance -{ - static dispatch_once_t once; - static id sharedInstance; - dispatch_once(&once, ^{ - sharedInstance = [[self alloc] init]; - }); - return sharedInstance; -} - -- (id)init { - if ((self = [super init])) { - self.configPollingState = POLL_STOPPED; - self.eventPollingState = POLL_STOPPED; - - self.configPollingIntervalMillis = kDefaultPollingInterval*kMillisInSecs; - self.eventPollingIntervalMillis = kDefaultFlushInterval*kMillisInSecs; - } - return self; -} - -- (void)dealloc { - [self stopConfigPolling]; - [self stopEventPolling]; - - configPollingState = POLL_STOPPED; - eventPollingState = POLL_STOPPED; -} - -#pragma mark - Config Polling methods - -- (void)startConfigPollTimer -{ - DEBUG_LOGX(@"PollingManager starting initial config polling"); - if ((!self.configTimer) && (self.configPollingIntervalMillis > 0.0)) { - self.configTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); - } - if (self.configTimer) { - configPollingState = POLL_RUNNING; - - dispatch_source_set_event_handler(self.configTimer, ^(void) { - [self configPoll]; - }); - - [self updateConfigPollingTimer]; - dispatch_resume(self.configTimer); - } -} - -- (void)configPoll { - @synchronized (self) { - if (configPollingState != POLL_STOPPED || configPollingState != POLL_SUSPENDED) - { - DEBUG_LOGX(@"PollingManager config interval reached"); - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - if (![[[LDClient sharedInstance] ldConfig] streaming]) { - [clientManager syncWithServerForConfig]; - } - } - } -} - -- (void)updateConfigPollingTimer { - if ((self.configTimer != NULL) && (self.configPollingIntervalMillis > 0.0)) { - uint64_t interval = (uint64_t)(self.configPollingIntervalMillis * NSEC_PER_MSEC); - dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, interval); - dispatch_source_set_timer(self.configTimer, startTime, interval, 1.0); - } -} - -- (void) startConfigPolling { - self.configPollingIntervalMillis = [[LDClient sharedInstance].ldConfig.pollingInterval intValue] * kMillisInSecs; - if (![LDClientManager sharedInstance].isOnline) { - DEBUG_LOGX(@"PollingManager aborting start config polling - client offline"); - return; - } - if (configPollingState == POLL_STOPPED) { - DEBUG_LOGX(@"PollingManager starting config polling"); - [self startConfigPollTimer]; - } -} - -- (void) pauseConfigPolling { - if (configPollingState == POLL_RUNNING) { - DEBUG_LOGX(@"PollingManager pausing config polling"); - dispatch_suspend(self.configTimer); - configPollingState = POLL_PAUSED; - } -} - -- (void) suspendConfigPolling { - if (configPollingState == POLL_RUNNING) { - DEBUG_LOGX(@"PollingManager suspending config polling"); - dispatch_suspend(self.configTimer); - configPollingState = POLL_SUSPENDED; - } -} - -- (void) resumeConfigPolling{ - if (configPollingState != POLL_PAUSED && configPollingState != POLL_SUSPENDED) { - DEBUG_LOGX(@"PollingManager aborting resume config polling - poll not paused or suspended"); - return; - } - - if (![LDClientManager sharedInstance].isOnline) { - DEBUG_LOGX(@"PollingManager aborting resume config polling - client offline"); - return; - } - - DEBUG_LOGX(@"PollingManager resuming config polling"); - dispatch_resume(self.configTimer); //If the configTimer would have fired while paused/suspended, it triggers a flag request - @synchronized (self) { - configPollingState = POLL_RUNNING; - } -} - -- (void)stopConfigPolling { - DEBUG_LOGX(@"PollingManager stopping config polling"); - if (self.configTimer) { - dispatch_source_cancel(self.configTimer); - - if (configPollingState == POLL_PAUSED || configPollingState == POLL_SUSPENDED) - { - dispatch_resume(self.configTimer); - } - -#if !OS_OBJECT_USE_OBJC - dispatch_release(self.pollingTimer); -#endif - self.configTimer = NULL; - configPollingState = POLL_STOPPED; - } -} - -#pragma mark - Event Polling methods -//Setter method -- (void) setEventPollingIntervalMillis:(NSTimeInterval)eTimerPollingInterval { - eventPollingIntervalMillis = [self calculateEventPollingIntervalMillis:eTimerPollingInterval]; - if (eventPollingState != POLL_STOPPED && eventPollingState != POLL_SUSPENDED) { - // pause the event polling interval - DEBUG_LOGX(@"Pausing event Polling"); - [self pauseEventPolling]; - - if (eTimerPollingInterval == kMinimumFlushIntervalMillis && [[[LDClient sharedInstance] ldConfig] debugEnabled] == YES) { - [self eventPoll]; - } - - [self updateEventPollingTimer]; - DEBUG_LOGX(@"updated event Polling"); - [self resumeEventPolling]; - DEBUG_LOGX(@"resuming event Polling"); - } -} - --(NSTimeInterval)calculateEventPollingIntervalMillis:(NSTimeInterval)eTimerPollingInterval { - LDConfig *config = [[LDClient sharedInstance] ldConfig]; - if (![config streaming] && [[config flushInterval] intValue] == kDefaultFlushInterval) { - return [config.pollingInterval intValue] * kMillisInSecs; - } - if (eTimerPollingInterval <= kMinimumFlushIntervalMillis) { - return kDefaultFlushInterval*kMillisInSecs; - } else { - return eTimerPollingInterval; - } -} - - -- (void)startEventPollTimer -{ - DEBUG_LOGX(@"PollingManager starting initial event polling"); - if ((!self.eventTimer) && (self.eventPollingIntervalMillis > 0.0)) { - self.eventTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); - } - if (self.eventTimer) { - eventPollingState = POLL_RUNNING; - - dispatch_source_set_event_handler(self.eventTimer, ^(void) { - [self eventPoll]; - }); - - [self updateEventPollingTimer]; - dispatch_resume(self.eventTimer); - } -} - -- (void)eventPoll { - @synchronized (self) { - if (eventPollingState != POLL_STOPPED || eventPollingState != POLL_SUSPENDED) - { - DEBUG_LOGX(@"PollingManager event interval reached"); - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager syncWithServerForEvents]; - } - } -} - -- (void)updateEventPollingTimer { - if ((self.eventTimer != NULL) && (self.eventPollingIntervalMillis > 0.0)) { - uint64_t interval = (uint64_t)(self.eventPollingIntervalMillis * NSEC_PER_MSEC); - dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, interval); - dispatch_source_set_timer(self.eventTimer, startTime, interval, 1.0); - } -} - -- (void) startEventPolling { - self.eventPollingIntervalMillis = [[[LDClient sharedInstance] ldConfig].flushInterval intValue] * kMillisInSecs; - if (![LDClientManager sharedInstance].isOnline) { - DEBUG_LOGX(@"PollingManager aborting start event polling - client offline"); - return; - } - if (eventPollingState == POLL_STOPPED) { - DEBUG_LOG(@"PollingManager starting event polling with pollingInterval=%f", self.eventPollingIntervalMillis); - [self startEventPollTimer]; - } -} - -- (void) pauseEventPolling { - if (eventPollingState == POLL_RUNNING) { - DEBUG_LOGX(@"PollingManager pausing event polling"); - dispatch_suspend(self.eventTimer); - eventPollingState = POLL_PAUSED; - } -} - -- (void) suspendEventPolling { - if (eventPollingState == POLL_RUNNING) { - DEBUG_LOGX(@"PollingManager suspending event polling"); - dispatch_suspend(self.eventTimer); - eventPollingState = POLL_SUSPENDED; - } -} - -- (void) resumeEventPolling{ - if (eventPollingState != POLL_PAUSED && eventPollingState != POLL_SUSPENDED) { - DEBUG_LOGX(@"PollingManager aborting resume event polling - poll is neither paused nor started"); - return; - } - - if (![LDClientManager sharedInstance].isOnline) { - DEBUG_LOGX(@"PollingManager aborting resume event polling - client offline"); - return; - } - - DEBUG_LOGX(@"PollingManager resuming event polling"); - BOOL checkEvent = eventPollingState == POLL_SUSPENDED ? YES : NO; - dispatch_resume(self.eventTimer); - @synchronized (self) { - eventPollingState = POLL_RUNNING; - } - if (checkEvent) { - [self eventPoll]; - } -} - -- (void)stopEventPolling { - DEBUG_LOGX(@"PollingManager stopping event polling"); - if (self.eventTimer) { - dispatch_source_cancel(self.eventTimer); - - if (eventPollingState == POLL_PAUSED || eventPollingState == POLL_SUSPENDED) - { - dispatch_resume(self.eventTimer); - } - -#if !OS_OBJECT_USE_OBJC - dispatch_release(self.pollingTimer); -#endif - self.eventTimer = NULL; - eventPollingState = POLL_STOPPED; - } -} - - -@end diff --git a/Darkly/LDRequestManager.h b/Darkly/LDRequestManager.h deleted file mode 100644 index 1bd03dd3..00000000 --- a/Darkly/LDRequestManager.h +++ /dev/null @@ -1,30 +0,0 @@ -// -// Copyright © 2015 Catamorphic Co. All rights reserved. -// - -#import "LDUserModel.h" - -@protocol RequestManagerDelegate - --(void)processedEvents:(BOOL)success jsonEventArray:(nonnull NSArray*)jsonEventArray responseDate:(nullable NSDate*)responseDate; --(void)processedConfig:(BOOL)success jsonConfigDictionary:(nonnull NSDictionary*)jsonConfigDictionary; - -@end - -extern NSString * _Nonnull const kHeaderMobileKey; - -@interface LDRequestManager : NSObject - -@property (nonnull, nonatomic, copy) NSString* mobileKey; -@property (nonnull, nonatomic, copy) NSString* baseUrl; -@property (nonnull, nonatomic, copy) NSString* eventsUrl; -@property (nonatomic, assign) NSTimeInterval connectionTimeout; -@property (nullable, nonatomic, weak) id delegate; - -+(nonnull LDRequestManager*)sharedInstance; - --(void)performFeatureFlagRequest:(nullable LDUserModel*)user; - --(void)performEventRequest:(nullable NSArray*)eventDictionaries; - -@end diff --git a/Darkly/NSDictionary+JSON.h b/Darkly/NSDictionary+JSON.h deleted file mode 100644 index 35108695..00000000 --- a/Darkly/NSDictionary+JSON.h +++ /dev/null @@ -1,9 +0,0 @@ -// -// Copyright © 2015 Catamorphic Co. All rights reserved. -// - -#import - -@interface NSDictionary (BVJSONString) --(nullable NSString*) jsonString; -@end diff --git a/Darkly/NSDictionary+JSON.m b/Darkly/NSDictionary+JSON.m deleted file mode 100644 index 9435d3d1..00000000 --- a/Darkly/NSDictionary+JSON.m +++ /dev/null @@ -1,15 +0,0 @@ -// -// Copyright © 2015 Catamorphic Co. All rights reserved. -// - -#import "NSDictionary+JSON.h" - -@implementation NSDictionary (BVJSONString) - --(NSString*) jsonString { - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self options:0 error:nil]; - if (!jsonData) { return nil; } - - return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; -} -@end diff --git a/Darkly/NSDictionary+LaunchDarkly.h b/Darkly/NSDictionary+LaunchDarkly.h new file mode 100644 index 00000000..bcd4abfc --- /dev/null +++ b/Darkly/NSDictionary+LaunchDarkly.h @@ -0,0 +1,10 @@ +// +// Copyright © 2015 Catamorphic Co. All rights reserved. +// + +#import + +@interface NSDictionary (LaunchDarkly) +-(nullable NSString*) jsonString; +-(nonnull NSDictionary*)compactMapUsingBlock:(nullable id (^)(_Nonnull id originalValue))mappingBlock; +@end diff --git a/Darkly/NSDictionary+LaunchDarkly.m b/Darkly/NSDictionary+LaunchDarkly.m new file mode 100644 index 00000000..82c1acef --- /dev/null +++ b/Darkly/NSDictionary+LaunchDarkly.m @@ -0,0 +1,24 @@ +// +// Copyright © 2015 Catamorphic Co. All rights reserved. +// + +#import "NSDictionary+LaunchDarkly.h" + +@implementation NSDictionary (LaunchDarkly) +-(NSString*) jsonString { + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self options:0 error:nil]; + if (!jsonData) { return nil; } + + return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; +} + +-(NSDictionary*)compactMapUsingBlock:(id (^)(id originalValue))mappingBlock { + NSMutableDictionary *mapped = [NSMutableDictionary dictionaryWithCapacity:self.allKeys.count]; + for (id key in self.allKeys) { + id mappedValue = mappingBlock(self[key]); + if (mappedValue == nil) { continue; } + mapped[key] = mappedValue; + } + return [NSDictionary dictionaryWithDictionary:mapped]; +} +@end diff --git a/Darkly/NSHTTPURLResponse+LaunchDarkly.h b/Darkly/NSHTTPURLResponse+LaunchDarkly.h index cb53db69..e93756f9 100644 --- a/Darkly/NSHTTPURLResponse+LaunchDarkly.h +++ b/Darkly/NSHTTPURLResponse+LaunchDarkly.h @@ -9,6 +9,10 @@ #import @interface NSHTTPURLResponse(LaunchDarkly) +@property (assign, nonatomic, readonly) BOOL isOk; +@property (assign, nonatomic, readonly) BOOL isNotModified; +@property (copy, nonatomic, readonly) NSString *etag; + -(BOOL)isUnauthorizedHTTPResponse; -(NSDate*)headerDate; @end diff --git a/Darkly/NSHTTPURLResponse+LaunchDarkly.m b/Darkly/NSHTTPURLResponse+LaunchDarkly.m index 3321096c..bcc484f3 100644 --- a/Darkly/NSHTTPURLResponse+LaunchDarkly.m +++ b/Darkly/NSHTTPURLResponse+LaunchDarkly.m @@ -11,14 +11,29 @@ #import "NSDateFormatter+JsonHeader.h" NSString * const kHeaderKeyDate = @"Date"; +NSString * const kFlagResponseHeaderEtag = @"etag"; @implementation NSHTTPURLResponse(LaunchDarkly) + +-(BOOL)isOk { + return self.statusCode == kHTTPStatusCodeOk; +} + +-(BOOL)isNotModified { + return self.statusCode == kHTTPStatusCodeNotModified; +} + -(BOOL)isUnauthorizedHTTPResponse { return self.statusCode == kHTTPStatusCodeUnauthorized; } + -(NSDate*)headerDate { NSString* headerDateString = [self allHeaderFields][kHeaderKeyDate]; if (headerDateString.length == 0) { return nil; } return [[NSDateFormatter jsonHeaderDateFormatter] dateFromString:headerDateString]; } + +-(NSString*)etag { + return self.allHeaderFields[kFlagResponseHeaderEtag]; +} @end diff --git a/Darkly/NSNumber+LaunchDarkly.h b/Darkly/NSNumber+LaunchDarkly.h index c5c24cd2..cb0a0c11 100644 --- a/Darkly/NSNumber+LaunchDarkly.h +++ b/Darkly/NSNumber+LaunchDarkly.h @@ -11,4 +11,5 @@ @interface NSNumber(LaunchDarkly) -(LDMillisecond)ldMillisecondValue; +-(uint64_t)nanoSecondValue; @end diff --git a/Darkly/NSNumber+LaunchDarkly.m b/Darkly/NSNumber+LaunchDarkly.m index 56938470..1f12a210 100644 --- a/Darkly/NSNumber+LaunchDarkly.m +++ b/Darkly/NSNumber+LaunchDarkly.m @@ -12,4 +12,8 @@ @implementation NSNumber(LaunchDarkly) -(LDMillisecond)ldMillisecondValue { return [self longLongValue]; } + +-(uint64_t)nanoSecondValue { + return self.integerValue * NSEC_PER_SEC; +} @end diff --git a/Darkly/NSString+LaunchDarkly.h b/Darkly/NSString+LaunchDarkly.h new file mode 100644 index 00000000..4d176d78 --- /dev/null +++ b/Darkly/NSString+LaunchDarkly.h @@ -0,0 +1,18 @@ +// +// NSString+LaunchDarkly.h +// Darkly +// +// Created by Mark Pokorny on 11/1/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSString (LaunchDarkly) ++(instancetype)stringWithBool:(BOOL)boolValue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Darkly/NSString+LaunchDarkly.m b/Darkly/NSString+LaunchDarkly.m new file mode 100644 index 00000000..f4f6c99d --- /dev/null +++ b/Darkly/NSString+LaunchDarkly.m @@ -0,0 +1,15 @@ +// +// NSString+LaunchDarkly.m +// Darkly +// +// Created by Mark Pokorny on 11/1/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "NSString+LaunchDarkly.h" + +@implementation NSString (LaunchDarkly) ++(instancetype)stringWithBool:(BOOL)boolValue { + return boolValue ? @"YES" : @"NO"; +} +@end diff --git a/Darkly/NSThread+MainExecutable.h b/Darkly/NSThread+MainExecutable.h index c66a14e2..0f18ad00 100644 --- a/Darkly/NSThread+MainExecutable.h +++ b/Darkly/NSThread+MainExecutable.h @@ -9,5 +9,5 @@ #import @interface NSThread (MainExecutable) -+ (void)performOnMainThread:(void(^)(void))executionBlock; ++ (void)performOnMainThread:(void(^)(void))executionBlock waitUntilDone:(BOOL)wait; @end diff --git a/Darkly/NSThread+MainExecutable.m b/Darkly/NSThread+MainExecutable.m index 01c7e949..7dc9b5e3 100644 --- a/Darkly/NSThread+MainExecutable.m +++ b/Darkly/NSThread+MainExecutable.m @@ -9,10 +9,10 @@ #import "NSThread+MainExecutable.h" @implementation NSThread (MainExecutable) -+ (void)performOnMainThread:(void(^)(void))executionBlock { ++ (void)performOnMainThread:(void(^)(void))executionBlock waitUntilDone:(BOOL)wait { if (!executionBlock) { return; } if (![NSThread isMainThread]) { - [self performSelectorOnMainThread:@selector(performOnMainThread:) withObject:executionBlock waitUntilDone:YES]; + [self performSelectorOnMainThread:@selector(performOnMainThread:waitUntilDone:) withObject:executionBlock waitUntilDone:wait]; return; } executionBlock(); diff --git a/Darkly/NSURLResponse+LaunchDarkly.h b/Darkly/NSURLResponse+LaunchDarkly.h index 77a14e4d..9c18debb 100644 --- a/Darkly/NSURLResponse+LaunchDarkly.h +++ b/Darkly/NSURLResponse+LaunchDarkly.h @@ -8,8 +8,11 @@ #import -/// Determines if this response is an unauthorized HTTP response. By default NO, but can be overridden by subclasses that can detected unuathorized response. @interface NSURLResponse(LaunchDarkly) +@property (assign, nonatomic, readonly) BOOL isOk; +@property (assign, nonatomic, readonly) BOOL isNotModified; +@property (copy, nonatomic, readonly) NSString *etag; + -(BOOL)isUnauthorizedHTTPResponse; -(NSDate*)headerDate; @end diff --git a/Darkly/NSURLResponse+LaunchDarkly.m b/Darkly/NSURLResponse+LaunchDarkly.m index 528367d5..417043c6 100644 --- a/Darkly/NSURLResponse+LaunchDarkly.m +++ b/Darkly/NSURLResponse+LaunchDarkly.m @@ -10,6 +10,15 @@ #import "NSHTTPURLResponse+LaunchDarkly.h" @implementation NSURLResponse(LaunchDarkly) + +-(BOOL)isOk { + return NO; +} + +-(BOOL)isNotModified { + return NO; +} + -(BOOL)isUnauthorizedHTTPResponse { return NO; } @@ -17,4 +26,9 @@ -(BOOL)isUnauthorizedHTTPResponse { -(NSDate*)headerDate { return nil; } + +-(NSString*)etag { + return nil; +} + @end diff --git a/Darkly/NSURLSession+LaunchDarkly.h b/Darkly/NSURLSession+LaunchDarkly.h new file mode 100644 index 00000000..68bf9d2c --- /dev/null +++ b/Darkly/NSURLSession+LaunchDarkly.h @@ -0,0 +1,22 @@ +// +// NSURLSession+LaunchDarkly.h +// Darkly +// +// Created by Mark Pokorny on 11/19/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import + +@class LDConfig; + +NS_ASSUME_NONNULL_BEGIN + +@interface NSURLSession (LaunchDarkly) + ++(void)setSharedLDSessionForConfig:(LDConfig*)config; ++(NSURLSession*)sharedLDSession; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Darkly/NSURLSession+LaunchDarkly.m b/Darkly/NSURLSession+LaunchDarkly.m new file mode 100644 index 00000000..d2aa3d23 --- /dev/null +++ b/Darkly/NSURLSession+LaunchDarkly.m @@ -0,0 +1,39 @@ +// +// NSURLSession+LaunchDarkly.m +// Darkly +// +// Created by Mark Pokorny on 11/19/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "NSURLSession+LaunchDarkly.h" +#import "DarklyConstants.h" +#import "LDConfig.h" +#import "LDURLCache.h" + +@implementation NSURLSession (LaunchDarkly) +static NSURLSession *sharedNSURLSession = nil; + ++(NSURLSession*)sharedLDSession { + return sharedNSURLSession; +} + ++(void)setSharedLDSessionForConfig:(LDConfig*)config { + if (sharedNSURLSession != nil && [sharedNSURLSession sessionMatchesConfig:config]) { + return; + } + NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLCache *nsUrlCache = [[NSURLCache alloc] initWithMemoryCapacity:kNSURLCacheMemoryCapacity diskCapacity:kNSURLCacheDiskCapacity diskPath:nil]; + sessionConfig.URLCache = [LDURLCache urlCacheForConfig:config usingCache:nsUrlCache]; + sharedNSURLSession = [NSURLSession sessionWithConfiguration:sessionConfig]; +} + +-(BOOL)sessionMatchesConfig:(LDConfig*)config { + return ([LDURLCache shouldUseLDURLCacheForConfig:config] && self.hasLDURLCache) || (![LDURLCache shouldUseLDURLCacheForConfig:config] && !self.hasLDURLCache); +} + +-(BOOL)hasLDURLCache { + return [self.configuration.URLCache isKindOfClass:[LDURLCache class]]; +} + +@end diff --git a/Darkly/Services/LDDataManager.h b/Darkly/Services/LDDataManager.h new file mode 100644 index 00000000..e2e8632d --- /dev/null +++ b/Darkly/Services/LDDataManager.h @@ -0,0 +1,49 @@ +// +// Copyright © 2015 Catamorphic Co. All rights reserved. +// + +#import +#import "LDConfig.h" + +@class LDUserModel; +@class LDFlagConfigModel; +@class LDFlagConfigValue; +@class LDFlagConfigTracker; + +@interface LDDataManager : NSObject +@property (nonnull, nonatomic, copy, readonly) NSString *mobileKey; +@property (nonnull, nonatomic, strong, readonly) LDConfig *config; +@property (nullable, nonatomic, strong) NSDate *lastEventResponseDate; + ++(nullable instancetype)dataManagerWithMobileKey:(nonnull NSString*)mobileKey config:(nonnull LDConfig*)config; +-(nullable instancetype)initWithMobileKey:(nonnull NSString*)mobileKey config:(nonnull LDConfig*)config; + +//User Store ++(void)convertToEnvironmentBasedCacheForUser:(LDUserModel*)user config:(LDConfig*)config; +-(void)saveUser:(nonnull LDUserModel*)user; +-(nullable LDUserModel*)findUserWithKey:(nonnull NSString*)key; +-(nullable LDFlagConfigModel*)retrieveFlagConfigForUser:(nonnull LDUserModel*)user; + +//Events +-(void)allEventDictionaries:(void (^)(NSArray * _Nullable eventDictionaries))completion; +-(void)recordFlagEvaluationEventsWithFlagKey:(nonnull NSString*)flagKey + reportedFlagValue:(nonnull id)reportedFlagValue + flagConfigValue:(nullable LDFlagConfigValue*)flagConfigValue + defaultFlagValue:(nonnull id)defaultFlagValue + user:(nonnull LDUserModel*)user; +-(void)recordFeatureEventWithFlagKey:(nonnull NSString*)flagKey + reportedFlagValue:(nonnull id)reportedFlagValue + flagConfigValue:(nullable LDFlagConfigValue*)flagConfigValue + defaultFlagValue:(nonnull id)defaultFlagValue + user:(nonnull LDUserModel*)user; +-(void)recordCustomEventWithKey:(nonnull NSString*)eventKey customData:(nullable NSDictionary*)customData user:(nonnull LDUserModel*)user; +-(void)recordIdentifyEventWithUser:(nonnull LDUserModel*)user; +-(void)recordSummaryEventWithTracker:(nullable LDFlagConfigTracker*)tracker; +-(void)recordDebugEventWithFlagKey:(nonnull NSString*)flagKey + reportedFlagValue:(nonnull id)reportedFlagValue + flagConfigValue:(nullable LDFlagConfigValue*)flagConfigValue + defaultFlagValue:(nonnull id)defaultFlagValue + user:(nonnull LDUserModel*)user; +-(void)deleteProcessedEvents:(nullable NSArray*)processedJsonArray; + +@end diff --git a/Darkly/Services/LDDataManager.m b/Darkly/Services/LDDataManager.m new file mode 100644 index 00000000..48a78e3c --- /dev/null +++ b/Darkly/Services/LDDataManager.m @@ -0,0 +1,455 @@ +// +// Copyright © 2015 Catamorphic Co. All rights reserved. +// + + +#import "LDDataManager.h" +#import "LDUserModel.h" +#import "LDEventModel.h" +#import "LDUtil.h" +#import "LDFlagConfigModel.h" +#import "LDFlagConfigValue.h" +#import "LDEventTrackingContext.h" +#import "LDFlagConfigTracker.h" +#import "LDUserEnvironment.h" +#import "NSDate+ReferencedDate.h" +#import "NSDictionary+LaunchDarkly.h" +#import "NSThread+MainExecutable.h" +#import "LDConfig+LaunchDarkly.h" + +int const kUserCacheSize = 5; +NSString * const kUserDefaultsKeyUserEnvironments = @"com.launchdarkly.dataManager.userEnvironments"; + +@interface LDDataManager() +@property (nonatomic, copy) NSString *mobileKey; +@property (nonatomic, strong) LDConfig *config; +@property (atomic, strong) NSMutableArray *eventsArray; +@property (nonatomic, strong) dispatch_queue_t eventsQueue; +@property (nonatomic, strong) dispatch_queue_t saveUserQueue; +@property (nonatomic, strong, readonly, class) NSUserDefaults *userSaveSyncKey; +@end + +@implementation LDDataManager ++(instancetype)dataManagerWithMobileKey:(NSString*)mobileKey config:(LDConfig*)config { + return [[LDDataManager alloc] initWithMobileKey:mobileKey config:config]; +} + +-(instancetype)initWithMobileKey:(NSString*)mobileKey config:(LDConfig*)config { + if (mobileKey.length == 0 || config == nil) { + return nil; + } + if (!(self = [super init])) { + return nil; + } + + self.mobileKey = mobileKey; + self.config = config; + self.eventsArray = [[NSMutableArray alloc] init]; + self.eventsQueue = dispatch_queue_create([[NSString stringWithFormat:@"com.launchdarkly.eventQueue.%@", mobileKey] UTF8String], DISPATCH_QUEUE_SERIAL); + self.saveUserQueue = dispatch_queue_create([[NSString stringWithFormat:@"com.launchdarkly.dataManager.saveUserQueue.%@", mobileKey] UTF8String], DISPATCH_QUEUE_SERIAL); + + return self; +} + +#pragma mark - users + ++(void)convertToEnvironmentBasedCacheForUser:(LDUserModel*)user config:(LDConfig*)config { + if (user == nil || config == nil) { + NSString *reason = @""; + if (user == nil) { + reason = [reason stringByAppendingString:@"user is missing."]; + } + if (config == nil) { + reason = [reason stringByAppendingString:[NSString stringWithFormat:@"%@config is missing.", reason.length > 0 ? @" " : @""]]; + } + DEBUG_LOG(@"LDDataManager cannot convert to environment based cache. %@", reason); + return; + } + @synchronized (LDDataManager.userSaveSyncKey) { + // userEnvironments is a dictionary - the new environment based store. Each entry contains all the mobile key feature flags for a single user. + NSMutableDictionary *userEnvironments = [NSMutableDictionary dictionaryWithDictionary:[LDDataManager retrieveUserEnvironments]]; + LDUserEnvironment *userEnvironment = userEnvironments[user.key]; + DEBUG_LOG(@"LDDataManager found cached user environments for:%@",[userEnvironments.allKeys componentsJoinedByString:@", "]); + // userModels is a dictionary - the old user store for a single environment + NSMutableDictionary *userModels = [NSMutableDictionary dictionaryWithDictionary:[LDDataManager retrieveStoredUserModels]]; + LDUserModel *userModel = userModels[user.key]; + DEBUG_LOG(@"LDDataManager found cached user models for:%@",[userModels.allKeys componentsJoinedByString:@", "]); + if (userEnvironment == nil) { + //no environment based store for the user + if (userModel == nil) { + DEBUG_LOG(@"LDDataManager did not find cached user:%@. Nothing to convert.", user.key); + return; //No stored userModel (old store), so there's nothing to convert + } + //There is an old user in the store, convert it by copying it into all the environments. We know that's not right for all but one, but since we don't know which this is the best we can do. As soon as the SDK gets responses from the server, these should all be replaced. + NSMutableDictionary *usersInEnvironments = [NSMutableDictionary dictionaryWithCapacity:config.mobileKeys.count]; + for (NSString *mobileKey in config.mobileKeys) { + usersInEnvironments[mobileKey] = [userModel copy]; + } + userEnvironments[user.key] = [LDUserEnvironment userEnvironmentForUserWithKey:user.key environments:usersInEnvironments]; + [LDDataManager saveUserEnvironments:[userEnvironments copy]]; + DEBUG_LOG(@"LDDataManager converted cached user:%@ to environment-based cache.", user.key); + return; + } + //Found a userEnvironment for this user, safe to delete the userModel store for this user + if (userModel == nil) { + DEBUG_LOG(@"LDDataManager found environment-based cache for user:%@. No cached user model found.", user.key); + return; //No stored userModel, nothing to delete + } + [userModels removeObjectForKey:user.key]; + [LDDataManager storeUserModels:[userModels copy]]; + DEBUG_LOG(@"LDDataManager found environment-based cache for user:%@. Removed cached user model.", user.key); + } +} + +-(void)purgeOldUserEnvironment:(NSMutableDictionary *)dictionary { + if (dictionary == nil || dictionary.count < kUserCacheSize) { + return; + } + NSArray *sortedKeys = [dictionary keysSortedByValueUsingComparator: ^(LDUserEnvironment *userEnvironment1, LDUserEnvironment *userEnvironment2) { + //Oldest to Newest + return [userEnvironment1.lastUpdated compare:userEnvironment2.lastUpdated]; + }]; + + [dictionary removeObjectForKey:sortedKeys.firstObject]; +} + +#pragma mark save ++(NSUserDefaults*)userSaveSyncKey { + return [NSUserDefaults standardUserDefaults]; +} + +-(void)saveUser:(LDUserModel*)user { + [self saveEnvironmentForUser:user completion:nil]; +} + +-(void)saveEnvironmentForUser:(LDUserModel*)user completion:(void (^)(void))completion { + if (user == nil) { + DEBUG_LOGX(@"LDDataManager unable to save environment for user. User is missing."); + if (completion != nil) { + completion(); + } + return; + } + LDUserModel *userCopy = [user copy]; //Preserve the user while waiting to save on the saveQueue + __weak typeof(self) weakSelf = self; + dispatch_async(self.saveUserQueue, ^{ + @synchronized (LDDataManager.userSaveSyncKey) { + __strong typeof(weakSelf) strongSelf = weakSelf; + NSMutableDictionary *storedUserEnvironments = [NSMutableDictionary dictionaryWithDictionary:[strongSelf retrieveUserEnvironments]]; + LDUserEnvironment *userEnvironment = storedUserEnvironments[userCopy.key]; + if (userEnvironment == nil) { + //UserEnvironment wasn't found, create one + userEnvironment = [LDUserEnvironment userEnvironmentForUserWithKey:userCopy.key environments:nil]; + [self purgeOldUserEnvironment:storedUserEnvironments]; + } + if (userEnvironment == nil) { + //Couldn't find or create a UserEnvironment - bailout + //This is totally defensive. The only way this would ever happen is if the system couldn't allocate memory to a new userEnvironment. + DEBUG_LOG(@"LDDataManager unable to save user:%@ for mobileKey:%@", userCopy.key, strongSelf.mobileKey); + if (completion != nil) { + completion(); + } + return; + } + + [userEnvironment setUser:userCopy mobileKey:strongSelf.mobileKey]; + storedUserEnvironments[userCopy.key] = userEnvironment; + [strongSelf saveUserEnvironments:storedUserEnvironments]; + + if (completion == nil) { + return; + } + completion(); + } + }); +} + +#pragma mark Find / Restore +-(LDUserModel*)findUserWithKey:(NSString*)key { + return [self findEnvironmentForUserWithKey:key]; +} + +-(LDFlagConfigModel*)retrieveFlagConfigForUser:(LDUserModel*)user { + if (user == nil) { + return [[LDFlagConfigModel alloc] init]; + } + LDUserModel *restoredUser = [self findUserWithKey:user.key]; + if (restoredUser == nil) { + return user.flagConfig ?: [[LDFlagConfigModel alloc] init]; + } + return restoredUser.flagConfig; +} + +-(LDUserModel*)findEnvironmentForUserWithKey:(NSString*)userKey { + if (userKey.length == 0) { + DEBUG_LOGX(@"LDDataManager unable to find user. Key is missing or empty."); + return nil; + } + NSDictionary *storedUserEnvironments = [self retrieveUserEnvironments]; + LDUserEnvironment *storedUserEnvironment = storedUserEnvironments[userKey]; + return [storedUserEnvironment userForMobileKey:self.mobileKey]; +} + +#pragma mark UserEnvironment + +-(void)saveUserEnvironments:(NSDictionary*)userEnvironments { + [LDDataManager saveUserEnvironments:userEnvironments]; +} + ++(void)saveUserEnvironments:(NSDictionary*)userEnvironments { + NSDictionary *userEnvironmentDictionaries = [userEnvironments compactMapUsingBlock:^id(id _Nonnull originalValue) { + if (![originalValue isKindOfClass:[LDUserEnvironment class]]) { + return nil; + } + LDUserEnvironment *userEnvironment = originalValue; + return [userEnvironment dictionaryValue]; + }]; + [[NSUserDefaults standardUserDefaults] setObject:userEnvironmentDictionaries forKey:kUserDefaultsKeyUserEnvironments]; +} + +-(NSDictionary*)retrieveUserEnvironments { + return [LDDataManager retrieveUserEnvironments]; +} + ++(NSDictionary*)retrieveUserEnvironments { + NSDictionary *userEnvironmentDictionaries = [[NSUserDefaults standardUserDefaults] objectForKey:kUserDefaultsKeyUserEnvironments]; + return [userEnvironmentDictionaries compactMapUsingBlock:^id(id _Nonnull originalValue) { + if (![originalValue isKindOfClass:[NSDictionary class]]) { + return nil; + } + NSDictionary *userEnvironmentDictionary = originalValue; + return [[LDUserEnvironment alloc] initWithDictionary:userEnvironmentDictionary]; + }]; +} + +#pragma mark Deprecated +-(void)saveUser:(LDUserModel *)user asDict:(BOOL)asDict completion:(void (^)(void))completion { + LDUserModel *userCopy = [user copy]; //Preserve the user while waiting to save on the saveQueue + dispatch_async(self.saveUserQueue, ^{ + @synchronized (LDDataManager.userSaveSyncKey) { + NSMutableDictionary * storedUserModels = [self retrieveStoredUserModels]; + if (storedUserModels) { + LDUserModel * storedUser = storedUserModels[userCopy.key]; + if (storedUser != nil) { + // User is found + userCopy.updatedAt = [NSDate date]; + storedUserModels[userCopy.key] = userCopy; + } else { + // User is not found so need to create and purge old users + userCopy.updatedAt = [NSDate date]; + storedUserModels[userCopy.key] = userCopy; + } + } else { + // No Dictionary exists so create + storedUserModels = [[NSMutableDictionary alloc] init]; + userCopy.updatedAt = [NSDate date]; + storedUserModels[userCopy.key] = userCopy; + } + storedUserModels[userCopy.key] = userCopy; + if (asDict) { + [self storeUserModels:storedUserModels]; + } + else{ + [self deprecatedStoreUserDictionary:storedUserModels]; + } + DEBUG_LOG(@"LDDataManager saved user:%@ %@", userCopy.key, userCopy); + if (completion != nil) { + [NSThread performOnMainThread:^{ + completion(); + } waitUntilDone:NO]; + } + } + }); +} + +-(LDUserModel*)findUserModelWithKey:(NSString*)key { + NSDictionary *storedUserModels = [self retrieveStoredUserModels]; + if (storedUserModels.count == 0) { + return nil; + } + + LDUserModel *foundUserModel = storedUserModels[key]; + if (foundUserModel == nil) { + return nil; + } + + DEBUG_LOG(@"LDDataManager found cached user:%@ %@", foundUserModel.key, foundUserModel); + foundUserModel.updatedAt = [NSDate date]; + return foundUserModel; +} + +- (void)deprecatedStoreUserDictionary:(NSDictionary *)userDictionary { + NSMutableDictionary *archiveDictionary = [[NSMutableDictionary alloc] init]; + for (NSString *key in userDictionary) { + NSData *userEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:(LDUserModel *)[userDictionary objectForKey:key]]; + [archiveDictionary setObject:userEncodedObject forKey:key]; + } + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setObject:archiveDictionary forKey:kUserDictionaryStorageKey]; + [defaults synchronize]; +} + +-(void)storeUserModels:(NSDictionary *)userModels { + [LDDataManager storeUserModels:userModels]; +} + ++(void)storeUserModels:(NSDictionary *)userModels { + NSDictionary *userModelDictionaries = [userModels compactMapUsingBlock:^id(id originalValue) { + if (originalValue == nil || ![originalValue isKindOfClass:[LDUserModel class]]) { + return nil; + } + LDUserModel *user = originalValue; + return [user dictionaryValueWithPrivateAttributesAndFlagConfig:YES]; + }]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setObject:userModelDictionaries forKey:kUserDictionaryStorageKey]; + [defaults synchronize]; +} + +-(nonnull NSMutableDictionary*)retrieveStoredUserModels { + return [LDDataManager retrieveStoredUserModels]; +} + ++(nonnull NSMutableDictionary*)retrieveStoredUserModels { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSDictionary *userModelDictionaries = [defaults objectForKey:kUserDictionaryStorageKey]; + NSMutableDictionary *userModels = [NSMutableDictionary dictionaryWithDictionary:[userModelDictionaries compactMapUsingBlock:^id(id originalValue) { + if (originalValue == nil) { + return nil; + } + if ([originalValue isKindOfClass:[NSData class]]) { + return [NSKeyedUnarchiver unarchiveObjectWithData:originalValue]; + } + if ([originalValue isKindOfClass:[NSDictionary class]]) { + return [[LDUserModel alloc] initWithDictionary:originalValue]; + } + return nil; + }]]; + return userModels; +} + +#pragma mark Test Support ++(void)removeStoredUsers { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:kUserDefaultsKeyUserEnvironments]; + [[NSUserDefaults standardUserDefaults] removeObjectForKey:kUserDictionaryStorageKey]; +} + +#pragma mark - events + +-(void)recordFlagEvaluationEventsWithFlagKey:(NSString*)flagKey + reportedFlagValue:(id)reportedFlagValue + flagConfigValue:(LDFlagConfigValue*)flagConfigValue + defaultFlagValue:(id)defaultFlagValue + user:(LDUserModel*)user { + [self recordFeatureEventWithFlagKey:flagKey reportedFlagValue:reportedFlagValue flagConfigValue:flagConfigValue defaultFlagValue:defaultFlagValue user:user]; + [self recordDebugEventWithFlagKey:flagKey reportedFlagValue:reportedFlagValue flagConfigValue:flagConfigValue defaultFlagValue:defaultFlagValue user:user]; + [user.flagConfigTracker logRequestForFlagKey:flagKey reportedFlagValue:reportedFlagValue flagConfigValue:flagConfigValue defaultValue:defaultFlagValue]; +} + +-(void)recordFeatureEventWithFlagKey:(NSString*)flagKey + reportedFlagValue:(id)reportedFlagValue + flagConfigValue:(LDFlagConfigValue*)flagConfigValue + defaultFlagValue:(id)defaultFlagValue + user:(LDUserModel*)user { + if (!flagConfigValue.eventTrackingContext || (flagConfigValue.eventTrackingContext && !flagConfigValue.eventTrackingContext.trackEvents)) { + DEBUG_LOG(@"Tracking is off. Discarding feature event %@", flagKey); + return; + } + DEBUG_LOG(@"Creating feature event for feature:%@ with flagConfigValue:%@ and fallback:%@", flagKey, flagConfigValue, defaultFlagValue); + [self addEventDictionary:[[LDEventModel featureEventWithFlagKey:flagKey + reportedFlagValue:reportedFlagValue + flagConfigValue:flagConfigValue + defaultFlagValue:defaultFlagValue + user:user + inlineUser:self.config.inlineUserInEvents] + dictionaryValueUsingConfig:self.config]]; +} + +-(void)recordCustomEventWithKey:(NSString *)eventKey customData:(NSDictionary *)customData user:(LDUserModel*)user { + DEBUG_LOG(@"Creating custom event for custom key:%@ and customData:%@", eventKey, customData); + [self addEventDictionary:[[LDEventModel customEventWithKey:eventKey customData:customData userValue:user inlineUser:self.config.inlineUserInEvents] dictionaryValueUsingConfig:self.config]]; +} + +-(void)recordIdentifyEventWithUser:(LDUserModel*)user { + DEBUG_LOG(@"Creating identify event for user key:%@", user.key); + [self addEventDictionary:[[LDEventModel identifyEventWithUser:user] dictionaryValueUsingConfig:self.config]]; +} + +-(void)recordSummaryEventWithTracker:(LDFlagConfigTracker*)tracker { + if (!tracker.hasTrackedEvents) { + DEBUG_LOGX(@"Tracker has no events to report. Discarding summary event."); + return; + } + LDEventModel *summaryEvent = [LDEventModel summaryEventWithTracker:tracker]; + if (summaryEvent == nil) { + DEBUG_LOGX(@"Failed to create summary event. Aborting."); + return; + } + DEBUG_LOGX(@"Creating summary event"); + [self addEventDictionary:[summaryEvent dictionaryValueUsingConfig:self.config]]; +} + +-(void)recordDebugEventWithFlagKey:(NSString *)flagKey + reportedFlagValue:(id)reportedFlagValue + flagConfigValue:(LDFlagConfigValue*)flagConfigValue + defaultFlagValue:(id)defaultFlagValue + user:(LDUserModel*)user { + if (![self shouldCreateDebugEventForContext:flagConfigValue.eventTrackingContext lastEventResponseDate:self.lastEventResponseDate]) { + DEBUG_LOG(@"LDDataManager createDebugEventWithFlagKey aborting, debug events are turned off. Discarding debug event %@", flagKey); + return; + } + DEBUG_LOG(@"Creating debug event for feature:%@ with flagConfigValue:%@ and fallback:%@", flagKey, flagConfigValue, defaultFlagValue); + [self addEventDictionary:[[LDEventModel debugEventWithFlagKey:flagKey + reportedFlagValue:reportedFlagValue + flagConfigValue:flagConfigValue + defaultFlagValue:defaultFlagValue + user:user] + dictionaryValueUsingConfig:self.config]]; +} + +-(BOOL)shouldCreateDebugEventForContext:(LDEventTrackingContext*)eventTrackingContext lastEventResponseDate:(NSDate*)lastEventResponseDate { + if (!eventTrackingContext || !eventTrackingContext.debugEventsUntilDate) { return NO; } + if ([lastEventResponseDate isLaterThan:eventTrackingContext.debugEventsUntilDate]) { return NO; } + if ([[NSDate date] isLaterThan:eventTrackingContext.debugEventsUntilDate]) { return NO; } + + return YES; +} + +-(void)addEventDictionary:(NSDictionary*)eventDictionary { + if (!eventDictionary || eventDictionary.allKeys.count == 0) { + DEBUG_LOGX(@"LDDataManager addEventDictionary aborting. Event dictionary is missing or empty."); + return; + } + dispatch_async(self.eventsQueue, ^{ + if([self isAtEventCapacity:self.eventsArray]) { + DEBUG_LOG(@"Events have surpassed capacity. Discarding event %@", eventDictionary[@"key"]); + return; + } + [self.eventsArray addObject:eventDictionary]; + }); +} + +-(BOOL)isAtEventCapacity:(NSArray *)currentArray { + return self.config.capacity && currentArray && [currentArray count] >= [self.config.capacity integerValue]; +} + +-(void) deleteProcessedEvents: (NSArray *) processedJsonArray { + // Loop through processedEvents + dispatch_async(self.eventsQueue, ^{ + NSInteger count = MIN([processedJsonArray count], [self.eventsArray count]); + [self.eventsArray removeObjectsInRange:NSMakeRange(0, count)]; + }); +} + +-(void) allEventDictionaries:(void (^)(NSArray *))completion { + dispatch_async(self.eventsQueue, ^{ + completion([NSArray arrayWithArray:self.eventsArray]); + }); +} + +-(void)discardEventsDictionary { + [self.eventsArray removeAllObjects]; +} + +@end diff --git a/Darkly/Services/LDEnvironment.h b/Darkly/Services/LDEnvironment.h new file mode 100644 index 00000000..cc6ce5ad --- /dev/null +++ b/Darkly/Services/LDEnvironment.h @@ -0,0 +1,44 @@ +// +// LDEnvironment.h +// Darkly +// +// Created by Mark Pokorny on 10/3/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import +#import "LDClient.h" +#import "LDConfig.h" +#import "LDUserModel.h" +#import "LDClientInterface.h" + +@interface LDEnvironment: NSObject + +@property (nonatomic, copy, readonly) NSString *mobileKey; +@property (nonatomic, strong, readonly) LDConfig *config; +@property (nonatomic, strong, readonly) LDUserModel *user; +@property (nonatomic, assign, getter=isStarted, readonly) BOOL start; +@property (nonatomic, assign, getter=isOnline) BOOL online; +@property (nonatomic, weak) id delegate; +@property (nonatomic, strong, readonly) NSDictionary *allFlags; +@property (nonatomic, assign, readonly) BOOL isPrimary; +@property (nonatomic, copy, readonly) NSString *environmentName; + ++(instancetype)environmentForMobileKey:(NSString*)mobileKey config:(LDConfig*)config user:(LDUserModel*)user; +-(instancetype)initForMobileKey:(NSString*)mobileKey config:(LDConfig*)config user:(LDUserModel*)user; + +-(void)start; +-(void)stop; +-(void)updateUser:(LDUserModel*)newUser; + +-(BOOL)boolVariation:(NSString *)featureKey fallback:(BOOL)fallback; +-(NSNumber*)numberVariation:(NSString *)featureKey fallback:(NSNumber*)fallback; +-(double)doubleVariation:(NSString *)featureKey fallback:(double)fallback; +-(NSString*)stringVariation:(NSString *)featureKey fallback:(NSString*)fallback; +-(NSArray*)arrayVariation:(NSString *)featureKey fallback:(NSArray*)fallback; +-(NSDictionary*)dictionaryVariation:(NSString *)featureKey fallback:(NSDictionary*)fallback; + +-(BOOL)track:(NSString*)eventName data:(NSDictionary *)dataDictionary; +-(BOOL)flush; + +@end diff --git a/Darkly/Services/LDEnvironment.m b/Darkly/Services/LDEnvironment.m new file mode 100644 index 00000000..b9808c13 --- /dev/null +++ b/Darkly/Services/LDEnvironment.m @@ -0,0 +1,368 @@ +// +// LDEnvironment.m +// Darkly +// +// Created by Mark Pokorny on 10/3/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDEnvironment.h" +#import "LDEnvironmentController.h" +#import "LDDataManager.h" + +#import "DarklyConstants.h" +#import "LDFlagConfigModel.h" +#import "LDFlagConfigValue.h" + +#import "LDUtil.h" +#import "NSThread+MainExecutable.h" + +@interface LDEnvironment () + +@property (nonatomic, copy) NSString *mobileKey; +@property (nonatomic, strong) LDConfig *config; +@property (nonatomic, strong) LDUserModel *user; +@property (nonatomic, assign, getter=isStarted) BOOL start; + +@property (nonatomic, strong) LDEnvironmentController *environmentController; +@property (nonatomic, strong) LDDataManager *dataManager; + +@end + +@implementation LDEnvironment +#pragma mark Lifecycle ++(instancetype)environmentForMobileKey:(NSString*)mobileKey config:(LDConfig*)config user:(LDUserModel*)user { + return [[LDEnvironment alloc] initForMobileKey:mobileKey config:config user:user]; +} + +-(instancetype)initForMobileKey:(NSString*)mobileKey config:(LDConfig*)config user:(LDUserModel*)user { + if (!(self = [super init])) { + return nil; + } + + self.mobileKey = mobileKey; + self.config = config; + self.user = [user copy]; + self.dataManager = [LDDataManager dataManagerWithMobileKey:self.mobileKey config:self.config]; + self.environmentController = [LDEnvironmentController controllerWithMobileKey:self.mobileKey config:self.config user:self.user dataManager:self.dataManager]; + + [self registerForNotifications]; + + return self; +} + +-(void)registerForNotifications { + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleUserUpdated:) name:kLDUserUpdatedNotification object: nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleUserUnchanged:) name:kLDUserNoChangeNotification object: nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleFeatureFlagsChanged:) name:kLDFeatureFlagsChangedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleServerUnavailable:) name:kLDServerConnectionUnavailableNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleClientUnauthorized:) name:kLDClientUnauthorizedNotification object:nil]; +} + +-(void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + self.delegate = nil; +} + +-(BOOL)isPrimary { + return [self.mobileKey isEqualToString:self.config.mobileKey]; +} + +-(NSString*)environmentName { + if ([self.mobileKey isEqualToString:self.config.mobileKey]) { + return kLDPrimaryEnvironmentName; + } + NSMutableDictionary *swappedMobileKeys = [NSMutableDictionary dictionaryWithCapacity:self.config.secondaryMobileKeys.count]; + [self.config.secondaryMobileKeys enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSString * _Nonnull obj, BOOL * _Nonnull stop) { + swappedMobileKeys[obj] = key; + }]; + return swappedMobileKeys[self.mobileKey]; +} + +#pragma mark - Controls + +-(void)start { + DEBUG_LOGX(@"LDEnvironment starting."); + if (self.isPrimary) { + [LDDataManager convertToEnvironmentBasedCacheForUser:self.user config:self.config]; + } + self.start = YES; + self.user.flagConfig = [self.dataManager retrieveFlagConfigForUser:self.user]; + [self.dataManager saveUser:self.user]; + [self.dataManager recordIdentifyEventWithUser:self.user]; +} + +-(void)setOnline:(BOOL)goOnline { + if (!self.isStarted && goOnline) { //Allow set offline when not started + DEBUG_LOG(@"LDEnvironment setOnline:%@ aborted. The environment is not started.", goOnline ? @"YES" : @"NO"); + return; + } + if (goOnline == self.isOnline && goOnline == self.environmentController.isOnline) { + DEBUG_LOG(@"LDEnvironment setOnline:%@ aborted. The environment is already %@.", goOnline ? @"YES" : @"NO", goOnline ? @"online" : @"offline"); + return; + } + + DEBUG_LOG(@"LDEnvironment going %@.", goOnline ? @"online" : @"offline"); + _online = goOnline; + self.environmentController.online = goOnline; +} + +-(BOOL)flush { + if (!self.isStarted) { + DEBUG_LOG(@"LDEnvironment %@ called while environment is not started.", NSStringFromSelector(_cmd)); + return NO; + } + if (!self.isOnline) { + DEBUG_LOG(@"LDEnvironment %@ called for mobile key %@ not online", NSStringFromSelector(_cmd), self.mobileKey); + return NO; + } + DEBUG_LOG(@"LDEnvironment %@ called.", NSStringFromSelector(_cmd)); + [self.environmentController flushEvents]; + return YES; +} + +-(void)stop { + DEBUG_LOGX(@"LDEnvironment stopping."); + self.online = NO; + self.start = NO; +} + +#pragma mark - Feature Flags + +-(BOOL)boolVariation:(NSString *)flagKey fallback:(BOOL)fallback { + if (!self.isStarted) { + DEBUG_LOG(@"LDEnvironment %@ called while environment is not started.", NSStringFromSelector(_cmd)); + return fallback; + } + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:flagKey]; + BOOL returnValue = flagConfigValue.value && [flagConfigValue.value isKindOfClass:[NSNumber class]] ? [flagConfigValue.value boolValue] : fallback; + + DEBUG_LOG(@"LDEnvironment %@ flagKey:%@ reportedValue:%@ fallback:%@", NSStringFromSelector(_cmd), flagKey, @(returnValue), @(fallback)); + [self.dataManager recordFlagEvaluationEventsWithFlagKey:flagKey + reportedFlagValue:@(returnValue) + flagConfigValue:flagConfigValue + defaultFlagValue:@(fallback) + user:self.user]; + return returnValue; +} + +-(NSNumber*)numberVariation:(NSString *)flagKey fallback:(NSNumber*)fallback { + if (!self.isStarted) { + DEBUG_LOG(@"LDEnvironment %@ called while environment is not started.", NSStringFromSelector(_cmd)); + return fallback; + } + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:flagKey]; + NSNumber *returnValue = flagConfigValue.value && [flagConfigValue.value isKindOfClass:[NSNumber class]] ? flagConfigValue.value : fallback; + + DEBUG_LOG(@"LDEnvironment %@ flagKey:%@ reportedValue:%@ fallback:%@", NSStringFromSelector(_cmd), flagKey, returnValue, fallback); + [self.dataManager recordFlagEvaluationEventsWithFlagKey:flagKey + reportedFlagValue:returnValue + flagConfigValue:flagConfigValue + defaultFlagValue:fallback + user:self.user]; + return returnValue; +} + +-(double)doubleVariation:(NSString *)flagKey fallback:(double)fallback { + if (!self.isStarted) { + DEBUG_LOG(@"LDEnvironment %@ called while environment is not started.", NSStringFromSelector(_cmd)); + return fallback; + } + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:flagKey]; + double returnValue = flagConfigValue.value && [flagConfigValue.value isKindOfClass:[NSNumber class]] ? [flagConfigValue.value doubleValue] : fallback; + + DEBUG_LOG(@"LDEnvironment %@ flagKey:%@ reportedValue:%@ fallback:%@", NSStringFromSelector(_cmd), flagKey, @(returnValue), @(fallback)); + [self.dataManager recordFlagEvaluationEventsWithFlagKey:flagKey + reportedFlagValue:@(returnValue) + flagConfigValue:flagConfigValue + defaultFlagValue:@(fallback) + user:self.user]; + return returnValue; +} + +-(NSString*)stringVariation:(NSString *)flagKey fallback:(NSString*)fallback { + if (!self.isStarted) { + DEBUG_LOG(@"LDEnvironment %@ called while environment is not started.", NSStringFromSelector(_cmd)); + return fallback; + } + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:flagKey]; + NSString *returnValue = flagConfigValue.value && [flagConfigValue.value isKindOfClass:[NSString class]] ? flagConfigValue.value : fallback; + + DEBUG_LOG(@"LDEnvironment %@ flagKey:%@ reportedValue:%@ fallback:%@", NSStringFromSelector(_cmd), flagKey, returnValue, fallback); + [self.dataManager recordFlagEvaluationEventsWithFlagKey:flagKey + reportedFlagValue:returnValue + flagConfigValue:flagConfigValue + defaultFlagValue:fallback + user:self.user]; + return returnValue; +} + +-(NSArray*)arrayVariation:(NSString *)flagKey fallback:(NSArray*)fallback { + if (!self.isStarted) { + DEBUG_LOG(@"LDEnvironment %@ called while environment is not started.", NSStringFromSelector(_cmd)); + return fallback; + } + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:flagKey]; + NSArray *returnValue = flagConfigValue.value && [flagConfigValue.value isKindOfClass:[NSArray class]] ? flagConfigValue.value : fallback; + + DEBUG_LOG(@"LDEnvironment %@ flagKey:%@ reportedValue:%@ fallback:%@", NSStringFromSelector(_cmd), flagKey, returnValue, fallback); + [self.dataManager recordFlagEvaluationEventsWithFlagKey:flagKey + reportedFlagValue:returnValue + flagConfigValue:flagConfigValue + defaultFlagValue:fallback + user:self.user]; + return returnValue; +} + +-(NSDictionary*)dictionaryVariation:(NSString *)flagKey fallback:(NSDictionary*)fallback { + if (!self.isStarted) { + DEBUG_LOG(@"LDEnvironment %@ called while environment is not started.", NSStringFromSelector(_cmd)); + return fallback; + } + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:flagKey]; + NSDictionary *returnValue = flagConfigValue.value && [flagConfigValue.value isKindOfClass:[NSDictionary class]] ? flagConfigValue.value : fallback; + + DEBUG_LOG(@"LDEnvironment %@ flagKey:%@ reportedValue:%@ fallback:%@", NSStringFromSelector(_cmd), flagKey, returnValue, fallback); + [self.dataManager recordFlagEvaluationEventsWithFlagKey:flagKey + reportedFlagValue:returnValue + flagConfigValue:flagConfigValue + defaultFlagValue:fallback + user:self.user]; + return returnValue; +} + +-(NSDictionary*)allFlags { + if (!self.isStarted) { + DEBUG_LOG(@"LDEnvironment %@ called while environment is not started.", NSStringFromSelector(_cmd)); + return nil; + } + return [self.user.flagConfig allFlagValues]; +} + +#pragma mark - Event Tracking + +-(BOOL)track:(NSString *)eventName data:(NSDictionary *)dataDictionary { + if (!self.isStarted) { + DEBUG_LOG(@"LDEnvironment %@ called while environment is not started.", NSStringFromSelector(_cmd)); + return NO; + } + DEBUG_LOG(@"LDEnvironment %@ eventName:%@ data:%@.", NSStringFromSelector(_cmd), eventName, dataDictionary); + [self.dataManager recordCustomEventWithKey:eventName customData:dataDictionary user:self.user]; + return YES; +} + +#pragma mark - User + +-(void)updateUser:(LDUserModel*)newUser { + if (!self.isStarted) { + DEBUG_LOGX(@"LDEnvironment updateUser aborted. The environment is not started."); + return; + } + if (newUser == nil) { + DEBUG_LOGX(@"LDEnvironment updateUser aborted. The newUser is nil."); + return; + } + + [self.dataManager recordSummaryEventWithTracker:self.user.flagConfigTracker]; + + DEBUG_LOG(@"LDEnvironment updating user key: %@, was online: %@", newUser.key, self.isOnline ? @"YES" : @"NO"); + BOOL wasOnline = self.isOnline; + self.online = NO; + + self.user = [newUser copy]; + if (self.isPrimary) { + [LDDataManager convertToEnvironmentBasedCacheForUser:self.user config:self.config]; + } + self.user.flagConfig = [self.dataManager retrieveFlagConfigForUser:self.user]; + [self.dataManager saveUser:self.user]; + [self.dataManager recordIdentifyEventWithUser:self.user]; + self.environmentController = [LDEnvironmentController controllerWithMobileKey:self.mobileKey config:self.config user:self.user dataManager:self.dataManager]; + + self.online = wasOnline; +} + +#pragma mark - Notification Handling +-(void)handleUserUpdated:(NSNotification*)notification { + if (![notification.userInfo[kLDNotificationUserInfoKeyMobileKey] isEqualToString:self.mobileKey]) { + return; + } + if (![self.delegate respondsToSelector:@selector(userDidUpdate)]) { + return; + } + __weak typeof(self) weakSelf = self; + [NSThread performOnMainThread:^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + [strongSelf.delegate userDidUpdate]; + } waitUntilDone:NO]; +} + +-(void)handleUserUnchanged:(NSNotification*)notification { + if (![notification.userInfo[kLDNotificationUserInfoKeyMobileKey] isEqualToString:self.mobileKey]) { + return; + } + if (![self.delegate respondsToSelector:@selector(userUnchanged)]) { + return; + } + __weak typeof(self) weakSelf = self; + [NSThread performOnMainThread:^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + [strongSelf.delegate userUnchanged]; + } waitUntilDone:NO]; +} + +-(void)handleFeatureFlagsChanged:(NSNotification *)notification { + if (![notification.userInfo[kLDNotificationUserInfoKeyMobileKey] isEqualToString:self.mobileKey]) { + return; + } + if (![self.delegate respondsToSelector:@selector(featureFlagDidUpdate:)]) { + return; + } + [self notifyDelegateOfUpdatesForFlagKeys:notification.userInfo[kLDNotificationUserInfoKeyFlagKeys]]; +} + +-(void)notifyDelegateOfUpdatesForFlagKeys:(NSArray*)updatedFlagKeys { + if (!self.isStarted || !self.isOnline) { + return; + } + if (updatedFlagKeys == nil || updatedFlagKeys.count == 0) { + return; + } + if (![self.delegate respondsToSelector:@selector(featureFlagDidUpdate:)]) { + return; + } + __weak typeof(self) weakSelf = self; + [NSThread performOnMainThread:^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + for (NSString *flagKey in updatedFlagKeys) { + [strongSelf.delegate featureFlagDidUpdate:flagKey]; + } + } waitUntilDone:NO]; +} + +-(void)handleServerUnavailable:(NSNotification*)notification { + if (![notification.userInfo[kLDNotificationUserInfoKeyMobileKey] isEqualToString:self.mobileKey]) { + return; + } + if (![self.delegate respondsToSelector:@selector(serverConnectionUnavailable)]) { + return; + } + __weak typeof(self) weakSelf = self; + [NSThread performOnMainThread:^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + [strongSelf.delegate serverConnectionUnavailable]; + } waitUntilDone:NO]; +} + +-(void)handleClientUnauthorized:(NSNotification*)notification { + if (![notification.userInfo[kLDNotificationUserInfoKeyMobileKey] isEqualToString:self.mobileKey]) { + return; + } + __weak typeof(self) weakSelf = self; + [NSThread performOnMainThread:^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + DEBUG_LOG(@"LDEnvironment for mobile key: %@ received Client Unauthorized notification. Taking LDEnvironment offline.", self.mobileKey); + strongSelf.online = NO; + } waitUntilDone:NO]; +} + +@end diff --git a/Darkly/Services/LDEnvironmentController.h b/Darkly/Services/LDEnvironmentController.h new file mode 100644 index 00000000..4dbd735e --- /dev/null +++ b/Darkly/Services/LDEnvironmentController.h @@ -0,0 +1,39 @@ +// +// Copyright © 2015 Catamorphic Co. All rights reserved. +// + + +#import "LDRequestManager.h" +#if TARGET_OS_OSX +#import +#else +#import +#endif +#import "LDConfig.h" +#import "LDUserModel.h" + +@class LDDataManager; + +#if TARGET_OS_OSX +@interface LDEnvironmentController : NSObject { +} +#else +@interface LDEnvironmentController : NSObject { +} +#endif + +NS_ASSUME_NONNULL_BEGIN + +@property (nonatomic, assign, getter=isOnline) BOOL online; +@property (nonatomic, copy, readonly) NSString *mobileKey; +@property (nonatomic, strong, readonly) LDConfig *config; +@property (nonatomic, strong, readonly) LDUserModel *user; + ++(instancetype)controllerWithMobileKey:(NSString*)mobileKey config:(LDConfig*)config user:(LDUserModel*)user dataManager:(LDDataManager*)dataManager; +-(instancetype)initWithMobileKey:(NSString*)mobileKey config:(LDConfig*)config user:(LDUserModel*)user dataManager:(LDDataManager*)dataManager; + +-(void)flushEvents; + +NS_ASSUME_NONNULL_END + +@end diff --git a/Darkly/Services/LDEnvironmentController.m b/Darkly/Services/LDEnvironmentController.m new file mode 100644 index 00000000..bea8e01d --- /dev/null +++ b/Darkly/Services/LDEnvironmentController.m @@ -0,0 +1,393 @@ +// +// Copyright © 2015 Catamorphic Co. All rights reserved. +// + +#import "LDEnvironmentController.h" +#import "LDPollingManager.h" +#import "LDDataManager.h" +#import "LDUtil.h" +#import "LDUserModel.h" +#import "LDEventModel.h" +#import "LDFlagConfigModel.h" +#import "NSDictionary+LaunchDarkly.h" +#import +#import "LDEvent+Unauthorized.h" +#import "LDEvent+EventTypes.h" + +NSString * const kLDStreamPath = @"meval"; + +@interface LDEnvironmentController() +@property (nonatomic, copy) NSString *mobileKey; +@property (nonatomic, strong) LDConfig *config; +@property (nonatomic, strong) LDUserModel *user; + +@property(nonatomic, strong) LDEventSource *eventSource; +@property(nonatomic, strong) NSDate *backgroundTime; +@property (nonatomic, weak) LDDataManager *dataManager; +@property(nonatomic, strong) LDRequestManager *requestManager; +@property (nonatomic, strong) dispatch_queue_t requestCallbackQueue; +@end + +@implementation LDEnvironmentController + +#pragma mark - Lifecycle + ++(instancetype)controllerWithMobileKey:(NSString*)mobileKey config:(LDConfig*)config user:(LDUserModel*)user dataManager:(LDDataManager*)dataManager { + return [[LDEnvironmentController alloc] initWithMobileKey:mobileKey config:config user:user dataManager:dataManager]; +} + +-(instancetype)initWithMobileKey:(NSString*)mobileKey config:(LDConfig*)config user:(LDUserModel*)user dataManager:(LDDataManager*)dataManager { + if (!(self = [super init])) { + return nil; + } + self.mobileKey = mobileKey; + self.config = config; + self.user = user; + self.dataManager = dataManager; + self.requestCallbackQueue = dispatch_queue_create([[NSString stringWithFormat:@"com.launchdarkly.environmentController.%@", self.mobileKey] UTF8String], DISPATCH_QUEUE_SERIAL); + self.requestManager = [LDRequestManager requestManagerForMobileKey:self.mobileKey config:self.config delegate:self callbackQueue:self.requestCallbackQueue]; + [self registerForNotifications]; + + return self; +} + +-(void)registerForNotifications { +#if TARGET_OS_IOS || TARGET_OS_TV + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterForeground) name:UIApplicationDidBecomeActiveNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterBackground) name:UIApplicationWillResignActiveNotification object:nil]; +#elif TARGET_OS_OSX + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterForeground) name:NSApplicationDidBecomeActiveNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willEnterBackground) name:NSApplicationWillResignActiveNotification object:nil]; +#endif + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundFetchInitiated) name:kLDBackgroundFetchInitiated object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(syncWithServerForConfig) name:kLDFlagConfigTimerFiredNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(syncWithServerForEvents) name:kLDEventTimerFiredNotification object:nil]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; //Required pre-ios9 +} + +#pragma mark - Control + +- (void)setOnline:(BOOL)online { + _online = online; + online ? [self startPolling] : [self stopPolling]; +} + +- (void)startPolling { + if (!self.isOnline) { + DEBUG_LOGX(@"EnvironmentController startPolling aborted - manager is offline"); + return; + } + + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:self.config isOnline:self.isOnline]; + + if ([self.config streaming]) { + [self configureEventSource]; + } + else{ + [self syncWithServerForConfig]; + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:self.config isOnline:self.isOnline]; + } +} + +- (void)stopPolling { + DEBUG_LOGX(@"EnvironmentController stopPolling method called"); + [[LDPollingManager sharedInstance] stopEventPolling]; + + if (self.config.streaming) { + [self stopEventSource]; + } else { + [[LDPollingManager sharedInstance] stopFlagConfigPolling]; + } + + [self flushEvents]; +} + +- (void)willEnterBackground { + DEBUG_LOGX(@"EnvironmentController entering background"); + [[LDPollingManager sharedInstance] suspendEventPolling]; + + if (self.config.streaming) { + [self stopEventSource]; + } + else{ + [[LDPollingManager sharedInstance] suspendFlagConfigPolling]; + } + + [self flushEvents]; + + self.backgroundTime = [NSDate date]; + +} + +- (void)willEnterForeground { + if (!self.isOnline) { + DEBUG_LOGX(@"EnvironmentController entering foreground offline"); + return; + } + DEBUG_LOGX(@"EnvironmentController entering foreground"); + [[LDPollingManager sharedInstance] resumeEventPollingWhenIsOnline:self.isOnline]; + + if (self.config.streaming) { + [self configureEventSource]; + } else { + [[LDPollingManager sharedInstance] resumeFlagConfigPollingWhenIsOnline:self.isOnline]; + } +} + +#pragma mark - Streaming + +- (void)configureEventSource { + @synchronized (self) { + if (!self.isOnline) { + DEBUG_LOGX(@"EnvironmentController configureEventSource aborted - manager is offline"); + return; + } + + if (self.eventSource) { + DEBUG_LOGX(@"EnvironmentController aborting event source creation - event source running"); + return; + } + + self.eventSource = [self eventSourceForUser:self.user config:self.config httpHeaders:[self httpHeadersForEventSource]]; + + __weak typeof(self) weakSelf = self; + [self.eventSource onMessage:^(LDEvent *event) { + __strong typeof(weakSelf) strongSelf = weakSelf; + [strongSelf handlePingEvent:event]; + [strongSelf handlePutEvent:event]; + [strongSelf handlePatchEvent:event]; + [strongSelf handleDeleteEvent:event]; + }]; + + [self.eventSource onError:^(LDEvent *event) { + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDServerConnectionUnavailableNotification message:@"clientstream reported error"]; + if (![event isUnauthorizedEvent]) { return; } + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDClientUnauthorizedNotification message:@"clientstream reported client unauthrorized"]; + }]; + } +} + +- (LDEventSource*)eventSourceForUser:(LDUserModel*)user config:(LDConfig*)config httpHeaders:(NSDictionary*)httpHeaders { + LDEventSource *eventSource; + if (config.useReport) { + eventSource = [LDEventSource eventSourceWithURL:[self eventSourceUrlForUser:user config:config] + httpHeaders:httpHeaders + connectMethod:kHTTPMethodReport + connectBody:[[[user dictionaryValueWithPrivateAttributesAndFlagConfig:NO] jsonString] dataUsingEncoding:NSUTF8StringEncoding]]; + } else { + eventSource = [LDEventSource eventSourceWithURL:[self eventSourceUrlForUser:user config:config] httpHeaders:httpHeaders connectMethod:nil connectBody:nil]; + } + return eventSource; +} + +- (NSURL*)eventSourceUrlForUser:(LDUserModel *)user config:(LDConfig*)config { + NSString *eventStreamUrl = [config.streamUrl stringByAppendingPathComponent:kLDStreamPath]; + if (!config.useReport) { + NSString *encodedUser = [LDUtil base64UrlEncodeString:[[user dictionaryValueWithPrivateAttributesAndFlagConfig:NO] jsonString]]; + eventStreamUrl = [eventStreamUrl stringByAppendingPathComponent:encodedUser]; + } + return [NSURL URLWithString:eventStreamUrl]; +} + +- (NSDictionary *)httpHeadersForEventSource { + return @{ @"Authorization": [kHeaderMobileKey stringByAppendingString:self.mobileKey], //Careful!! Use the mobileKey set into this environmentController, not from LDConfig!! + @"User-Agent": [@"iOS/" stringByAppendingString:kClientVersion] }; +} + +- (void)stopEventSource { + @synchronized (self) { + DEBUG_LOGX(@"EnvironmentController stopping event source."); + [self.eventSource close]; + self.eventSource = nil; + } +} + +- (void)handlePingEvent:(LDEvent*)event { + if (![event.event isEqualToString:kLDEventTypePing]) { return; } + [self syncWithServerForConfig]; +} + +- (void)handlePutEvent:(LDEvent*)event { + if (![event.event isEqualToString:kLDEventTypePut]) { return; } + if (event.data.length == 0) { + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDUserNoChangeNotification message:@"SSE put event missing data"]; + return; + } + NSDictionary *newConfigDictionary = [NSJSONSerialization JSONObjectWithData:[event.data dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil]; + if (!newConfigDictionary) { + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDUserNoChangeNotification message:@"SSE put event json could not be read"]; + return; + } + + LDFlagConfigModel *newConfig = [[LDFlagConfigModel alloc] initWithDictionary:newConfigDictionary]; + + NSString *updateResultNotificationName = [self.user.flagConfig isEqualToConfig:newConfig] ? kLDUserNoChangeNotification : kLDUserUpdatedNotification; + [self postFeatureFlagsChangedNotificationForChangedFlagKeys:[self.user.flagConfig differingFlagKeysFromConfig:newConfig]]; + self.user.flagConfig = newConfig; + [self.dataManager saveUser:self.user]; + [self reportFlagConfigProcessingCompleteWithNotificationName:updateResultNotificationName message:@"SSE put event complete"]; +} + +- (void)handlePatchEvent:(LDEvent*)event { + if (![event.event isEqualToString:kLDEventTypePatch]) { return; } + if (event.data.length == 0) { + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDUserNoChangeNotification message:@"SSE patch event missing data"]; + return; + } + NSDictionary *patchDictionary = [NSJSONSerialization JSONObjectWithData:[event.data dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil]; + if (!patchDictionary) { + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDUserNoChangeNotification message:@"SSE patch event json could not be read"]; + return; + } + + LDFlagConfigModel *originalFlagConfig = [self.user.flagConfig copy]; + + [self.user.flagConfig addOrReplaceFromDictionary:patchDictionary]; + + if ([self.user.flagConfig hasFeaturesEqualToDictionary:originalFlagConfig.dictionaryValue]) { + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDUserNoChangeNotification message:@"SSE patch event did not change feature flag value"]; + return; + } + + [self postFeatureFlagsChangedNotificationForChangedFlagKeys:[originalFlagConfig differingFlagKeysFromConfig:self.user.flagConfig]]; + [self.dataManager saveUser:self.user]; + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDUserUpdatedNotification message:@"SSE patch event complete"]; +} + +- (void)handleDeleteEvent:(LDEvent*)event { + if (![event.event isEqualToString:kLDEventTypeDelete]) { return; } + if (event.data.length == 0) { + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDUserNoChangeNotification message:@"SSE delete event missing data"]; + return; + } + NSDictionary *deleteDictionary = [NSJSONSerialization JSONObjectWithData:[event.data dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil]; + if (!deleteDictionary) { + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDUserNoChangeNotification message:@"SSE delete event json could not be read"]; + return; + } + + LDFlagConfigModel *originalFlagConfig = [self.user.flagConfig copy]; + + [self.user.flagConfig deleteFromDictionary:deleteDictionary]; + + if ([self.user.flagConfig hasFeaturesEqualToDictionary:originalFlagConfig.dictionaryValue]) { + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDUserNoChangeNotification message:@"SSE delete event did not change feature flags"]; + return; + } + + [self.dataManager saveUser:self.user]; + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDUserUpdatedNotification message:@"SSE delete event complete"]; +} + +#pragma mark - Polling + +- (void)backgroundFetchInitiated { + NSTimeInterval time = [[NSDate date] timeIntervalSinceDate:self.backgroundTime]; + if (time >= [self.config.backgroundFetchInterval doubleValue]) { + [self syncWithServerForConfig]; + } +} + +-(void)syncWithServerForConfig { + if (!self.isOnline) { + DEBUG_LOGX(@"EnvironmentController is in offline mode so won't sync config with server"); + return; + } + + if (!self.user) { + DEBUG_LOGX(@"EnvironmentController has no user so won't sync config with server"); + return; + } + + [self.requestManager performFeatureFlagRequest:self.user isOnline:self.isOnline]; +} + +- (void)processedConfig:(BOOL)success jsonConfigDictionary:(NSDictionary *)jsonConfigDictionary { + if (!success) { + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDServerConnectionUnavailableNotification message:@"flag request failed"]; + return; + } + + //success without json means a 304 Not Modified + if (jsonConfigDictionary == nil) { + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDUserNoChangeNotification message:@"not modified"]; + return; + } + + LDFlagConfigModel *newConfig = [[LDFlagConfigModel alloc] initWithDictionary:jsonConfigDictionary]; + if (!newConfig || [self.user.flagConfig isEqualToConfig:newConfig]) { + [self.user.flagConfig updateEventTrackingContextFromConfig:newConfig]; + //Notify interested clients and bail out if no new config, or the new config equals the existing config + NSString *message = newConfig == nil ? @"unable to create new flag config from json" : @"feature flags unchanged"; + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDUserNoChangeNotification message:message]; + return; + } + + [self postFeatureFlagsChangedNotificationForChangedFlagKeys:[self.user.flagConfig differingFlagKeysFromConfig:newConfig]]; + self.user.flagConfig = newConfig; + [self.dataManager saveUser:self.user]; + [self reportFlagConfigProcessingCompleteWithNotificationName:kLDUserUpdatedNotification message:@"flag request complete."]; +} + +#pragma mark - Flag Config Processing Notification + +-(void)postFeatureFlagsChangedNotificationForChangedFlagKeys:(NSArray*)flagKeys { + if (flagKeys == nil || flagKeys.count == 0) { + return; + } + [[NSNotificationCenter defaultCenter] postNotificationName:kLDFeatureFlagsChangedNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey, + kLDNotificationUserInfoKeyFlagKeys:flagKeys}]; + DEBUG_LOG(@"EnvironmentController for mobileKey:%@ posted %@ for flagKeys:%@", self.mobileKey, kLDFeatureFlagsChangedNotification, [flagKeys description]); +} + +-(void)reportFlagConfigProcessingCompleteWithNotificationName:(NSString*)notificationName message:(NSString*)message { + [[NSNotificationCenter defaultCenter] postNotificationName:notificationName object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey}]; + DEBUG_LOG(@"EnvironmentController for mobileKey:%@ posted %@%@%@.", self.mobileKey, notificationName, message.length > 0 ? @", " : @"", message ?: @""); +} + +#pragma mark - Events + +-(void)syncWithServerForEvents { + if (!self.isOnline) { + DEBUG_LOGX(@"EnvironmentController is in offline mode so won't sync events with server"); + return; + } + + DEBUG_LOGX(@"EnvironmentController syncing events with server"); + + [self.dataManager recordSummaryEventWithTracker:self.user.flagConfigTracker]; + + __weak typeof(self) weakSelf = self; + [self.dataManager allEventDictionaries:^(NSArray *eventDictionaries) { + __strong typeof(weakSelf) strongSelf = weakSelf; + [strongSelf.user resetTracker]; + if (eventDictionaries.count == 0) { + DEBUG_LOGX(@"EnvironmentController has no events so won't sync events with server"); + return; + } + [strongSelf.requestManager performEventRequest:eventDictionaries isOnline:self.isOnline]; + }]; +} + +- (void)processedEvents:(BOOL)success jsonEventArray:(NSArray *)jsonEventArray responseDate:(NSDate*)responseDate { + if (!success) { + return; + } + DEBUG_LOGX(@"EnvironmentController processedEvents method called after receiving successful response from server"); + [self.dataManager deleteProcessedEvents:jsonEventArray]; + self.dataManager.lastEventResponseDate = responseDate; +} + +- (void)flushEvents { + if (!self.isOnline) { + DEBUG_LOGX(@"EnvironmentController flushEvents aborted - manager is offline"); + return; + } + [self syncWithServerForEvents]; +} + +@end diff --git a/Darkly/Services/LDPollingManager.h b/Darkly/Services/LDPollingManager.h new file mode 100644 index 00000000..836479d1 --- /dev/null +++ b/Darkly/Services/LDPollingManager.h @@ -0,0 +1,40 @@ +// +// Copyright © 2015 Catamorphic Co. All rights reserved. +// + +#import +#import "LDConfig.h" + +typedef enum { + POLL_STOPPED = 0, + POLL_STARTING = 1, + POLL_RUNNING = 2, + POLL_SUSPENDED = 3, + +} PollingState; + +@interface LDPollingManager : NSObject + ++ (LDPollingManager*)sharedInstance; +@property (atomic, assign, readonly) PollingState flagConfigPollingState; +@property (atomic, assign, readonly) PollingState eventPollingState; + +@property (strong, nonatomic, readonly) dispatch_source_t flagConfigTimer; +@property (strong, nonatomic, readonly) dispatch_source_t eventTimer; + +@property (nonatomic, strong, readonly) LDConfig *config; + +- (void) startFlagConfigPollingUsingConfig:(LDConfig*)config isOnline:(BOOL)isOnline; +- (void) suspendFlagConfigPolling; +- (void) resumeFlagConfigPollingWhenIsOnline:(BOOL)isOnline; +- (void) stopFlagConfigPolling; +- (PollingState)flagConfigPollingState; + +///The LDPollingManager has no way to determine if the SDK is online. Verify the SDK is online prior to starting event polling +- (void) startEventPollingUsingConfig:(LDConfig*)config isOnline:(BOOL)isOnline; +- (void) suspendEventPolling; +- (void) resumeEventPollingWhenIsOnline:(BOOL)isOnline; +- (void) stopEventPolling; +- (PollingState)eventPollingState; + +@end diff --git a/Darkly/Services/LDPollingManager.m b/Darkly/Services/LDPollingManager.m new file mode 100644 index 00000000..ce4fdc6d --- /dev/null +++ b/Darkly/Services/LDPollingManager.m @@ -0,0 +1,258 @@ +// +// Copyright © 2015 Catamorphic Co. All rights reserved. +// + + +#import "LDPollingManager.h" +#import "LDUtil.h" +#import "DarklyConstants.h" +#import "NSNumber+LaunchDarkly.h" + +@interface LDPollingManager() +@property (atomic, assign) PollingState flagConfigPollingState; +@property (atomic, assign) PollingState eventPollingState; + +@property (strong, nonatomic) dispatch_source_t flagConfigTimer; +@property (strong, nonatomic) dispatch_source_t eventTimer; + +@property (nonatomic, strong) LDConfig *config; + +@property (nonatomic, assign) uint64_t flagConfigPollingIntervalNanos; +@property (nonatomic, assign) uint64_t eventPollingIntervalNanos; +@end + +@implementation LDPollingManager +static LDPollingManager *sharedInstance = nil; + ++ (instancetype)sharedInstance +{ + static dispatch_once_t once; + static LDPollingManager *sharedInstance; + dispatch_once(&once, ^{ + sharedInstance = [[self alloc] init]; + sharedInstance.config = nil; + }); + return sharedInstance; +} + +- (id)init { + if ((self = [super init])) { + self.flagConfigPollingState = POLL_STOPPED; + self.eventPollingState = POLL_STOPPED; + } + return self; +} + +- (void)dealloc { + [self stopFlagConfigPolling]; + [self stopEventPolling]; + + _flagConfigPollingState = POLL_STOPPED; + _eventPollingState = POLL_STOPPED; +} + +#pragma mark - General Polling methods +-(void)setFireTimeForTimer:(dispatch_source_t)timer pollingIntervalNanos:(uint64_t)pollingIntervalNanos { + if (timer == nil || pollingIntervalNanos <= 0) { + return; + } + dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, pollingIntervalNanos); + dispatch_source_set_timer(timer, startTime, pollingIntervalNanos, 1.0); +} + +#pragma mark - Config Polling methods +-(void)startFlagConfigPollingUsingConfig:(LDConfig*)config isOnline:(BOOL)isOnline { + @synchronized(self) { + if (!isOnline) { + DEBUG_LOGX(@"PollingManager unable to start flag config polling because SDK is offline."); + return; + } + if (self.flagConfigPollingState != POLL_STOPPED) { + return; //This could be called multiple times for any given start attempt, only the first should succeed + } + + self.flagConfigPollingState = POLL_STARTING; + self.config = config; + if ((self.flagConfigTimer == nil) && (self.flagConfigPollingIntervalNanos > 0.0)) { + self.flagConfigTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); + } + if (self.flagConfigTimer == nil) { + DEBUG_LOGX(@"PollingManager unable to create flagConfig timer."); + self.flagConfigPollingState = POLL_STOPPED; + return; + } + + DEBUG_LOG(@"PollingManager starting flagConfig pollingwith pollingInterval=%f", (double)self.flagConfigPollingIntervalNanos / NSEC_PER_SEC); + self.flagConfigPollingState = POLL_RUNNING; + dispatch_source_set_event_handler(self.flagConfigTimer, ^(void) { + [self flagConfigPoll]; + }); + [self setFireTimeForTimer:self.flagConfigTimer pollingIntervalNanos:self.flagConfigPollingIntervalNanos]; + dispatch_resume(self.flagConfigTimer); + } +} + +-(uint64_t)flagConfigPollingIntervalNanos { + if (self.config == nil) { + return [@(kDefaultPollingInterval) nanoSecondValue]; + } + //LDConfig precludes setting the pollingInterval below the min polling interval + return [self.config.pollingInterval nanoSecondValue]; +} + +- (void)flagConfigPoll { + @synchronized (self) { + if (self.flagConfigPollingState != POLL_RUNNING) { + DEBUG_LOGX(@"PollingManager flagConfig interval reached, but poll is not running. Aborting."); + [self stopFlagConfigPolling]; + return; + } + + DEBUG_LOGX(@"PollingManager flagConfig interval reached"); + [[NSNotificationCenter defaultCenter] postNotificationName:kLDFlagConfigTimerFiredNotification object:nil]; + } +} + +- (void) suspendFlagConfigPolling { + @synchronized(self) { + if (self.flagConfigPollingState != POLL_RUNNING) { + DEBUG_LOGX(@"PollingManager flagConfig polling is not running, unable to suspend"); + return; + } + DEBUG_LOGX(@"PollingManager suspending flagConfig polling"); + dispatch_suspend(self.flagConfigTimer); + self.flagConfigPollingState = POLL_SUSPENDED; + } +} + +- (void) resumeFlagConfigPollingWhenIsOnline:(BOOL)isOnline { + @synchronized (self) { + if (!isOnline) { + DEBUG_LOGX(@"PollingManager aborting resume flagConfig polling - sdk is offline"); + return; + } + if (self.flagConfigPollingState != POLL_SUSPENDED) { + DEBUG_LOGX(@"PollingManager aborting resume flagConfig polling - poll is not suspended"); + return; + } + + DEBUG_LOGX(@"PollingManager resuming flagConfig polling"); + dispatch_resume(self.flagConfigTimer); //If the configTimer would have fired while suspended, it triggers a flag request + self.flagConfigPollingState = POLL_RUNNING; + } +} + +- (void)stopFlagConfigPolling { + @synchronized (self) { + DEBUG_LOGX(@"PollingManager stopping flagConfig polling"); + if (self.flagConfigTimer != nil) { + dispatch_source_cancel(self.flagConfigTimer); + if (self.flagConfigPollingState == POLL_SUSPENDED) { + dispatch_resume(self.flagConfigTimer); + } + } + self.flagConfigTimer = nil; + self.flagConfigPollingState = POLL_STOPPED; + } +} + +#pragma mark - Event Polling methods +- (void) startEventPollingUsingConfig:(LDConfig*)config isOnline:(BOOL)isOnline { + @synchronized (self) { + if (!isOnline) { + DEBUG_LOGX(@"PollingManager unable to start event polling because SDK is offline."); + return; + } + if (self.eventPollingState != POLL_STOPPED) { + return; + } + + self.eventPollingState = POLL_STARTING; + self.config = config; + if ((!self.eventTimer) && (self.eventPollingIntervalNanos > 0.0)) { + self.eventTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)); + } + if (self.eventTimer == nil) { + DEBUG_LOGX(@"PollingManager unable to create event timer."); + self.eventPollingState = POLL_STOPPED; + return; + } + + DEBUG_LOG(@"PollingManager starting event polling with pollingInterval=%f", (double)self.eventPollingIntervalNanos / NSEC_PER_SEC); + self.eventPollingState = POLL_RUNNING; + dispatch_source_set_event_handler(self.eventTimer, ^(void) { + [self eventPoll]; + }); + [self setFireTimeForTimer:self.eventTimer pollingIntervalNanos:self.eventPollingIntervalNanos]; + dispatch_resume(self.eventTimer); + } +} + +-(uint64_t)eventPollingIntervalNanos { + if (self.config == nil) { return [@(kDefaultFlushInterval) nanoSecondValue]; } + if (!self.config.streaming && [self.config.flushInterval isEqual:@(kDefaultFlushInterval)]) { + return self.config.pollingInterval.nanoSecondValue; + } + if ([self.config.flushInterval intValue] <= kMinimumFlushInterval) { + return [@(kMinimumFlushInterval) nanoSecondValue]; + } + return [self.config.flushInterval nanoSecondValue]; +} + +- (void)eventPoll { + @synchronized (self) { + if (self.eventPollingState != POLL_RUNNING) { + DEBUG_LOGX(@"PollingManager event interval reached, but poll is not running. Aborting."); + [self stopEventPolling]; + return; + } + + DEBUG_LOGX(@"PollingManager event interval reached"); + [[NSNotificationCenter defaultCenter] postNotificationName:kLDEventTimerFiredNotification object:nil]; + } +} + +- (void) suspendEventPolling { + @synchronized (self) { + if (self.eventPollingState != POLL_RUNNING) { + DEBUG_LOGX(@"PollingManager event polling is not running, unable to suspend"); + return; + } + DEBUG_LOGX(@"PollingManager suspending event polling"); + dispatch_suspend(self.eventTimer); + self.eventPollingState = POLL_SUSPENDED; + } +} + +-(void)resumeEventPollingWhenIsOnline:(BOOL)isOnline { + @synchronized (self) { + if (!isOnline) { + DEBUG_LOGX(@"PollingManager aborting resume event polling - sdk is offline"); + return; + } + if (self.eventPollingState != POLL_SUSPENDED) { + DEBUG_LOGX(@"PollingManager aborting resume event polling - poll is not suspended"); + return; + } + + DEBUG_LOGX(@"PollingManager resuming event polling"); + dispatch_resume(self.eventTimer); //If the eventTimer would have fired while suspended, it triggers an event request + self.eventPollingState = POLL_RUNNING; + } +} + +- (void)stopEventPolling { + @synchronized (self) { + DEBUG_LOGX(@"PollingManager stopping event polling"); + if (self.eventTimer != nil) { + dispatch_source_cancel(self.eventTimer); + if (self.eventPollingState == POLL_SUSPENDED) { + dispatch_resume(self.eventTimer); + } + } + self.eventTimer = nil; + self.eventPollingState = POLL_STOPPED; + } +} + +@end diff --git a/Darkly/Services/LDRequestManager.h b/Darkly/Services/LDRequestManager.h new file mode 100644 index 00000000..cf8de1bc --- /dev/null +++ b/Darkly/Services/LDRequestManager.h @@ -0,0 +1,32 @@ +// +// Copyright © 2015 Catamorphic Co. All rights reserved. +// + +#import "LDUserModel.h" +#import "LDConfig.h" + +NS_ASSUME_NONNULL_BEGIN + +@protocol RequestManagerDelegate +-(void)processedEvents:(BOOL)success jsonEventArray:(NSArray*)jsonEventArray responseDate:(nullable NSDate*)responseDate; +-(void)processedConfig:(BOOL)success jsonConfigDictionary:(nullable NSDictionary*)jsonConfigDictionary; +@end + +@interface LDRequestManager : NSObject +@property (nonatomic, copy, readonly) NSString* mobileKey; +@property (nullable, nonatomic, weak) id delegate; + ++(nullable instancetype)requestManagerForMobileKey:(NSString*)mobileKey + config:(LDConfig*)config + delegate:(nullable id)delegate + callbackQueue:(nullable dispatch_queue_t)callbackQueue; +-(nullable instancetype)initForMobileKey:(NSString*)mobileKey + config:(LDConfig*)config + delegate:(nullable id)delegate + callbackQueue:(nullable dispatch_queue_t)callbackQueue; + +-(void)performFeatureFlagRequest:(nullable LDUserModel*)user isOnline:(BOOL)isOnline; +-(void)performEventRequest:(nullable NSArray*)eventDictionaries isOnline:(BOOL)isOnline; +@end + +NS_ASSUME_NONNULL_END diff --git a/Darkly/LDRequestManager.m b/Darkly/Services/LDRequestManager.m similarity index 53% rename from Darkly/LDRequestManager.m rename to Darkly/Services/LDRequestManager.m index c4c598a2..2346f94e 100644 --- a/Darkly/LDRequestManager.m +++ b/Darkly/Services/LDRequestManager.m @@ -4,92 +4,96 @@ #import "LDRequestManager.h" #import "LDUtil.h" -#import "LDClientManager.h" -#import "LDConfig.h" #import "NSURLResponse+LaunchDarkly.h" -#import "NSDictionary+JSON.h" +#import "NSDictionary+LaunchDarkly.h" #import "NSHTTPURLResponse+LaunchDarkly.h" +#import "NSURLSession+LaunchDarkly.h" -static NSString * const kFeatureFlagGetUrl = @"/msdk/evalx/users/"; +NSString * const kFeatureFlagGetUrl = @"/msdk/evalx/users/"; static NSString * const kFeatureFlagReportUrl = @"/msdk/evalx/user"; static NSString * const kEventUrl = @"/mobile/events/bulk"; -NSString * const kHeaderMobileKey = @"api_key "; static NSString * const kConfigRequestCompletedNotification = @"config_request_completed_notification"; static NSString * const kEventRequestCompletedNotification = @"event_request_completed_notification"; +NSString * const kFlagRequestHeaderIfNoneMatch = @"if-none-match"; NSString * const kEventHeaderLaunchDarklyEventSchema = @"X-LaunchDarkly-Event-Schema"; NSString * const kEventSchema = @"3"; -@implementation LDRequestManager - -@synthesize mobileKey, baseUrl, eventsUrl, connectionTimeout, delegate; +@interface LDRequestManager() +@property (nonnull, nonatomic, copy) NSString* mobileKey; +@property (nonnull, nonatomic) LDConfig *config; +@property (nullable, nonatomic, strong) dispatch_queue_t callbackQueue; +@property (nonnull, readonly) dispatch_queue_t responseCallbackQueue; +@property (nullable, copy, nonatomic) NSString *featureFlagEtag; +@end -dispatch_queue_t notificationQueue; +@implementation LDRequestManager -+(LDRequestManager *)sharedInstance { - static LDRequestManager *sharedApiManager = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedApiManager = [[self alloc] init]; - [sharedApiManager setDelegate:[LDClientManager sharedInstance]]; - notificationQueue = dispatch_queue_create("com.launchdarkly.LDRequestManager.NotificationQueue", NULL); - }); - return sharedApiManager; ++(instancetype)requestManagerForMobileKey:(NSString*)mobileKey config:(LDConfig*)config delegate:(id)delegate callbackQueue:(dispatch_queue_t)callbackQueue { + return [[LDRequestManager alloc] initForMobileKey:mobileKey config:config delegate:delegate callbackQueue:callbackQueue]; } -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; +-(instancetype)initForMobileKey:(NSString*)mobileKey config:(LDConfig*)config delegate:(id)delegate callbackQueue:(dispatch_queue_t)callbackQueue { + if (!(self = [super init])) { + return nil; + } + self.mobileKey = mobileKey; + self.config = config; + self.delegate = delegate; + self.callbackQueue = callbackQueue; + + return self; } --(void)configure:(LDConfig*)config { - self.mobileKey = config.mobileKey; - self.baseUrl = config.baseUrl; - self.eventsUrl = config.eventsUrl; - self.connectionTimeout = [config.connectionTimeout doubleValue]; +-(dispatch_queue_t)responseCallbackQueue { + if (self.callbackQueue != nil) { + return self.callbackQueue; + } + return dispatch_get_main_queue(); } --(void)performFeatureFlagRequest:(LDUserModel *)user -{ - [self configure:[LDClient sharedInstance].ldConfig]; - if (!mobileKey) { +-(void)performFeatureFlagRequest:(LDUserModel *)user isOnline:(BOOL)isOnline { + if (!isOnline) { + DEBUG_LOGX(@"RequestManager unable to sync config to server because SDK is offline"); + return; + } + if (!self.mobileKey) { DEBUG_LOGX(@"RequestManager unable to sync config to server since no mobileKey"); return; } - if (!user) { DEBUG_LOGX(@"RequestManager unable to sync config to server since no user"); return; } - if (![LDClientManager sharedInstance].isOnline) { - DEBUG_LOGX(@"RequestManager aborting sync config - client is offline"); - return; - } - - if ([LDClient sharedInstance].ldConfig.useReport) { + if (self.config.useReport) { DEBUG_LOGX(@"RequestManager syncing config to server via REPORT"); NSURLRequest *flagRequestUsingReportMethod = [self flagRequestUsingReportMethodForUser:user]; + __weak typeof(self) weakSelf = self; [self performFlagRequest:flagRequestUsingReportMethod completionHandler:^(NSData * _Nullable originalData, NSURLResponse * _Nullable originalResponse, NSError * _Nullable originalError) { - - if ([self shouldTryFlagGetRequestForFlagResponse:originalResponse]) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if ([strongSelf shouldTryFlagGetRequestForFlagResponse:originalResponse]) { NSURLRequest *flagRequestUsingGetMethod = [self flagRequestUsingGetMethodForUser:user]; if (flagRequestUsingGetMethod) { DEBUG_LOGX(@"RequestManager syncing config to server via GET"); - [self performFlagRequest:flagRequestUsingGetMethod completionHandler:^(NSData * _Nullable retriedData, NSURLResponse * _Nullable retriedResponse, NSError * _Nullable retriedError) { - [self processFlagResponseWithData:retriedData error:retriedError]; + [strongSelf performFlagRequest:flagRequestUsingGetMethod completionHandler:^(NSData * _Nullable retriedData, NSURLResponse * _Nullable retriedResponse, NSError * _Nullable retriedError) { + __strong typeof(weakSelf) strongSelf = weakSelf; + [strongSelf processFlagResponse:retriedResponse data:retriedData error:retriedError]; }]; return; } } - [self processFlagResponseWithData:originalData error:originalError]; + [strongSelf processFlagResponse:originalResponse data:originalData error:originalError]; }]; } else { DEBUG_LOGX(@"RequestManager syncing config to server via GET"); NSURLRequest *flagRequestUsingGetMethod = [self flagRequestUsingGetMethodForUser:user]; + __weak typeof(self) weakSelf = self; [self performFlagRequest:flagRequestUsingGetMethod completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { - [self processFlagResponseWithData:data error:error]; + __strong typeof(weakSelf) strongSelf = weakSelf; + [strongSelf processFlagResponse:response data:data error:error]; }]; } } @@ -98,33 +102,40 @@ -(BOOL)shouldTryFlagGetRequestForFlagResponse:(NSURLResponse*)flagResponse { if (!flagResponse) { return NO; } if (![flagResponse isKindOfClass:[NSHTTPURLResponse class]]) { return NO; } NSHTTPURLResponse *httpFlagResponse = (NSHTTPURLResponse*)flagResponse; - return [LDClient sharedInstance].ldConfig.useReport && [[LDClient sharedInstance].ldConfig isFlagRetryStatusCode:httpFlagResponse.statusCode]; + return self.config.useReport && [self.config isFlagRetryStatusCode:httpFlagResponse.statusCode]; } --(void)processFlagResponseWithData:(NSData*)data error:(NSError*)error { +-(void)processFlagResponse:(NSURLResponse*)response data:(NSData*)data error:(NSError*)error { BOOL configProcessed = NO; NSDictionary *featureFlags; if (!error) { NSError *jsonError; featureFlags = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError]; - configProcessed = featureFlags != nil; + configProcessed = (response.isOk && featureFlags != nil) || response.isNotModified; } - dispatch_async(dispatch_get_main_queue(), ^{ - [self.delegate processedConfig:configProcessed jsonConfigDictionary:featureFlags]; + if (response.isOk && featureFlags != nil) { + self.featureFlagEtag = response.etag; + } else if (!response.isNotModified) { + self.featureFlagEtag = nil; + } + __weak typeof(self) weakSelf = self; + dispatch_async(self.responseCallbackQueue, ^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + [strongSelf.delegate processedConfig:configProcessed jsonConfigDictionary:featureFlags]; }); } -(void)performFlagRequest:(NSURLRequest*)request completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler { if (!request) { return; } - NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; - NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration:configuration delegate:nil delegateQueue:nil]; - - NSURLSessionDataTask *dataTask = [defaultSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + __weak typeof(self) weakSelf = self; + NSURLSessionDataTask *dataTask = [[NSURLSession sharedLDSession] dataTaskWithRequest:request + completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + __strong typeof(weakSelf) strongSelf = weakSelf; if ([response isUnauthorizedHTTPResponse]) { - //Calling postNotification on the task completion handler thread causes the LDRequestManager to hang in some situations. Dispatching the postNotification onto the notificationQueue avoids that hang. - dispatch_async(notificationQueue, ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:kLDClientUnauthorizedNotification object:nil]; + //Calling postNotification on the task completion handler thread causes the LDRequestManager to hang in some situations. Dispatching the postNotification onto the delegateCallbackQueue avoids that hang. + dispatch_async(strongSelf.responseCallbackQueue, ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:kLDClientUnauthorizedNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey}]; }); } @@ -136,9 +147,12 @@ -(void)performFlagRequest:(NSURLRequest*)request completionHandler:(void (^)(NSD [dataTask resume]; } --(void)performEventRequest:(NSArray *)eventDictionaries { - [self configure:[LDClient sharedInstance].ldConfig]; - if (!mobileKey) { +-(void)performEventRequest:(NSArray *)eventDictionaries isOnline:(BOOL)isOnline { + if (!isOnline) { + DEBUG_LOGX(@"RequestManager unable to sync events to server because SDK is offline"); + return; + } + if (!self.mobileKey) { DEBUG_LOGX(@"RequestManager unable to sync events to server since no mobileKey"); return; } @@ -148,18 +162,12 @@ -(void)performEventRequest:(NSArray *)eventDictionaries { return; } - if (![LDClientManager sharedInstance].isOnline) { - DEBUG_LOGX(@"RequestManager aborting sync events - client is offline"); - return; - } - DEBUG_LOGX(@"RequestManager syncing events to server"); - NSURLSession *defaultSession = [NSURLSession sharedSession]; - NSString *requestUrl = [eventsUrl stringByAppendingString:kEventUrl]; + NSString *requestUrl = [self.config.eventsUrl stringByAppendingString:kEventUrl]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:requestUrl]]; - [request setTimeoutInterval:self.connectionTimeout]; + [request setTimeoutInterval:[self.config.connectionTimeout doubleValue]]; [self addEventRequestHeaders:request]; NSError *error; @@ -168,17 +176,20 @@ -(void)performEventRequest:(NSArray *)eventDictionaries { [request setHTTPMethod:@"POST"]; [request setHTTPBody:postData]; - NSURLSessionDataTask *dataTask = [defaultSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + __weak typeof(self) weakSelf = self; + NSURLSessionDataTask *dataTask = [[NSURLSession sharedLDSession] dataTaskWithRequest:request + completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + __strong typeof(weakSelf) strongSelf = weakSelf; if ([response isUnauthorizedHTTPResponse]) { - //Calling postNotification on the task completion handler thread causes the LDRequestManager to hang in some situations. Dispatching the postNotification onto the notificationQueue avoids that hang. - dispatch_async(notificationQueue, ^{ - [[NSNotificationCenter defaultCenter] postNotificationName:kLDClientUnauthorizedNotification object:nil]; + //Calling postNotification on the task completion handler thread causes the LDRequestManager to hang in some situations. Dispatching the postNotification onto the delegateCallbackQueue avoids that hang. + dispatch_async(strongSelf.responseCallbackQueue, ^{ + [[NSNotificationCenter defaultCenter] postNotificationName:kLDClientUnauthorizedNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey}]; }); } - dispatch_async(dispatch_get_main_queue(), ^{ + dispatch_async(strongSelf.responseCallbackQueue, ^{ BOOL processedEvents = !error; - [self.delegate processedEvents:processedEvents jsonEventArray:eventDictionaries responseDate:[response headerDate]]; + [strongSelf.delegate processedEvents:processedEvents jsonEventArray:eventDictionaries responseDate:[response headerDate]]; }); }]; @@ -197,11 +208,12 @@ -(NSURLRequest*)flagRequestUsingReportMethodForUser:(LDUserModel*)user { return nil; } - NSString *requestUrl = [baseUrl stringByAppendingString:kFeatureFlagReportUrl]; + NSString *requestUrl = [self.config.baseUrl stringByAppendingString:kFeatureFlagReportUrl]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:requestUrl]]; request.HTTPMethod = kHTTPMethodReport; request.HTTPBody = [userJson dataUsingEncoding:NSUTF8StringEncoding]; - [request setTimeoutInterval:self.connectionTimeout]; + [request setTimeoutInterval:[self.config.connectionTimeout doubleValue]]; + request.cachePolicy = [self flagRequestCachePolicyForEtag:self.featureFlagEtag]; [self addFeatureRequestHeaders:request]; return request; @@ -222,24 +234,33 @@ -(NSURLRequest*)flagRequestUsingGetMethodForUser:(LDUserModel*)user { DEBUG_LOGX(@"RequestManager could not base64Url encode user, aborting sync config to server"); return nil; } - NSString *requestUrl = [baseUrl stringByAppendingString:kFeatureFlagGetUrl]; + NSString *requestUrl = [self.config.baseUrl stringByAppendingString:kFeatureFlagGetUrl]; requestUrl = [requestUrl stringByAppendingString:encodedUser]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:requestUrl]]; - [request setTimeoutInterval:self.connectionTimeout]; + [request setTimeoutInterval:[self.config.connectionTimeout doubleValue]]; + request.cachePolicy = [self flagRequestCachePolicyForEtag:self.featureFlagEtag]; [self addFeatureRequestHeaders:request]; return request; } +-(NSURLRequestCachePolicy)flagRequestCachePolicyForEtag:(NSString*)etag { + return etag.length == 0 ? NSURLRequestReloadIgnoringLocalCacheData : NSURLRequestUseProtocolCachePolicy; +} + -(void)addFeatureRequestHeaders:(NSMutableURLRequest *)request { - NSString *authKey = [kHeaderMobileKey stringByAppendingString:mobileKey]; + NSString *authKey = [kHeaderMobileKey stringByAppendingString:self.mobileKey]; [request addValue:authKey forHTTPHeaderField:@"Authorization"]; [request addValue:[@"iOS/" stringByAppendingString:kClientVersion] forHTTPHeaderField:@"User-Agent"]; + if (self.featureFlagEtag.length == 0) { + return; + } + [request addValue: self.featureFlagEtag forHTTPHeaderField:kFlagRequestHeaderIfNoneMatch]; } -(void)addEventRequestHeaders: (NSMutableURLRequest *)request { - NSString *authKey = [kHeaderMobileKey stringByAppendingString:mobileKey]; + NSString *authKey = [kHeaderMobileKey stringByAppendingString:self.mobileKey]; [request addValue:authKey forHTTPHeaderField:@"Authorization"]; [request addValue:kEventSchema forHTTPHeaderField:kEventHeaderLaunchDarklyEventSchema]; diff --git a/Darkly/LDThrottler.h b/Darkly/Services/LDThrottler.h similarity index 100% rename from Darkly/LDThrottler.h rename to Darkly/Services/LDThrottler.h diff --git a/Darkly/LDThrottler.m b/Darkly/Services/LDThrottler.m similarity index 100% rename from Darkly/LDThrottler.m rename to Darkly/Services/LDThrottler.m diff --git a/Darkly/Services/LDURLCache.h b/Darkly/Services/LDURLCache.h new file mode 100644 index 00000000..0834acd6 --- /dev/null +++ b/Darkly/Services/LDURLCache.h @@ -0,0 +1,27 @@ +// +// LDURLCache.h +// Darkly +// +// Created by Mark Pokorny on 11/16/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import + +@class LDConfig; + +NS_ASSUME_NONNULL_BEGIN + +@interface LDURLCache : NSURLCache ++(NSURLCache*)urlCacheForConfig:(LDConfig*)config usingCache:(NSURLCache*)baseCache; + ++(BOOL)shouldUseLDURLCacheForConfig:(LDConfig*)config; + +-(void)storeCachedResponse:(NSCachedURLResponse*)cachedResponse forDataTask:(NSURLSessionDataTask*)dataTask; +-(void)storeCachedResponse:(NSCachedURLResponse*)cachedResponse forRequest:(NSURLRequest*)request; + +-(void)getCachedResponseForDataTask:(NSURLSessionDataTask*)dataTask completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler; +-(NSCachedURLResponse*)cachedResponseForRequest:(NSURLRequest*)request; +@end + +NS_ASSUME_NONNULL_END diff --git a/Darkly/Services/LDURLCache.m b/Darkly/Services/LDURLCache.m new file mode 100644 index 00000000..12d565ac --- /dev/null +++ b/Darkly/Services/LDURLCache.m @@ -0,0 +1,84 @@ +// +// LDURLCache.m +// Darkly +// +// Created by Mark Pokorny on 11/16/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDURLCache.h" +#import "DarklyConstants.h" +#import "LDUtil.h" +#import "LDConfig.h" + +extern NSString * const kFeatureFlagGetUrl; + +@interface LDURLCache () +@property (nonatomic, strong) NSURLCache *baseUrlCache; +@end + +@implementation LDURLCache ++(instancetype)urlCacheWithNSURLCache:(NSURLCache*)baseUrlCache { + return [[LDURLCache alloc] initWithNSURLCache:baseUrlCache]; +} + +-(instancetype)initWithNSURLCache:(NSURLCache*)baseUrlCache { + if (baseUrlCache == nil || !(self = [super init])) { + return nil; + } + self.baseUrlCache = baseUrlCache; + return self; +} + ++(NSURLCache*)urlCacheForConfig:(LDConfig*)config usingCache:(NSURLCache*)baseCache { + if (![LDURLCache shouldUseLDURLCacheForConfig:config]) { + return baseCache; + } + return [LDURLCache urlCacheWithNSURLCache:baseCache]; +} + ++(BOOL)shouldUseLDURLCacheForConfig:(LDConfig*)config { + return !config.streaming && config.useReport; +} + +-(void)storeCachedResponse:(NSCachedURLResponse*)cachedResponse forDataTask:(NSURLSessionDataTask*)dataTask { + if (![dataTask.originalRequest.HTTPMethod isEqualToString:kHTTPMethodReport]) { + [self.baseUrlCache storeCachedResponse:cachedResponse forDataTask:dataTask]; + } + [self storeCachedResponse:cachedResponse forRequest:dataTask.originalRequest]; +} + +-(void)storeCachedResponse:(NSCachedURLResponse*)cachedResponse forRequest:(NSURLRequest*)request { + [self.baseUrlCache storeCachedResponse:cachedResponse forRequest:[self getRequestFromRequest:request]]; +} + +-(NSURLRequest*)getRequestFromRequest:(NSURLRequest*)request { + if (![request.HTTPMethod isEqualToString:kHTTPMethodReport]) { + return request; + } + NSString *encodedUser = [LDUtil base64UrlEncodeString:[[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding]]; + NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@://%@%@%@", request.URL.scheme, request.URL.host, kFeatureFlagGetUrl, encodedUser]]; + NSMutableURLRequest *getRequest = [NSMutableURLRequest requestWithURL:url]; + getRequest.timeoutInterval = request.timeoutInterval; + getRequest.cachePolicy = request.cachePolicy; + getRequest.allHTTPHeaderFields = request.allHTTPHeaderFields; + + return getRequest; +} + +-(void)getCachedResponseForDataTask:(NSURLSessionDataTask*)dataTask completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler { + if (![dataTask.originalRequest.HTTPMethod isEqualToString:kHTTPMethodReport]) { + [self.baseUrlCache getCachedResponseForDataTask:dataTask completionHandler:completionHandler]; + return; + } + + NSCachedURLResponse *cachedResponse = [self cachedResponseForRequest:[self getRequestFromRequest:dataTask.originalRequest]]; + if (completionHandler != nil) { + completionHandler(cachedResponse); + } +} + +-(NSCachedURLResponse*)cachedResponseForRequest:(NSURLRequest*)request { + return [self.baseUrlCache cachedResponseForRequest:[self getRequestFromRequest:request]]; +} +@end diff --git a/Darkly/Darkly-Prefix.pch b/Darkly/Support/Darkly-Prefix.pch similarity index 65% rename from Darkly/Darkly-Prefix.pch rename to Darkly/Support/Darkly-Prefix.pch index 4dd16f0f..b84a1ab0 100644 --- a/Darkly/Darkly-Prefix.pch +++ b/Darkly/Support/Darkly-Prefix.pch @@ -4,6 +4,4 @@ #ifdef __OBJC__ #import - #import "DarklyConstants.h" - #import "NSDictionary+JSON.h" #endif diff --git a/Darkly/LDUtil.h b/Darkly/Utilities/LDUtil.h similarity index 100% rename from Darkly/LDUtil.h rename to Darkly/Utilities/LDUtil.h diff --git a/Darkly/LDUtil.m b/Darkly/Utilities/LDUtil.m similarity index 100% rename from Darkly/LDUtil.m rename to Darkly/Utilities/LDUtil.m diff --git a/DarklyTests/Categories/NSArray+Testable.h b/DarklyTests/Categories/CocoaCategories/NSArray+Testable.h similarity index 100% rename from DarklyTests/Categories/NSArray+Testable.h rename to DarklyTests/Categories/CocoaCategories/NSArray+Testable.h diff --git a/DarklyTests/Categories/NSArray+Testable.m b/DarklyTests/Categories/CocoaCategories/NSArray+Testable.m similarity index 100% rename from DarklyTests/Categories/NSArray+Testable.m rename to DarklyTests/Categories/CocoaCategories/NSArray+Testable.m diff --git a/DarklyTests/Categories/NSDate+Testable.h b/DarklyTests/Categories/CocoaCategories/NSDate+Testable.h similarity index 100% rename from DarklyTests/Categories/NSDate+Testable.h rename to DarklyTests/Categories/CocoaCategories/NSDate+Testable.h diff --git a/DarklyTests/Categories/NSDate+Testable.m b/DarklyTests/Categories/CocoaCategories/NSDate+Testable.m similarity index 100% rename from DarklyTests/Categories/NSDate+Testable.m rename to DarklyTests/Categories/CocoaCategories/NSDate+Testable.m diff --git a/DarklyTests/Categories/NSDateFormatter+JsonHeader+Testable.h b/DarklyTests/Categories/CocoaCategories/NSDateFormatter+JsonHeader+Testable.h similarity index 100% rename from DarklyTests/Categories/NSDateFormatter+JsonHeader+Testable.h rename to DarklyTests/Categories/CocoaCategories/NSDateFormatter+JsonHeader+Testable.h diff --git a/DarklyTests/Categories/NSDateFormatter+JsonHeader+Testable.m b/DarklyTests/Categories/CocoaCategories/NSDateFormatter+JsonHeader+Testable.m similarity index 100% rename from DarklyTests/Categories/NSDateFormatter+JsonHeader+Testable.m rename to DarklyTests/Categories/CocoaCategories/NSDateFormatter+JsonHeader+Testable.m diff --git a/DarklyTests/Categories/NSDictionary+StringKey_Matchable.h b/DarklyTests/Categories/CocoaCategories/NSDictionary+StringKey_Matchable.h similarity index 100% rename from DarklyTests/Categories/NSDictionary+StringKey_Matchable.h rename to DarklyTests/Categories/CocoaCategories/NSDictionary+StringKey_Matchable.h diff --git a/DarklyTests/Categories/NSDictionary+StringKey_Matchable.m b/DarklyTests/Categories/CocoaCategories/NSDictionary+StringKey_Matchable.m similarity index 100% rename from DarklyTests/Categories/NSDictionary+StringKey_Matchable.m rename to DarklyTests/Categories/CocoaCategories/NSDictionary+StringKey_Matchable.m diff --git a/DarklyTests/Categories/NSDictionary+Testable.h b/DarklyTests/Categories/CocoaCategories/NSDictionary+Testable.h similarity index 82% rename from DarklyTests/Categories/NSDictionary+Testable.h rename to DarklyTests/Categories/CocoaCategories/NSDictionary+Testable.h index a4a5a3a3..2ec29959 100644 --- a/DarklyTests/Categories/NSDictionary+Testable.h +++ b/DarklyTests/Categories/CocoaCategories/NSDictionary+Testable.h @@ -12,4 +12,5 @@ @interface NSDictionary(Testable) -(BOOL)boolValueForKey:(nullable NSString*)key; -(NSInteger)integerValueForKey:(nullable NSString*)key; +-(BOOL)isEqualToUserEnvironmentUsersDictionary:(NSDictionary*)otherDictionary; @end diff --git a/DarklyTests/Categories/CocoaCategories/NSDictionary+Testable.m b/DarklyTests/Categories/CocoaCategories/NSDictionary+Testable.m new file mode 100644 index 00000000..ec21ba1e --- /dev/null +++ b/DarklyTests/Categories/CocoaCategories/NSDictionary+Testable.m @@ -0,0 +1,32 @@ +// +// NSDictionary+Testable.m +// DarklyTests +// +// Created by Mark Pokorny on 1/25/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "NSDictionary+Testable.h" +#import "LDUserModel+Testable.h" + +@implementation NSDictionary(Testable) +-(BOOL)boolValueForKey:(NSString*)key { + return [self[key] boolValue]; +} + +-(NSInteger)integerValueForKey:(nullable NSString*)key { + return [self[key] integerValue]; +} + +-(BOOL)isEqualToUserEnvironmentUsersDictionary:(NSDictionary*)otherDictionary { + for (NSString *environmentKey in self.allKeys) { + LDUserModel *userForEnvironment = self[environmentKey]; + LDUserModel *otherUserForEnvironment = otherDictionary[environmentKey]; + if (![userForEnvironment isEqual:otherUserForEnvironment ignoringAttributes:@[kUserAttributeUpdatedAt]]) { + return NO; + } + } + return YES; +} + +@end diff --git a/DarklyTests/Categories/NSHTTPURLResponse+LaunchDarkly+Testable.h b/DarklyTests/Categories/CocoaCategories/NSHTTPURLResponse+LaunchDarkly+Testable.h similarity index 78% rename from DarklyTests/Categories/NSHTTPURLResponse+LaunchDarkly+Testable.h rename to DarklyTests/Categories/CocoaCategories/NSHTTPURLResponse+LaunchDarkly+Testable.h index a69c71bf..9c629397 100644 --- a/DarklyTests/Categories/NSHTTPURLResponse+LaunchDarkly+Testable.h +++ b/DarklyTests/Categories/CocoaCategories/NSHTTPURLResponse+LaunchDarkly+Testable.h @@ -2,7 +2,7 @@ // NSHTTPURLResponse+LaunchDarkly+Testable.h // Darkly // -// Created by Mark Pokorny on 5/8/18. +// Created by Mark Pokorny on 5/8/18. +JMJ // Copyright © 2018 LaunchDarkly. All rights reserved. // diff --git a/DarklyTests/Categories/NSJSONSerialization+Testable.h b/DarklyTests/Categories/CocoaCategories/NSJSONSerialization+Testable.h similarity index 100% rename from DarklyTests/Categories/NSJSONSerialization+Testable.h rename to DarklyTests/Categories/CocoaCategories/NSJSONSerialization+Testable.h diff --git a/DarklyTests/Categories/NSJSONSerialization+Testable.m b/DarklyTests/Categories/CocoaCategories/NSJSONSerialization+Testable.m similarity index 100% rename from DarklyTests/Categories/NSJSONSerialization+Testable.m rename to DarklyTests/Categories/CocoaCategories/NSJSONSerialization+Testable.m diff --git a/DarklyTests/Categories/NSObject+Testable.h b/DarklyTests/Categories/CocoaCategories/NSObject+Testable.h similarity index 100% rename from DarklyTests/Categories/NSObject+Testable.h rename to DarklyTests/Categories/CocoaCategories/NSObject+Testable.h diff --git a/DarklyTests/Categories/NSObject+Testable.m b/DarklyTests/Categories/CocoaCategories/NSObject+Testable.m similarity index 100% rename from DarklyTests/Categories/NSObject+Testable.m rename to DarklyTests/Categories/CocoaCategories/NSObject+Testable.m diff --git a/Darkly/NSString+RemoveWhitespace.h b/DarklyTests/Categories/CocoaCategories/NSString+RemoveWhitespace.h similarity index 100% rename from Darkly/NSString+RemoveWhitespace.h rename to DarklyTests/Categories/CocoaCategories/NSString+RemoveWhitespace.h diff --git a/Darkly/NSString+RemoveWhitespace.m b/DarklyTests/Categories/CocoaCategories/NSString+RemoveWhitespace.m similarity index 100% rename from Darkly/NSString+RemoveWhitespace.m rename to DarklyTests/Categories/CocoaCategories/NSString+RemoveWhitespace.m diff --git a/DarklyTests/Categories/CocoaCategories/NSURLSession+LaunchDarklyTest.m b/DarklyTests/Categories/CocoaCategories/NSURLSession+LaunchDarklyTest.m new file mode 100644 index 00000000..c04185b0 --- /dev/null +++ b/DarklyTests/Categories/CocoaCategories/NSURLSession+LaunchDarklyTest.m @@ -0,0 +1,199 @@ +// +// NSURLSession+LaunchDarklyTest.m +// DarklyTests +// +// Created by Mark Pokorny on 11/19/18. +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "DarklyXCTestCase.h" +#import "LDConfig.h" +#import "NSURLSession+LaunchDarkly.h" +#import "LDURLCache.h" + +static NSString * const testMobileKey = @"com.launchdarkly.test.nsurlsession+launchdarkly"; + +@interface NSURLSession (NSURLSession_LaunchDarklyTest) +@property (strong, nonatomic, readonly) NSURLCache *urlCache; +@property (assign, nonatomic, readonly) BOOL hasLDURLCache; +@end + +@implementation NSURLSession (NSURLSession_LaunchDarklyTest) +@dynamic hasLDURLCache; +-(NSURLCache*)urlCache { + return self.configuration.URLCache; +} +@end + +@interface NSURLSession_LaunchDarklyTest : DarklyXCTestCase +@property (nonatomic, strong) LDConfig *config; +@end + +@implementation NSURLSession_LaunchDarklyTest + +-(void)setUp { + self.config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; + self.config.streaming = NO; + self.config.useReport = YES; +} + +-(void)tearDown { + +} + +-(void)testSetSharedLDSessionForConfig_streaming_get { + self.config.streaming = YES; + self.config.useReport = NO; + + [NSURLSession setSharedLDSessionForConfig:self.config]; + + XCTAssertNotNil([NSURLSession sharedLDSession]); + XCTAssertNotNil([NSURLSession sharedLDSession].urlCache); + XCTAssertFalse([NSURLSession sharedLDSession].hasLDURLCache); +} + +-(void)testSetSharedLDSessionForConfig_streaming_get_updated { + [NSURLSession setSharedLDSessionForConfig:self.config]; + NSURLSession *originalSession = [NSURLSession sharedLDSession]; + self.config.streaming = YES; + self.config.useReport = NO; + + [NSURLSession setSharedLDSessionForConfig:self.config]; + + XCTAssertNotNil([NSURLSession sharedLDSession]); + XCTAssertNotEqual([NSURLSession sharedLDSession], originalSession); + XCTAssertNotNil([NSURLSession sharedLDSession].urlCache); + XCTAssertFalse([NSURLSession sharedLDSession].hasLDURLCache); +} + +-(void)testSetSharedLDSessionForConfig_streaming_get_unchanged { + self.config.streaming = YES; + self.config.useReport = NO; + [NSURLSession setSharedLDSessionForConfig:self.config]; + NSURLSession *originalSession = [NSURLSession sharedLDSession]; + + [NSURLSession setSharedLDSessionForConfig:self.config]; + + XCTAssertNotNil([NSURLSession sharedLDSession]); + XCTAssertEqual([NSURLSession sharedLDSession], originalSession); + XCTAssertNotNil([NSURLSession sharedLDSession].urlCache); + XCTAssertFalse([NSURLSession sharedLDSession].hasLDURLCache); +} + +-(void)testSetSharedLDSessionForConfig_streaming_report { + self.config.streaming = YES; + self.config.useReport = YES; + + [NSURLSession setSharedLDSessionForConfig:self.config]; + + XCTAssertNotNil([NSURLSession sharedLDSession]); + XCTAssertNotNil([NSURLSession sharedLDSession].urlCache); + XCTAssertFalse([NSURLSession sharedLDSession].hasLDURLCache); +} + +-(void)testSetSharedLDSessionForConfig_streaming_report_updated { + [NSURLSession setSharedLDSessionForConfig:self.config]; + NSURLSession *originalSession = [NSURLSession sharedLDSession]; + self.config.streaming = YES; + self.config.useReport = YES; + + [NSURLSession setSharedLDSessionForConfig:self.config]; + + XCTAssertNotNil([NSURLSession sharedLDSession]); + XCTAssertNotEqual([NSURLSession sharedLDSession], originalSession); + XCTAssertNotNil([NSURLSession sharedLDSession].urlCache); + XCTAssertFalse([NSURLSession sharedLDSession].hasLDURLCache); +} + +-(void)testSetSharedLDSessionForConfig_streaming_report_unchanged { + self.config.streaming = YES; + self.config.useReport = YES; + [NSURLSession setSharedLDSessionForConfig:self.config]; + NSURLSession *originalSession = [NSURLSession sharedLDSession]; + + [NSURLSession setSharedLDSessionForConfig:self.config]; + + XCTAssertNotNil([NSURLSession sharedLDSession]); + XCTAssertEqual([NSURLSession sharedLDSession], originalSession); + XCTAssertNotNil([NSURLSession sharedLDSession].urlCache); + XCTAssertFalse([NSURLSession sharedLDSession].hasLDURLCache); +} + +-(void)testSetSharedLDSessionForConfig_polling_get { + self.config.streaming = NO; + self.config.useReport = NO; + + [NSURLSession setSharedLDSessionForConfig:self.config]; + + XCTAssertNotNil([NSURLSession sharedLDSession]); + XCTAssertNotNil([NSURLSession sharedLDSession].urlCache); + XCTAssertFalse([NSURLSession sharedLDSession].hasLDURLCache); +} + +-(void)testSetSharedLDSessionForConfig_polling_get_updated { + [NSURLSession setSharedLDSessionForConfig:self.config]; + NSURLSession *originalSession = [NSURLSession sharedLDSession]; + self.config.streaming = NO; + self.config.useReport = NO; + + [NSURLSession setSharedLDSessionForConfig:self.config]; + + XCTAssertNotNil([NSURLSession sharedLDSession]); + XCTAssertNotEqual([NSURLSession sharedLDSession], originalSession); + XCTAssertNotNil([NSURLSession sharedLDSession].urlCache); + XCTAssertFalse([NSURLSession sharedLDSession].hasLDURLCache); +} + +-(void)testSetSharedLDSessionForConfig_polling_get_unchanged { + self.config.streaming = NO; + self.config.useReport = NO; + [NSURLSession setSharedLDSessionForConfig:self.config]; + NSURLSession *originalSession = [NSURLSession sharedLDSession]; + + [NSURLSession setSharedLDSessionForConfig:self.config]; + + XCTAssertNotNil([NSURLSession sharedLDSession]); + XCTAssertEqual([NSURLSession sharedLDSession], originalSession); + XCTAssertNotNil([NSURLSession sharedLDSession].urlCache); + XCTAssertFalse([NSURLSession sharedLDSession].hasLDURLCache); +} + +-(void)testSetSharedLDSessionForConfig_polling_report { + self.config.streaming = NO; + self.config.useReport = YES; + + [NSURLSession setSharedLDSessionForConfig:self.config]; + + XCTAssertNotNil([NSURLSession sharedLDSession]); + XCTAssertNotNil([NSURLSession sharedLDSession].urlCache); + XCTAssertTrue([NSURLSession sharedLDSession].hasLDURLCache); +} + +-(void)testSetSharedLDSessionForConfig_polling_report_updated { + self.config.streaming = YES; + [NSURLSession setSharedLDSessionForConfig:self.config]; + NSURLSession *originalSession = [NSURLSession sharedLDSession]; + self.config.streaming = NO; + self.config.useReport = YES; + + [NSURLSession setSharedLDSessionForConfig:self.config]; + + XCTAssertNotNil([NSURLSession sharedLDSession]); + XCTAssertNotEqual([NSURLSession sharedLDSession], originalSession); + XCTAssertNotNil([NSURLSession sharedLDSession].urlCache); + XCTAssertTrue([NSURLSession sharedLDSession].hasLDURLCache); +} + +-(void)testSetSharedLDSessionForConfig_polling_report_unchanged { + [NSURLSession setSharedLDSessionForConfig:self.config]; + NSURLSession *originalSession = [NSURLSession sharedLDSession]; + + [NSURLSession setSharedLDSessionForConfig:self.config]; + + XCTAssertNotNil([NSURLSession sharedLDSession]); + XCTAssertEqual([NSURLSession sharedLDSession], originalSession); + XCTAssertNotNil([NSURLSession sharedLDSession].urlCache); + XCTAssertTrue([NSURLSession sharedLDSession].hasLDURLCache); +} + +@end diff --git a/DarklyTests/Categories/LDClient+Testable.h b/DarklyTests/Categories/LDCategories/LDClient+Testable.h similarity index 69% rename from DarklyTests/Categories/LDClient+Testable.h rename to DarklyTests/Categories/LDCategories/LDClient+Testable.h index f9a17e71..7af3d346 100644 --- a/DarklyTests/Categories/LDClient+Testable.h +++ b/DarklyTests/Categories/LDCategories/LDClient+Testable.h @@ -7,9 +7,13 @@ // #import +#import "LDDataManager.h" +#import "LDEnvironment.h" +#import "LDEnvironmentController.h" #import "LDThrottler.h" @interface LDClient(Testable) @property (nonatomic, assign) BOOL clientStarted; +@property (nonatomic, strong) LDEnvironment *primaryEnvironment; @property (nonatomic, strong) LDThrottler *throttler; @end diff --git a/DarklyTests/Categories/LDClient+Testable.m b/DarklyTests/Categories/LDCategories/LDClient+Testable.m similarity index 90% rename from DarklyTests/Categories/LDClient+Testable.m rename to DarklyTests/Categories/LDCategories/LDClient+Testable.m index 409249be..1af70867 100644 --- a/DarklyTests/Categories/LDClient+Testable.m +++ b/DarklyTests/Categories/LDCategories/LDClient+Testable.m @@ -10,5 +10,6 @@ @implementation LDClient(Testable) @dynamic clientStarted; +@dynamic primaryEnvironment; @dynamic throttler; @end diff --git a/DarklyTests/Categories/LDConfig+Testable.h b/DarklyTests/Categories/LDCategories/LDConfig+Testable.h similarity index 100% rename from DarklyTests/Categories/LDConfig+Testable.h rename to DarklyTests/Categories/LDCategories/LDConfig+Testable.h diff --git a/DarklyTests/Categories/LDConfig+Testable.m b/DarklyTests/Categories/LDCategories/LDConfig+Testable.m similarity index 100% rename from DarklyTests/Categories/LDConfig+Testable.m rename to DarklyTests/Categories/LDCategories/LDConfig+Testable.m diff --git a/DarklyTests/Categories/LDDataManager+Testable.h b/DarklyTests/Categories/LDCategories/LDDataManager+Testable.h similarity index 67% rename from DarklyTests/Categories/LDDataManager+Testable.h rename to DarklyTests/Categories/LDCategories/LDDataManager+Testable.h index a56adfcf..a83fc856 100644 --- a/DarklyTests/Categories/LDDataManager+Testable.h +++ b/DarklyTests/Categories/LDCategories/LDDataManager+Testable.h @@ -9,6 +9,10 @@ #import #import "LDDataManager.h" +extern int kUserCacheSize; + @interface LDDataManager(Testable) @property (strong, atomic) NSMutableArray *eventsArray; +@property (nonatomic, strong) dispatch_queue_t eventsQueue; +@property (nonatomic, strong) dispatch_queue_t saveUserQueue; @end diff --git a/DarklyTests/Categories/LDDataManager+Testable.m b/DarklyTests/Categories/LDCategories/LDDataManager+Testable.m similarity index 85% rename from DarklyTests/Categories/LDDataManager+Testable.m rename to DarklyTests/Categories/LDCategories/LDDataManager+Testable.m index e5ef1f5f..886ca70e 100644 --- a/DarklyTests/Categories/LDDataManager+Testable.m +++ b/DarklyTests/Categories/LDCategories/LDDataManager+Testable.m @@ -10,4 +10,6 @@ @implementation LDDataManager(Testable) @dynamic eventsArray; +@dynamic eventsQueue; +@dynamic saveUserQueue; @end diff --git a/DarklyTests/Categories/LDClientManager+EventSource.h b/DarklyTests/Categories/LDCategories/LDEnvironmentController+EventSource.h similarity index 59% rename from DarklyTests/Categories/LDClientManager+EventSource.h rename to DarklyTests/Categories/LDCategories/LDEnvironmentController+EventSource.h index b847733b..fb405637 100644 --- a/DarklyTests/Categories/LDClientManager+EventSource.h +++ b/DarklyTests/Categories/LDCategories/LDEnvironmentController+EventSource.h @@ -1,14 +1,14 @@ // -// LDClientManager+EventSource.h +// LDEnvironmentController+EventSource.h // Darkly // // Created by Mark Pokorny on 8/2/17. +JMJ // Copyright © 2017 LaunchDarkly. All rights reserved. // -#import +#import "LDEnvironmentController.h" #import "LDEventSource.h" -@interface LDClientManager (EventSource) +@interface LDEnvironmentController (EventSource) -(LDEventSource*)eventSource; @end diff --git a/DarklyTests/Categories/LDClientManager+EventSource.m b/DarklyTests/Categories/LDCategories/LDEnvironmentController+EventSource.m similarity index 56% rename from DarklyTests/Categories/LDClientManager+EventSource.m rename to DarklyTests/Categories/LDCategories/LDEnvironmentController+EventSource.m index 40f30a62..06d6aee6 100644 --- a/DarklyTests/Categories/LDClientManager+EventSource.m +++ b/DarklyTests/Categories/LDCategories/LDEnvironmentController+EventSource.m @@ -1,17 +1,17 @@ // -// LDClientManager+EventSource.m +// LDEnvironmentController+EventSource.m // Darkly // // Created by Mark Pokorny on 8/2/17. +JMJ // Copyright © 2017 LaunchDarkly. All rights reserved. // -#import "LDClientManager+EventSource.h" -@interface LDClientManager (EventSourcePrivate) +#import "LDEnvironmentController+EventSource.h" +@interface LDEnvironmentController (EventSourcePrivate) @property(nonatomic, strong, readonly) LDEventSource *eventSource; @end -@implementation LDClientManager (EventSourcePrivate) +@implementation LDEnvironmentController (EventSourcePrivate) -(LDEventSource*)activeEventSource { return self.eventSource; } diff --git a/DarklyTests/Categories/LDCategories/LDEnvironmentController+Testable.h b/DarklyTests/Categories/LDCategories/LDEnvironmentController+Testable.h new file mode 100644 index 00000000..bbb06d0e --- /dev/null +++ b/DarklyTests/Categories/LDCategories/LDEnvironmentController+Testable.h @@ -0,0 +1,16 @@ +// +// LDEnvironmentController+Testable.h +// DarklyTests +// +// Created by Mark Pokorny on 9/13/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDEnvironmentController.h" + +@interface LDEnvironmentController (Testable) +@property (nonatomic, weak) LDDataManager *dataManager; +@property (nonatomic, strong) LDUserModel *user; +@property(nonatomic, strong) LDRequestManager *requestManager; +@property(nonatomic, strong) NSDate *backgroundTime; +@end diff --git a/DarklyTests/Categories/LDCategories/LDEnvironmentController+Testable.m b/DarklyTests/Categories/LDCategories/LDEnvironmentController+Testable.m new file mode 100644 index 00000000..5e512fbb --- /dev/null +++ b/DarklyTests/Categories/LDCategories/LDEnvironmentController+Testable.m @@ -0,0 +1,16 @@ +// +// LDEnvironmentController+Testable.m +// DarklyTests +// +// Created by Mark Pokorny on 9/13/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDEnvironmentController+Testable.h" + +@implementation LDEnvironmentController (Testable) +@dynamic dataManager; +@dynamic user; +@dynamic requestManager; +@dynamic backgroundTime; +@end diff --git a/DarklyTests/Categories/LDEvent+Testable.h b/DarklyTests/Categories/LDCategories/LDEvent+Testable.h similarity index 78% rename from DarklyTests/Categories/LDEvent+Testable.h rename to DarklyTests/Categories/LDCategories/LDEvent+Testable.h index 39c8ad11..37d2855f 100644 --- a/DarklyTests/Categories/LDEvent+Testable.h +++ b/DarklyTests/Categories/LDCategories/LDEvent+Testable.h @@ -11,6 +11,7 @@ @interface LDEvent(Testable) +(nonnull instancetype)stubPingEvent; +(nonnull instancetype)stubEvent:(nonnull NSString*)eventType fromJsonFileNamed:(nonnull NSString*)fileName; ++(nonnull instancetype)stubEvent:(nonnull NSString*)eventType flagKey:(nonnull NSString*)flagKey withDataDictionary:(nonnull NSDictionary*)dataDictionary; +(nonnull instancetype)stubEvent:(nonnull NSString*)eventType withDataDictionary:(nonnull NSDictionary*)dataDictionary; +(nonnull instancetype)stubUnauthorizedEvent; +(nonnull instancetype)stubErrorEvent; diff --git a/DarklyTests/Categories/LDEvent+Testable.m b/DarklyTests/Categories/LDCategories/LDEvent+Testable.m similarity index 79% rename from DarklyTests/Categories/LDEvent+Testable.m rename to DarklyTests/Categories/LDCategories/LDEvent+Testable.m index 0a22a0ca..e7327177 100644 --- a/DarklyTests/Categories/LDEvent+Testable.m +++ b/DarklyTests/Categories/LDCategories/LDEvent+Testable.m @@ -10,7 +10,7 @@ #import "LDEvent+EventTypes.h" #import "DarklyConstants.h" #import "NSJSONSerialization+Testable.h" -#import "NSDictionary+JSON.h" +#import "NSDictionary+LaunchDarkly.h" @implementation LDEvent(Testable) +(instancetype)stubPingEvent{ @@ -28,6 +28,12 @@ +(instancetype)stubEvent:(NSString*)eventType fromJsonFileNamed:(NSString*)fileN return event; } ++(instancetype)stubEvent:(NSString*)eventType flagKey:(NSString*)flagKey withDataDictionary:(NSDictionary*)dataDictionary { + NSMutableDictionary *dataWithFlagKey = [NSMutableDictionary dictionaryWithDictionary:dataDictionary]; + dataWithFlagKey[@"key"] = flagKey; + return [LDEvent stubEvent:eventType withDataDictionary:[dataWithFlagKey copy]]; +} + +(instancetype)stubEvent:(NSString*)eventType withDataDictionary:(NSDictionary*)dataDictionary { LDEvent *event = [LDEvent new]; event.event = eventType; diff --git a/DarklyTests/Categories/LDEventModel+Testable.h b/DarklyTests/Categories/LDCategories/LDEventModel+Testable.h similarity index 100% rename from DarklyTests/Categories/LDEventModel+Testable.h rename to DarklyTests/Categories/LDCategories/LDEventModel+Testable.h diff --git a/DarklyTests/Categories/LDEventModel+Testable.m b/DarklyTests/Categories/LDCategories/LDEventModel+Testable.m similarity index 100% rename from DarklyTests/Categories/LDEventModel+Testable.m rename to DarklyTests/Categories/LDCategories/LDEventModel+Testable.m diff --git a/DarklyTests/Categories/LDFlagConfig/LDEventTrackingContext+Testable.h b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDEventTrackingContext+Testable.h similarity index 100% rename from DarklyTests/Categories/LDFlagConfig/LDEventTrackingContext+Testable.h rename to DarklyTests/Categories/LDCategories/LDFlagConfig/LDEventTrackingContext+Testable.h diff --git a/DarklyTests/Categories/LDFlagConfig/LDEventTrackingContext+Testable.m b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDEventTrackingContext+Testable.m similarity index 100% rename from DarklyTests/Categories/LDFlagConfig/LDEventTrackingContext+Testable.m rename to DarklyTests/Categories/LDCategories/LDFlagConfig/LDEventTrackingContext+Testable.m diff --git a/DarklyTests/Categories/LDFlagConfig/LDFlagConfigModel+Testable.h b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigModel+Testable.h similarity index 74% rename from DarklyTests/Categories/LDFlagConfig/LDFlagConfigModel+Testable.h rename to DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigModel+Testable.h index 82207f22..9e0b12e3 100644 --- a/DarklyTests/Categories/LDFlagConfig/LDFlagConfigModel+Testable.h +++ b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigModel+Testable.h @@ -9,6 +9,9 @@ #import "LDFlagConfigModel.h" @class LDEventTrackingContext; +@class LDEvent; + +extern NSString * const kLDFlagConfigModelKeyKey; @interface LDFlagConfigModel(Testable) @@ -17,8 +20,15 @@ +(instancetype)flagConfigFromJsonFileNamed:(NSString *)fileName omitKey:(NSString*)key; +(instancetype)flagConfigFromJsonFileNamed:(NSString *)fileName eventTrackingContext:(LDEventTrackingContext*)eventTrackingContext; +(instancetype)flagConfigFromJsonFileNamed:(NSString *)fileName eventTrackingContext:(LDEventTrackingContext*)eventTrackingContext omitKey:(NSString*)key; ++(instancetype)stub; ++(instancetype)stubWithAlternateValuesForFlagKeys:(NSArray*)flagKeys; ++(instancetype)stubOmittingFlagKeys:(NSArray*)flagKeys; + +(NSDictionary*)patchFromJsonFileNamed:(NSString *)fileName useVersion:(NSInteger)version; +(NSDictionary*)patchFromJsonFileNamed:(NSString *)fileName omitKey:(NSString*)key; +-(LDFlagConfigModel*)applySSEEvent:(LDEvent*)event; +(NSDictionary*)deleteFromJsonFileNamed:(NSString *)fileName useVersion:(NSInteger)version; +(NSDictionary*)deleteFromJsonFileNamed:(NSString *)fileName omitKey:(NSString*)key; +-(void)setFlagConfigValue:(LDFlagConfigValue*)flagConfigValue forKey:(NSString*)flagKey; +-(BOOL)isEmpty; @end diff --git a/DarklyTests/Categories/LDFlagConfig/LDFlagConfigModel+Testable.m b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigModel+Testable.m similarity index 59% rename from DarklyTests/Categories/LDFlagConfig/LDFlagConfigModel+Testable.m rename to DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigModel+Testable.m index b3e91d28..dd7d555a 100644 --- a/DarklyTests/Categories/LDFlagConfig/LDFlagConfigModel+Testable.m +++ b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigModel+Testable.m @@ -6,13 +6,15 @@ // Copyright © 2017 LaunchDarkly. All rights reserved. // -#import "LDFlagConfigModel.h" -#import "LDFlagConfigValue.h" #import "LDFlagConfigModel+Testable.h" +#import "LDFlagConfigValue.h" +#import "LDFlagConfigValue+Testable.h" #import "LDFlagConfigTracker+Testable.h" #import "LDEventTrackingContext.h" #import "LDEventTrackingContext+Testable.h" #import "NSJSONSerialization+Testable.h" +#import "LDEventSource.h" +#import "LDEvent+EventTypes.h" @implementation LDFlagConfigModel(Testable) @@ -72,6 +74,34 @@ +(instancetype)flagConfigFromJsonFileNamed:(NSString *)fileName eventTrackingCon return flagConfigModel; } ++(instancetype)stub { + return [LDFlagConfigModel stubOmittingFlagKeys:nil]; +} + ++(instancetype)stubWithAlternateValuesForFlagKeys:(NSArray*)alternateValueFlagKeys { + NSMutableDictionary *featureFlags = [NSMutableDictionary dictionaryWithCapacity:[LDFlagConfigValue flagKeys].count]; + for (NSString *flagKey in [LDFlagConfigValue flagKeys]) { + featureFlags[flagKey] = [LDFlagConfigValue stubForFlagKey:flagKey useAlternateValue:[alternateValueFlagKeys containsObject:flagKey]]; + } + LDFlagConfigModel *flagConfigModel = [[LDFlagConfigModel alloc] init]; + flagConfigModel.featuresJsonDictionary = [featureFlags copy]; + + return flagConfigModel; +} + ++(instancetype)stubOmittingFlagKeys:(NSArray*)omittedFlagKeys { + NSMutableArray *includedFlagKeys = [NSMutableArray arrayWithArray:[LDFlagConfigValue flagKeys]]; + [includedFlagKeys removeObjectsInArray:omittedFlagKeys]; + NSMutableDictionary *featureFlags = [NSMutableDictionary dictionaryWithCapacity:includedFlagKeys.count]; + for (NSString *flagKey in [includedFlagKeys copy]) { + featureFlags[flagKey] = [LDFlagConfigValue stubForFlagKey:flagKey]; + } + LDFlagConfigModel *flagConfigModel = [[LDFlagConfigModel alloc] init]; + flagConfigModel.featuresJsonDictionary = [featureFlags copy]; + + return flagConfigModel; +} + +(NSDictionary*)patchFromJsonFileNamed:(NSString *)fileName useVersion:(NSInteger)version { NSMutableDictionary *patch = [NSMutableDictionary dictionaryWithDictionary:[NSJSONSerialization jsonObjectFromFileNamed:fileName]]; patch[kLDFlagConfigValueKeyVersion] = @(version); @@ -86,6 +116,26 @@ +(NSDictionary*)patchFromJsonFileNamed:(NSString *)fileName omitKey:(NSString*)k return patch; } +-(LDFlagConfigModel*)applySSEEvent:(LDEvent*)event { + NSDictionary *eventDictionary = [NSJSONSerialization JSONObjectWithData:[event.data dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:nil]; + LDFlagConfigModel *targetFlagConfig = [self copy]; + if ([event.event isEqualToString:kLDEventTypePut]) { + targetFlagConfig = [[LDFlagConfigModel alloc] initWithDictionary:eventDictionary]; + } + if ([event.event isEqualToString:kLDEventTypePatch]) { + NSString *flagKey = eventDictionary[kLDFlagConfigModelKeyKey]; + LDFlagConfigValue *targetFlagConfigValue = [LDFlagConfigValue flagConfigValueWithObject:eventDictionary]; + [targetFlagConfig setFlagConfigValue:targetFlagConfigValue forKey:flagKey]; + } + if ([event.event isEqualToString:kLDEventTypeDelete]) { + NSString *flagKey = eventDictionary[kLDFlagConfigModelKeyKey]; + NSMutableDictionary *flagConfigValues = [NSMutableDictionary dictionaryWithDictionary:self.featuresJsonDictionary]; + [flagConfigValues removeObjectForKey:flagKey]; + targetFlagConfig.featuresJsonDictionary = [flagConfigValues copy]; + } + return targetFlagConfig; +} + +(NSDictionary*)deleteFromJsonFileNamed:(NSString *)fileName useVersion:(NSInteger)version { return [LDFlagConfigModel patchFromJsonFileNamed:fileName useVersion:version]; } @@ -93,4 +143,14 @@ +(NSDictionary*)deleteFromJsonFileNamed:(NSString *)fileName useVersion:(NSInteg +(NSDictionary*)deleteFromJsonFileNamed:(NSString *)fileName omitKey:(NSString*)key { return [LDFlagConfigModel patchFromJsonFileNamed:fileName omitKey:key]; } + +-(void)setFlagConfigValue:(LDFlagConfigValue*)flagConfigValue forKey:(NSString*)flagKey { + NSMutableDictionary *featureFlags = [NSMutableDictionary dictionaryWithDictionary:self.featuresJsonDictionary]; + featureFlags[flagKey] = flagConfigValue; + self.featuresJsonDictionary = [featureFlags copy]; +} + +-(BOOL)isEmpty { + return self.featuresJsonDictionary.count == 0; +} @end diff --git a/DarklyTests/Categories/LDFlagConfig/LDFlagConfigTracker+Testable.h b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigTracker+Testable.h similarity index 100% rename from DarklyTests/Categories/LDFlagConfig/LDFlagConfigTracker+Testable.h rename to DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigTracker+Testable.h diff --git a/DarklyTests/Categories/LDFlagConfig/LDFlagConfigTracker+Testable.m b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigTracker+Testable.m similarity index 100% rename from DarklyTests/Categories/LDFlagConfig/LDFlagConfigTracker+Testable.m rename to DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigTracker+Testable.m diff --git a/DarklyTests/Categories/LDFlagConfig/LDFlagConfigValue+Testable.h b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigValue+Testable.h similarity index 85% rename from DarklyTests/Categories/LDFlagConfig/LDFlagConfigValue+Testable.h rename to DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigValue+Testable.h index ea3d4780..8f0a0c6c 100644 --- a/DarklyTests/Categories/LDFlagConfig/LDFlagConfigValue+Testable.h +++ b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigValue+Testable.h @@ -26,12 +26,17 @@ extern NSString * const kLDFlagConfigValueKeyVariation; @interface LDFlagConfigValue(Testable) +(NSDictionary*)flagConfigJsonObjectFromFileNamed:(NSString*)fileName flagKey:(NSString*)flagKey eventTrackingContext:(LDEventTrackingContext*)eventTrackingContext; +(instancetype)flagConfigValueFromJsonFileNamed:(NSString*)fileName flagKey:(NSString*)flagKey eventTrackingContext:(LDEventTrackingContext*)eventTrackingContext; ++(instancetype)stubForFlagKey:(NSString*)flagKey; ++(instancetype)stubForFlagKey:(NSString*)flagKey useAlternateValue:(BOOL)useAlternateValue; +(NSArray*)stubFlagConfigValuesForFlagKey:(NSString*)flagKey; +(NSArray*)stubFlagConfigValuesForFlagKey:(NSString*)flagKey eventTrackingContext:(LDEventTrackingContext*)eventTrackingContext; +(NSArray*)stubFlagConfigValuesForFlagKey:(NSString*)flagKey includeFlagVersion:(BOOL)includeFlagVersion; +(NSArray*)stubFlagConfigValuesForFlagKey:(NSString*)flagKey eventTrackingContext:(LDEventTrackingContext*)eventTrackingContext includeFlagVersion:(BOOL)includeFlagVersion; +(NSArray*)fixtureFileNamesForFlagKey:(NSString*)flagKey; +(id)defaultValueForFlagKey:(NSString*)flagKey; ++(NSInteger)variationWithAlternateValue:(BOOL)useAlternateValue; ++(NSInteger)modelVersionWithAlternateValue:(BOOL)useAlternateValue; ++(NSNumber*)flagVersionWithAlternateValue:(BOOL)useAlternateValue; +(NSArray*)flagKeys; +(NSDictionary*>*)flagConfigValues; -(NSDictionary*)dictionaryValueIncludeContext:(BOOL)includeContext; diff --git a/DarklyTests/Categories/LDFlagConfig/LDFlagConfigValue+Testable.m b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigValue+Testable.m similarity index 86% rename from DarklyTests/Categories/LDFlagConfig/LDFlagConfigValue+Testable.m rename to DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigValue+Testable.m index 10fc9bf4..1718e9bc 100644 --- a/DarklyTests/Categories/LDFlagConfig/LDFlagConfigValue+Testable.m +++ b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagConfigValue+Testable.m @@ -42,6 +42,20 @@ +(instancetype)flagConfigValueFromJsonFileNamed:(NSString*)fileName flagKey:(NSS return flagConfigValue; } ++(instancetype)stubForFlagKey:(NSString*)flagKey { + return [LDFlagConfigValue stubForFlagKey:flagKey useAlternateValue:NO]; +} + ++(instancetype)stubForFlagKey:(NSString*)flagKey useAlternateValue:(BOOL)useAlternateValue { + LDFlagConfigValue *flagConfigValue = [[LDFlagConfigValue alloc] init]; + flagConfigValue.value = useAlternateValue ? [LDFlagConfigValue differentValueForFlagKey:flagKey] : [LDFlagConfigValue defaultValueForFlagKey:flagKey]; + flagConfigValue.variation = [LDFlagConfigValue variationWithAlternateValue:useAlternateValue]; + flagConfigValue.modelVersion = [LDFlagConfigValue modelVersionWithAlternateValue:useAlternateValue]; + flagConfigValue.flagVersion = [LDFlagConfigValue flagVersionWithAlternateValue:useAlternateValue]; + + return flagConfigValue; +} + +(NSArray*)stubFlagConfigValuesForFlagKey:(NSString*)flagKey { return [LDFlagConfigValue stubFlagConfigValuesForFlagKey:flagKey eventTrackingContext:[LDEventTrackingContext stub] includeFlagVersion:YES]; } @@ -142,6 +156,18 @@ +(id)differentValueForFlagKey:(NSString*)flagKey { return @(YES); } ++(NSInteger)variationWithAlternateValue:(BOOL)useAlternateValue { + return useAlternateValue ? 3 : 2; +} + ++(NSInteger)modelVersionWithAlternateValue:(BOOL)useAlternateValue { + return useAlternateValue ? 3 : 2; +} + ++(NSNumber*)flagVersionWithAlternateValue:(BOOL)useAlternateValue { + return useAlternateValue ? @(2) : @(1); +} + +(NSArray*)flagKeys { return @[kLDFlagKeyIsABool, kLDFlagKeyIsANumber, kLDFlagKeyIsADouble, kLDFlagKeyIsAString, kLDFlagKeyIsAnArray, kLDFlagKeyIsADictionary, kLDFlagKeyIsANull]; } diff --git a/DarklyTests/Categories/LDFlagConfig/LDFlagCounter+Testable.h b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagCounter+Testable.h similarity index 100% rename from DarklyTests/Categories/LDFlagConfig/LDFlagCounter+Testable.h rename to DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagCounter+Testable.h diff --git a/DarklyTests/Categories/LDFlagConfig/LDFlagCounter+Testable.m b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagCounter+Testable.m similarity index 100% rename from DarklyTests/Categories/LDFlagConfig/LDFlagCounter+Testable.m rename to DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagCounter+Testable.m diff --git a/DarklyTests/Categories/LDFlagConfig/LDFlagValueCounter+Testable.h b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagValueCounter+Testable.h similarity index 100% rename from DarklyTests/Categories/LDFlagConfig/LDFlagValueCounter+Testable.h rename to DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagValueCounter+Testable.h diff --git a/DarklyTests/Categories/LDFlagConfig/LDFlagValueCounter+Testable.m b/DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagValueCounter+Testable.m similarity index 100% rename from DarklyTests/Categories/LDFlagConfig/LDFlagValueCounter+Testable.m rename to DarklyTests/Categories/LDCategories/LDFlagConfig/LDFlagValueCounter+Testable.m diff --git a/DarklyTests/Categories/LDCategories/LDRequestManager+Testable.h b/DarklyTests/Categories/LDCategories/LDRequestManager+Testable.h new file mode 100644 index 00000000..ee319d56 --- /dev/null +++ b/DarklyTests/Categories/LDCategories/LDRequestManager+Testable.h @@ -0,0 +1,15 @@ +// +// LDRequestManager+Testable.h +// DarklyTests +// +// Created by Mark Pokorny on 9/13/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDRequestManager.h" + +@interface LDRequestManager (Testable) +@property (nonnull, nonatomic) LDConfig *config; +@property (nullable, nonatomic, strong) dispatch_queue_t callbackQueue; +@property (nullable, copy, nonatomic) NSString *featureFlagEtag; +@end diff --git a/DarklyTests/Categories/LDCategories/LDRequestManager+Testable.m b/DarklyTests/Categories/LDCategories/LDRequestManager+Testable.m new file mode 100644 index 00000000..326ee8f7 --- /dev/null +++ b/DarklyTests/Categories/LDCategories/LDRequestManager+Testable.m @@ -0,0 +1,15 @@ +// +// LDRequestManager+Testable.m +// DarklyTests +// +// Created by Mark Pokorny on 9/13/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDRequestManager+Testable.h" + +@implementation LDRequestManager (Testable) +@dynamic config; +@dynamic callbackQueue; +@dynamic featureFlagEtag; +@end diff --git a/DarklyTests/Categories/LDThrottler+Testable.h b/DarklyTests/Categories/LDCategories/LDThrottler+Testable.h similarity index 100% rename from DarklyTests/Categories/LDThrottler+Testable.h rename to DarklyTests/Categories/LDCategories/LDThrottler+Testable.h diff --git a/DarklyTests/Categories/LDThrottler+Testable.m b/DarklyTests/Categories/LDCategories/LDThrottler+Testable.m similarity index 100% rename from DarklyTests/Categories/LDThrottler+Testable.m rename to DarklyTests/Categories/LDCategories/LDThrottler+Testable.m diff --git a/DarklyTests/Categories/LDUserBuilder+Testable.h b/DarklyTests/Categories/LDCategories/LDUserBuilder+Testable.h similarity index 100% rename from DarklyTests/Categories/LDUserBuilder+Testable.h rename to DarklyTests/Categories/LDCategories/LDUserBuilder+Testable.h diff --git a/DarklyTests/Categories/LDUserBuilder+Testable.m b/DarklyTests/Categories/LDCategories/LDUserBuilder+Testable.m similarity index 100% rename from DarklyTests/Categories/LDUserBuilder+Testable.m rename to DarklyTests/Categories/LDCategories/LDUserBuilder+Testable.m diff --git a/DarklyTests/Categories/LDCategories/LDUserEnvironment+Testable.h b/DarklyTests/Categories/LDCategories/LDUserEnvironment+Testable.h new file mode 100644 index 00000000..836af054 --- /dev/null +++ b/DarklyTests/Categories/LDCategories/LDUserEnvironment+Testable.h @@ -0,0 +1,30 @@ +// +// LDUserEnvironment+Testable.h +// DarklyTests +// +// Created by Mark Pokorny on 10/12/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDUserEnvironment.h" + +extern NSString *const kEnvironmentKeyPrimary; +extern NSString *const kEnvironmentKeySecondaryB; +extern NSString *const kEnvironmentKeySecondaryC; +extern NSString *const kEnvironmentKeySecondaryD; +extern NSString *const kEnvironmentKeySecondaryE; + +@interface LDUserEnvironment (Testable) +@property (nonatomic, strong) NSString *userKey; +@property (nonatomic, strong) NSDictionary *users; +@property (nonatomic, strong, class, readonly) NSArray *environmentKeys; +@property (nonatomic, strong, class, readonly) NSDictionary *flagConfigFilenames; + ++(NSDictionary*)stubUserModelsForUserWithKey:(NSString*)userKey environmentKeys:(NSArray*)environmentKeys; ++(NSArray*)environmentKeys; ++(NSDictionary*)flagConfigFilenames; ++(NSDictionary*)stubUserEnvironmentsForUsersWithKeys:(NSArray*)userKeys; ++(NSDictionary*)stubUserEnvironmentsForUsersWithKeys:(NSArray*)userKeys mobileKeys:(NSArray*)mobileKeys; +-(BOOL)isEqualToUserEnvironment:(LDUserEnvironment*)otherUserEnvironment; + +@end diff --git a/DarklyTests/Categories/LDCategories/LDUserEnvironment+Testable.m b/DarklyTests/Categories/LDCategories/LDUserEnvironment+Testable.m new file mode 100644 index 00000000..a4c48d84 --- /dev/null +++ b/DarklyTests/Categories/LDCategories/LDUserEnvironment+Testable.m @@ -0,0 +1,75 @@ +// +// LDUserEnvironment+Testable.m +// DarklyTests +// +// Created by Mark Pokorny on 10/12/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDUserEnvironment+Testable.h" +#import "LDUserModel+Testable.h" +#import "LDEventTrackingContext.h" +#import "LDFlagConfigModel+Testable.h" + +NSString *const kEnvironmentKeyPrimary = @"com.launchdarkly.DarklyTest.mobileKey.A"; +NSString *const kEnvironmentKeySecondaryB = @"com.launchdarkly.DarklyTest.mobileKey.B"; +NSString *const kEnvironmentKeySecondaryC = @"com.launchdarkly.DarklyTest.mobileKey.C"; +NSString *const kEnvironmentKeySecondaryD = @"com.launchdarkly.DarklyTest.mobileKey.D"; +NSString *const kEnvironmentKeySecondaryE = @"com.launchdarkly.DarklyTest.mobileKey.E"; + +@implementation LDUserEnvironment (Testable) +@dynamic userKey; +@dynamic users; + ++(NSDictionary*)stubUserModelsForUserWithKey:(NSString*)userKey environmentKeys:(NSArray*)environmentKeys { + NSMutableDictionary *users = [NSMutableDictionary dictionaryWithCapacity:environmentKeys.count]; + LDUserModel *baseUser = [LDUserModel stubWithKey:userKey]; + for (NSString *environmentKey in environmentKeys) { + LDUserModel *userWithFlagConfigForEnvironment = [baseUser copy]; + LDFlagConfigModel *flagConfigForEnvironment = [LDFlagConfigModel flagConfigFromJsonFileNamed:LDUserEnvironment.flagConfigFilenames[environmentKey] + eventTrackingContext:[[LDEventTrackingContext alloc] init]]; + userWithFlagConfigForEnvironment.flagConfig = flagConfigForEnvironment; + users[environmentKey] = userWithFlagConfigForEnvironment; + } + + return [users copy]; +} + ++(NSArray*)environmentKeys { + return @[kEnvironmentKeyPrimary, kEnvironmentKeySecondaryB, kEnvironmentKeySecondaryC, kEnvironmentKeySecondaryD, kEnvironmentKeySecondaryE]; +} + ++(NSDictionary*)flagConfigFilenames { + return @{kEnvironmentKeyPrimary: @"featureFlags", + kEnvironmentKeySecondaryB: @"emptyConfig", + kEnvironmentKeySecondaryC: @"ldEnvironmentControllerTestConfigA", + kEnvironmentKeySecondaryD: @"ldFlagConfigModelTest", + kEnvironmentKeySecondaryE: @"dictionaryConfigIsADictionary-3Key"}; +} + ++(NSDictionary*)stubUserEnvironmentsForUsersWithKeys:(NSArray*)userKeys { + return [LDUserEnvironment stubUserEnvironmentsForUsersWithKeys:userKeys mobileKeys:LDUserEnvironment.environmentKeys]; +} + ++(NSDictionary*)stubUserEnvironmentsForUsersWithKeys:(NSArray*)userKeys mobileKeys:(NSArray*)mobileKeys { + NSMutableDictionary *userEnvironments = [NSMutableDictionary dictionaryWithCapacity:userKeys.count]; + for (NSString *userKey in userKeys) { + NSDictionary *userModels = [LDUserEnvironment stubUserModelsForUserWithKey:userKey environmentKeys:mobileKeys]; + userEnvironments[userKey] = [LDUserEnvironment userEnvironmentForUserWithKey:userKey environments:userModels]; + } + + return userEnvironments; +} + +-(BOOL)isEqualToUserEnvironment:(LDUserEnvironment*)otherUserEnvironment { + if (![self.userKey isEqualToString:otherUserEnvironment.userKey]) { return NO; } + if (self.users.count != otherUserEnvironment.users.count) { return NO; } + for (NSString *environmentKey in self.users.allKeys) { + LDUserModel *userForEnvironment = self.users[environmentKey]; + LDUserModel *otherUserForEnvironment = otherUserEnvironment.users[environmentKey]; + if (![userForEnvironment isEqual:otherUserForEnvironment ignoringAttributes:@[kUserAttributeUpdatedAt]]) { return NO; } + } + return YES; +} + +@end diff --git a/DarklyTests/Categories/LDUserModel+Testable.h b/DarklyTests/Categories/LDCategories/LDUserModel+Testable.h similarity index 92% rename from DarklyTests/Categories/LDUserModel+Testable.h rename to DarklyTests/Categories/LDCategories/LDUserModel+Testable.h index 44b2d918..3dcc4464 100644 --- a/DarklyTests/Categories/LDUserModel+Testable.h +++ b/DarklyTests/Categories/LDCategories/LDUserModel+Testable.h @@ -6,7 +6,7 @@ // Copyright © 2018 LaunchDarkly. All rights reserved. // -#import +#import "LDUserModel.h" @class LDEventTrackingContext; @@ -37,8 +37,10 @@ extern NSString * const kFlagKeyIsABawler; +(instancetype)stubWithKey:(NSString*)key; +(instancetype)stubWithKey:(NSString*)key usingTracker:(LDFlagConfigTracker*)tracker eventTrackingContext:(LDEventTrackingContext*)eventTrackingContext; ++(NSDictionary*)stubUsersWithKeys:(NSArray*)keys; +(NSDictionary*)customStub; +(LDUserModel*)userFrom:(NSString*)jsonUser; ++(NSArray*)stubUserKeysWithCount:(NSUInteger)count; /** -[LDUserModel dictionaryValueWithFlags: includePrivateAttributes: config:] intentionally omits the private attributes LIST from the dictionary when includePrivateAttributes == YES to satisfy an LD server requirement. This method allows control over including that list for testing. */ diff --git a/DarklyTests/Categories/LDUserModel+Testable.m b/DarklyTests/Categories/LDCategories/LDUserModel+Testable.m similarity index 95% rename from DarklyTests/Categories/LDUserModel+Testable.m rename to DarklyTests/Categories/LDCategories/LDUserModel+Testable.m index 5aa5af03..5b50d939 100644 --- a/DarklyTests/Categories/LDUserModel+Testable.m +++ b/DarklyTests/Categories/LDCategories/LDUserModel+Testable.m @@ -54,6 +54,15 @@ +(instancetype)stubWithKey:(NSString*)key usingTracker:(LDFlagConfigTracker*)tra return stub; } ++(NSDictionary*)stubUsersWithKeys:(NSArray*)keys { + NSMutableDictionary *stubbedUsers = [NSMutableDictionary dictionaryWithCapacity:keys.count]; + for (NSString *key in keys) { + stubbedUsers[key] = [LDUserModel stubWithKey:key]; + } + + return [stubbedUsers copy]; +} + +(NSDictionary*)customStub { //If you add new values that are non-string type, you might need to add the type to //-[LDUserModel+Equatable matchesDictionary: includeFlags: includePrivateAttributes: privateAttributes:] to handle the new type. @@ -67,6 +76,14 @@ +(LDUserModel*)userFrom:(NSString*)jsonUser { return [[LDUserModel alloc] initWithDictionary:userDictionary]; } ++(NSArray*)stubUserKeysWithCount:(NSUInteger)count { + NSMutableArray *userKeys = [NSMutableArray arrayWithCapacity:count]; + for (NSUInteger index = 0; index < count; index++) { + [userKeys addObject:[[NSUUID UUID] UUIDString]]; + } + return [userKeys copy]; +} + -(NSDictionary *)dictionaryValueWithFlags:(BOOL)includeFlags includePrivateAttributes:(BOOL)includePrivate config:(LDConfig*)config includePrivateAttributeList:(BOOL)includePrivateList { NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithDictionary:[self dictionaryValueWithFlagConfig:includeFlags includePrivateAttributes:includePrivate config:config]]; dictionary[kUserAttributePrivateAttributes] = includePrivateList ? self.privateAttributes : nil; diff --git a/DarklyTests/Categories/LDRequestManager+Testable.h b/DarklyTests/Categories/LDRequestManager+Testable.h new file mode 100644 index 00000000..7ff1304e --- /dev/null +++ b/DarklyTests/Categories/LDRequestManager+Testable.h @@ -0,0 +1,13 @@ +// +// LDRequestManager+Testable.h +// DarklyTests +// +// Created by Mark Pokorny on 9/13/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import + +@interface LDRequestManager (Testable) +@property (nonnull, nonatomic) LDConfig *config; +@end diff --git a/DarklyTests/Categories/LDRequestManager+Testable.m b/DarklyTests/Categories/LDRequestManager+Testable.m new file mode 100644 index 00000000..52a207cb --- /dev/null +++ b/DarklyTests/Categories/LDRequestManager+Testable.m @@ -0,0 +1,13 @@ +// +// LDRequestManager+Testable.m +// DarklyTests +// +// Created by Mark Pokorny on 9/13/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDRequestManager+Testable.h" + +@implementation LDRequestManager (Testable) +@dynamic config; +@end diff --git a/DarklyTests/Categories/NSDictionary+Testable.m b/DarklyTests/Categories/NSDictionary+Testable.m deleted file mode 100644 index 674c83e2..00000000 --- a/DarklyTests/Categories/NSDictionary+Testable.m +++ /dev/null @@ -1,19 +0,0 @@ -// -// NSDictionary+Testable.m -// DarklyTests -// -// Created by Mark Pokorny on 1/25/18. +JMJ -// Copyright © 2018 LaunchDarkly. All rights reserved. -// - -#import "NSDictionary+Testable.h" - -@implementation NSDictionary(Testable) --(BOOL)boolValueForKey:(NSString*)key { - return [self[key] boolValue]; -} - --(NSInteger)integerValueForKey:(nullable NSString*)key { - return [self[key] integerValue]; -} -@end diff --git a/DarklyTests/DarklyXCTestCase.h b/DarklyTests/DarklyXCTestCase.h index d654ed75..248d89ab 100644 --- a/DarklyTests/DarklyXCTestCase.h +++ b/DarklyTests/DarklyXCTestCase.h @@ -6,5 +6,5 @@ #import "DarklyConstants.h" @interface DarklyXCTestCase : XCTestCase - +@property (nonatomic, strong) void (^cleanup)(void); @end diff --git a/DarklyTests/DarklyXCTestCase.m b/DarklyTests/DarklyXCTestCase.m index 33c9f5ba..ae3dfefe 100644 --- a/DarklyTests/DarklyXCTestCase.m +++ b/DarklyTests/DarklyXCTestCase.m @@ -16,6 +16,7 @@ - (void)setUp { } - (void)tearDown { + if (self.cleanup) { self.cleanup(); } [super tearDown]; } diff --git a/DarklyTests/Fixtures/featureFlags.json b/DarklyTests/Fixtures/featureFlags.json index 32e7b994..8b93291b 100644 --- a/DarklyTests/Fixtures/featureFlags.json +++ b/DarklyTests/Fixtures/featureFlags.json @@ -26,7 +26,7 @@ "version": 2, "flagVersion": 1 }, - "isAArray": { + "isAnArray": { "value": [0, 1, 2], "variation": 2, "version": 2, @@ -57,5 +57,29 @@ "variation": 2, "version": 2, "flagVersion": 1 + }, + "isABool": { + "value": false, + "variation": 2, + "version": 2, + "flagVersion": 1 + }, + "isADouble": { + "value": 3.141592653589793238462643383279502884197169399375105820974944592307816406286, + "variation": 2, + "version": 2, + "flagVersion": 1 + }, + "isADictionary": { + "value": { + "keyA": true, + "keyB": [1, 2, 3], + "keyC": { + "keyD": "someStringValue" + } + }, + "variation": 2, + "version": 2, + "flagVersion": 1 } } diff --git a/DarklyTests/Fixtures/ldClientManagerTestConfigA.json b/DarklyTests/Fixtures/ldEnvironmentControllerTestConfigA.json similarity index 100% rename from DarklyTests/Fixtures/ldClientManagerTestConfigA.json rename to DarklyTests/Fixtures/ldEnvironmentControllerTestConfigA.json diff --git a/DarklyTests/Fixtures/ldClientManagerTestConfigB.json b/DarklyTests/Fixtures/ldEnvironmentControllerTestConfigB.json similarity index 100% rename from DarklyTests/Fixtures/ldClientManagerTestConfigB.json rename to DarklyTests/Fixtures/ldEnvironmentControllerTestConfigB.json diff --git a/DarklyTests/Fixtures/ldClientManagerTestDeleteIsANumber.json b/DarklyTests/Fixtures/ldEnvironmentControllerTestDeleteIsANumber.json similarity index 100% rename from DarklyTests/Fixtures/ldClientManagerTestDeleteIsANumber.json rename to DarklyTests/Fixtures/ldEnvironmentControllerTestDeleteIsANumber.json diff --git a/DarklyTests/Fixtures/ldClientManagerTestPatchIsANumber.json b/DarklyTests/Fixtures/ldEnvironmentControllerTestPatchIsANumber.json similarity index 100% rename from DarklyTests/Fixtures/ldClientManagerTestPatchIsANumber.json rename to DarklyTests/Fixtures/ldEnvironmentControllerTestPatchIsANumber.json diff --git a/DarklyTests/LDClientManagerTest.m b/DarklyTests/LDClientManagerTest.m deleted file mode 100644 index 796b6c05..00000000 --- a/DarklyTests/LDClientManagerTest.m +++ /dev/null @@ -1,1346 +0,0 @@ -// -// Copyright © 2015 Catamorphic Co. All rights reserved. -// - -#import "DarklyXCTestCase.h" -#import "LDClientManager.h" -#import "LDUserBuilder.h" -#import "LDClient.h" -#import "OCMock.h" -#import "LDRequestManager.h" -#import "LDDataManager.h" -#import "LDPollingManager.h" -#import "LDEventModel.h" -#import "LDEventModel+Testable.h" -#import "LDClientManager+EventSource.h" -#import "LDEvent+Testable.h" -#import "LDEvent+EventTypes.h" -#import "LDFlagConfigModel+Testable.h" -#import "LDFlagConfigValue.h" -#import "LDFlagConfigValue+Testable.h" -#import "LDEventTrackingContext.h" -#import "LDEventTrackingContext+Testable.h" -#import "LDUserModel+Testable.h" -#import "NSJSONSerialization+Testable.h" -#import "LDFlagConfigTracker+Testable.h" -#import "NSDate+ReferencedDate.h" -#import "NSDate+Testable.h" -#import "NSDateFormatter+JsonHeader+Testable.h" -#import "NSDate+ReferencedDate.h" - -extern NSString * _Nonnull const kLDFlagConfigValueKeyValue; -extern NSString * _Nonnull const kLDFlagConfigValueKeyVersion; -extern NSString * _Nonnull const kLDClientManagerStreamMethod; - -NSString *const mockMobileKey = @"mockMobileKey"; -NSString *const kFeaturesJsonDictionary = @"featuresJsonDictionary"; -NSString *const kBoolFlagKey = @"isABawler"; - -@interface LDClientManagerTest : DarklyXCTestCase -@property (nonatomic) id requestManagerMock; -@property (nonatomic) id ldClientMock; -@property (nonatomic) id dataManagerMock; -@property (nonatomic) id pollingManagerMock; -@property (nonatomic) id eventSourceMock; -@property (nonatomic, strong) void (^cleanup)(void); -@end - -@implementation LDClientManagerTest - -- (void)setUp { - [super setUp]; - - LDUserModel *user = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; - - LDConfig *config = [[LDConfig alloc] initWithMobileKey:mockMobileKey]; - - self.ldClientMock = [self mockClientWithUser:user]; - OCMStub(ClassMethod([self.ldClientMock sharedInstance])).andReturn(self.ldClientMock); - [[[self.ldClientMock expect] andReturn: config] ldConfig]; - [[[self.ldClientMock expect] andReturn: user] ldUser]; - - self.requestManagerMock = OCMClassMock([LDRequestManager class]); - OCMStub(ClassMethod([self.requestManagerMock sharedInstance])).andReturn(self.requestManagerMock); - - self.dataManagerMock = OCMClassMock([LDDataManager class]); - OCMStub(ClassMethod([self.dataManagerMock sharedManager])).andReturn(self.dataManagerMock); - - self.pollingManagerMock = OCMClassMock([LDPollingManager class]); - OCMStub(ClassMethod([self.pollingManagerMock sharedInstance])).andReturn(self.pollingManagerMock); - - self.eventSourceMock = OCMClassMock([LDEventSource class]); - OCMStub(ClassMethod([self.eventSourceMock eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]])).andReturn(self.eventSourceMock); -} - -- (void)tearDown { - if (self.cleanup) { self.cleanup(); } - [LDClientManager sharedInstance].online = NO; - [self.ldClientMock stopMocking]; - [self.requestManagerMock stopMocking]; - [self.dataManagerMock stopMocking]; - [self.pollingManagerMock stopMocking]; - [self.eventSourceMock stopMocking]; - self.cleanup = nil; - self.ldClientMock = nil; - self.requestManagerMock = nil; - self.dataManagerMock = nil; - self.pollingManagerMock = nil; - self.eventSourceMock = nil; - [super tearDown]; -} - -- (void)testEventSourceConfiguredToConnectUsingGetMethod { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - LDUserModel *user = [LDClient sharedInstance].ldUser; - NSString *encodedUser = [LDUtil base64UrlEncodeString:[[user dictionaryValueWithPrivateAttributesAndFlagConfig:NO] jsonString]]; - - __block NSURL *streamUrl; - __block NSString *streamConnectMethod; - __block NSData *streamConnectData; - OCMVerify([self.eventSourceMock eventSourceWithURL:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[NSURL class]]) { return NO; } - streamUrl = obj; - return YES; - }] httpHeaders:[OCMArg any] connectMethod:[OCMArg checkWithBlock:^BOOL(id obj) { - if (obj && ![obj isKindOfClass:[NSString class]]) { return NO; } - streamConnectMethod = obj; - return YES; - }] connectBody:[OCMArg checkWithBlock:^BOOL(id obj) { - if (obj && ![obj isKindOfClass:[NSData class]]) { return NO; } - streamConnectData = obj; - return YES; - }]]); - XCTAssertTrue([[streamUrl pathComponents] containsObject:kLDClientManagerStreamMethod]); - XCTAssertFalse([[streamUrl pathComponents] containsObject:@"mping"]); - XCTAssertTrue([[streamUrl lastPathComponent] isEqualToString:encodedUser]); - XCTAssertTrue(streamConnectMethod == nil || [streamConnectMethod isEqualToString:@"GET"]); - XCTAssertNil(streamConnectData); -} - -- (void)testEventSourceConfiguredToConnectUsingReportMethod { - LDConfig *config = [LDClient sharedInstance].ldConfig; - config.useReport = YES; - LDUserModel *user = [LDClient sharedInstance].ldUser; - - [self.ldClientMock stopMocking]; - self.ldClientMock = OCMClassMock([LDClient class]); - OCMStub(ClassMethod([self.ldClientMock sharedInstance])).andReturn(self.ldClientMock); - OCMStub([self.ldClientMock ldUser]).andReturn(user); - OCMStub([self.ldClientMock ldConfig]).andReturn(config); - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - NSData *encodedUserData = [[[user dictionaryValueWithPrivateAttributesAndFlagConfig:NO] jsonString] dataUsingEncoding:NSUTF8StringEncoding]; - - __block NSURL *streamUrl; - __block NSString *streamConnectMethod; - __block NSData *streamConnectData; - OCMVerify([self.eventSourceMock eventSourceWithURL:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[NSURL class]]) { return NO; } - streamUrl = obj; - return YES; - }] httpHeaders:[OCMArg any] connectMethod:[OCMArg checkWithBlock:^BOOL(id obj) { - if (obj && ![obj isKindOfClass:[NSString class]]) { return NO; } - streamConnectMethod = obj; - return YES; - }] connectBody:[OCMArg checkWithBlock:^BOOL(id obj) { - if (obj && ![obj isKindOfClass:[NSData class]]) { return NO; } - streamConnectData = obj; - return YES; - }]]); - XCTAssertTrue([[streamUrl lastPathComponent] isEqualToString:kLDClientManagerStreamMethod]); - XCTAssertFalse([[streamUrl pathComponents] containsObject:@"mping"]); - XCTAssertTrue([streamConnectMethod isEqualToString:kHTTPMethodReport]); - XCTAssertTrue([streamConnectData isEqualToData:encodedUserData]); -} - -- (void)testEventSourceCreatedOnStartPollingWhileOnline { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - [clientManager startPolling]; - XCTAssertNotNil(clientManager.eventSource); -} - -- (void)testEventSourceNotCreatedOnStartPollingWhileOffline { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = NO; - [clientManager startPolling]; - XCTAssertNil(clientManager.eventSource); -} - -- (void)testEventSourceRemainsConstantAcrossStartPollingCalls { - int numTries = 5; - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - [clientManager startPolling]; - LDEventSource *eventSource = clientManager.eventSource; - XCTAssertNotNil(eventSource); - - for (int i = 0; i < numTries; i++) { - [clientManager startPolling]; - XCTAssert(eventSource == clientManager.eventSource); - } -} - -- (void)testEventSourceRemovedOnStopPolling { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - [clientManager startPolling]; - XCTAssertNotNil(clientManager.eventSource); - - [clientManager stopPolling]; - XCTAssertNil(clientManager.eventSource); -} - -- (void)testEventSourceCreatedOnWillEnterForeground { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - [clientManager startPolling]; - XCTAssertNotNil(clientManager.eventSource); - - [clientManager willEnterBackground]; - XCTAssertNil(clientManager.eventSource); - [clientManager willEnterForeground]; - XCTAssertNotNil(clientManager.eventSource); -} - -- (void)testEventSourceRemainsConstantAcrossWillEnterForegroundCalls { - int numTries = 5; - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - [clientManager startPolling]; - XCTAssertNotNil(clientManager.eventSource); - - LDEventSource *eventSource = [[LDClientManager sharedInstance] eventSource]; - for (int i = 0; i < numTries; i++) { - [clientManager willEnterForeground]; - XCTAssert(eventSource == clientManager.eventSource); - } -} - -- (void)testSyncWithServerForConfigWhenUserExistsAndOnline { - [[self.requestManagerMock expect] performFeatureFlagRequest:[self.ldClientMock ldUser]]; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager setOnline:YES]; - - [clientManager syncWithServerForConfig]; - - [self.requestManagerMock verify]; -} - -- (void)testSyncWithServerForConfigWhenUserDoesNotExist { - self.ldClientMock = [self mockClientWithUser:nil]; - - [[self.requestManagerMock reject] performFeatureFlagRequest:[OCMArg any]]; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager setOnline:YES]; - - [clientManager syncWithServerForConfig]; -} - -- (void)testSyncWithServerForConfigWhenOffline { - [[self.requestManagerMock reject] performFeatureFlagRequest:[OCMArg any]]; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager setOnline:NO]; - - [clientManager syncWithServerForConfig]; -} - -- (void)testSyncWithServerForEventsWhenEventsExist { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:mockMobileKey]; - NSArray *eventDictionaries = [LDEventModel stubEventDictionariesForUser:[LDClient sharedInstance].ldUser config:config]; - OCMStub([self.dataManagerMock allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { - void (^completion)(NSArray *) = obj; - completion(eventDictionaries); - return YES; - }]]); - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager setOnline:YES]; - LDMillisecond startDateMillis = [[NSDate date] millisSince1970]; - - [clientManager syncWithServerForEvents]; - - OCMVerify([self.requestManagerMock performEventRequest:[OCMArg isEqual:eventDictionaries]]); - XCTAssertNotNil([LDClient sharedInstance].ldUser.flagConfigTracker); - XCTAssertFalse([LDClient sharedInstance].ldUser.flagConfigTracker.hasTrackedEvents); - XCTAssertTrue(Approximately([LDClient sharedInstance].ldUser.flagConfigTracker.startDateMillis, startDateMillis, 10)); -} - -- (void)testDoNotSyncWithServerForEventsWhenEventsDoNotExist { - NSData *testData = nil; - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager setOnline:YES]; - - [[self.requestManagerMock reject] performEventRequest:[OCMArg isEqual:testData]]; - - [clientManager syncWithServerForEvents]; - - [self.requestManagerMock verify]; -} - -- (void)testSyncWithServerForEventsNotProcessedWhenOffline { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:mockMobileKey]; - NSArray *eventDictionaries = [LDEventModel stubEventDictionariesForUser:[LDClient sharedInstance].ldUser config:config]; - OCMStub([self.dataManagerMock allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { - void (^completion)(NSArray *) = obj; - completion(eventDictionaries); - return YES; - }]]); - [[self.requestManagerMock reject] performEventRequest:[OCMArg any]]; - [[self.dataManagerMock reject] allEventDictionaries:[OCMArg any]]; - - [[LDClientManager sharedInstance] syncWithServerForEvents]; - - [self.requestManagerMock verify]; - [self.dataManagerMock verify]; -} - -- (void)testStartPollingOnline { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - [clientManager startPolling]; - - OCMVerify([self.pollingManagerMock startEventPolling]); - OCMVerify([self.eventSourceMock onMessage:[OCMArg isNotNil]]); -} - -- (void)testStartPollingOffline { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = NO; - [clientManager startPolling]; - - OCMReject([self.pollingManagerMock startEventPolling]); - XCTAssertNil(clientManager.eventSource); -} - -- (void)testStopPolling { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager stopPolling]; - - OCMVerify([self.pollingManagerMock stopEventPolling]); - XCTAssertNil([clientManager eventSource]); -} - -- (void)testUpdateUser_offline_streaming { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:mockMobileKey]; - config.streaming = YES; - [[self.eventSourceMock reject] eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]]; - [[self.pollingManagerMock reject] startConfigPolling]; - - [[LDClientManager sharedInstance] updateUser]; - - [self.eventSourceMock verify]; - [self.pollingManagerMock verify]; -} - -- (void)testUpdateUser_offline_polling { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:mockMobileKey]; - config.streaming = NO; - [[self.eventSourceMock reject] eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]]; - [[self.pollingManagerMock reject] startConfigPolling]; - - [[LDClientManager sharedInstance] updateUser]; - - [self.eventSourceMock verify]; - [self.pollingManagerMock verify]; -} - -- (void)testUpdateUser_online_streaming { - LDConfig *config = [LDClient sharedInstance].ldConfig; - config.streaming = YES; - [[LDClientManager sharedInstance] setOnline:YES]; - //The eventSourceMock registered the eventSourceWithURL call from the setOnline call above. Replace it so it can measure the updateUser response - [self.eventSourceMock stopMocking]; - self.eventSourceMock = OCMClassMock([LDEventSource class]); - OCMStub(ClassMethod([self.eventSourceMock eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]])).andReturn(self.eventSourceMock); - [[self.eventSourceMock expect] close]; - [[self.pollingManagerMock expect] stopConfigPolling]; - [[self.pollingManagerMock reject] startConfigPolling]; - - [[LDClientManager sharedInstance] updateUser]; - - //Calling verify on the mock isn't working correctly, but the macro syntax does - OCMVerify([self.eventSourceMock eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]]); - [self.pollingManagerMock verify]; -} - -- (void)testUpdateUser_online_polling { - LDConfig *config = [LDClient sharedInstance].ldConfig; - config.streaming = NO; - [[LDClientManager sharedInstance] setOnline:YES]; - [[self.eventSourceMock reject] eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]]; - [[self.pollingManagerMock expect] stopConfigPolling]; - [[self.pollingManagerMock expect] startConfigPolling]; - - [[LDClientManager sharedInstance] updateUser]; - - [self.eventSourceMock verify]; - [self.pollingManagerMock verify]; -} - -- (void)testWillEnterBackground { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager willEnterBackground]; - - OCMVerify([self.pollingManagerMock suspendEventPolling]); -} - -- (void)testWillEnterForeground { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager willEnterForeground]; - - OCMVerify([self.pollingManagerMock resumeEventPolling]); -} - -- (void)testProcessedEventsSuccessWithProcessedEvents { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:@"testMobileKey"]; - LDFlagConfigValue *flagConfigValue = [LDFlagConfigValue flagConfigValueFromJsonFileNamed:@"boolConfigIsABool-true" - flagKey:kLDFlagKeyIsABool - eventTrackingContext:[LDEventTrackingContext stub]]; - LDEventModel *event = [LDEventModel featureEventWithFlagKey:kFeatureEventKeyStub - reportedFlagValue:flagConfigValue.value - flagConfigValue:flagConfigValue - defaultFlagValue:@(NO) - user:[LDClient sharedInstance].ldUser - inlineUser:config.inlineUserInEvents]; - NSArray *events = @[[event dictionaryValueUsingConfig:config]]; - NSDate *headerDate = [NSDateFormatter eventDateHeaderStub]; - [[self.dataManagerMock expect] deleteProcessedEvents:events]; - [[self.dataManagerMock expect] setLastEventResponseDate:headerDate]; - - [[LDClientManager sharedInstance] processedEvents:YES jsonEventArray:events responseDate:headerDate]; - - [self.dataManagerMock verify]; -} - -- (void)testProcessedEventsSuccessWithoutProcessedEvents { - NSDate *headerDate = [NSDateFormatter eventDateHeaderStub]; - [[self.dataManagerMock expect] deleteProcessedEvents:@[]]; - [[self.dataManagerMock expect] setLastEventResponseDate:headerDate]; - - [[LDClientManager sharedInstance] processedEvents:YES jsonEventArray:@[] responseDate:headerDate]; - - [self.dataManagerMock verify]; -} - -- (void)testProcessedEventsFailure { - [[self.dataManagerMock reject] deleteProcessedEvents:[OCMArg any]]; - - [[LDClientManager sharedInstance] processedEvents:NO jsonEventArray:nil responseDate:nil]; - - [self.dataManagerMock verify]; -} - -- (void)testProcessedConfigSuccessWithUserConfigChanged { - id mockUserUpdatedObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:mockUserUpdatedObserver name:kLDUserUpdatedNotification object:nil]; - [[mockUserUpdatedObserver expect] notificationWithName:kLDUserUpdatedNotification object:[OCMArg any]]; - - id mockUserNoChangeObserver = OCMObserverMock(); //expect this NOT to be posted - [[NSNotificationCenter defaultCenter] addMockObserver:mockUserNoChangeObserver name:kLDUserNoChangeNotification object:nil]; - - LDFlagConfigModel *flagConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"featureFlags"]; - - LDUserModel *user = [self.ldClientMock ldUser]; - user.flagConfig = flagConfig; - [[[self.ldClientMock expect] andReturn:user] ldUser]; - - NSMutableDictionary *updatedFlags = [NSMutableDictionary dictionaryWithDictionary:[flagConfig dictionaryValue]]; - NSMutableDictionary *updatedBoolFlag = [NSMutableDictionary dictionaryWithDictionary:updatedFlags[kBoolFlagKey]]; - updatedBoolFlag[kLDFlagConfigValueKeyValue] = @(![updatedBoolFlag[kLDFlagConfigValueKeyValue] boolValue]); - updatedBoolFlag[kLDFlagConfigValueKeyVersion] = @([updatedBoolFlag[kLDFlagConfigValueKeyVersion] integerValue] + 1); - updatedFlags[kBoolFlagKey] = updatedBoolFlag; - - [[LDClientManager sharedInstance] processedConfig:YES jsonConfigDictionary:[updatedFlags copy]]; - - OCMVerify([self.dataManagerMock saveUser:[OCMArg any]]); - OCMVerifyAll(mockUserUpdatedObserver); - OCMVerifyAll(mockUserNoChangeObserver); - - [[NSNotificationCenter defaultCenter] removeObserver:mockUserUpdatedObserver]; - [[NSNotificationCenter defaultCenter] removeObserver:mockUserNoChangeObserver]; -} - -- (void)testProcessedConfigSuccessWithUserConfigUnchanged { - id mockUserUpdatedObserver = OCMObserverMock(); //expect this NOT to be posted - [[NSNotificationCenter defaultCenter] addMockObserver:mockUserUpdatedObserver name:kLDUserUpdatedNotification object:nil]; - - id mockUserNoChangeObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:mockUserNoChangeObserver name:kLDUserNoChangeNotification object:nil]; - [[mockUserNoChangeObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any]]; - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - LDEventTrackingContext *eventTrackingContext = [LDEventTrackingContext contextWithTrackEvents:NO debugEventsUntilDate:nil]; - LDFlagConfigModel *flagConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"featureFlags" eventTrackingContext:eventTrackingContext]; - - LDUserModel *user = [self.ldClientMock ldUser]; - user.flagConfig = flagConfig; - [[[self.ldClientMock expect] andReturn:user] ldUser]; - - LDEventTrackingContext *updatedEventTrackingContext = [LDEventTrackingContext contextWithTrackEvents:YES debugEventsUntilDate:[NSDate dateWithTimeIntervalSinceNow:30.0]]; - LDFlagConfigModel *updatedFlagConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"featureFlags" eventTrackingContext:updatedEventTrackingContext]; - - [[LDClientManager sharedInstance] processedConfig:YES jsonConfigDictionary:[updatedFlagConfig dictionaryValue]]; - - for (NSString *flagKey in flagConfig.featuresJsonDictionary.allKeys) { - LDFlagConfigValue *flagConfigValue = flagConfig.featuresJsonDictionary[flagKey]; - XCTAssertEqualObjects(flagConfigValue.eventTrackingContext, updatedEventTrackingContext); - } - - OCMVerifyAll(self.dataManagerMock); - OCMVerifyAll(mockUserUpdatedObserver); - OCMVerifyAll(mockUserNoChangeObserver); - - [[NSNotificationCenter defaultCenter] removeObserver:mockUserUpdatedObserver]; - [[NSNotificationCenter defaultCenter] removeObserver:mockUserNoChangeObserver]; -} - -- (void)testProcessedConfigSuccessWithoutUserConfig { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager processedConfig:YES jsonConfigDictionary:nil]; - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - [self.dataManagerMock verify]; -} - -- (void)testProcessedConfigFailure { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager processedConfig:NO jsonConfigDictionary:nil]; - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - [self.dataManagerMock verify]; -} - -- (void)testProcessedConfigSuccessWithUserSameUserConfig { - LDFlagConfigModel *startingConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"ldClientManagerTestConfigA"]; - XCTAssertNotNil(startingConfig); - - LDUserModel *clientUser = [[LDClient sharedInstance] ldUser]; - clientUser.flagConfig = startingConfig; - - [[LDClientManager sharedInstance] processedConfig:YES jsonConfigDictionary:[startingConfig dictionaryValue]]; - XCTAssertTrue(clientUser.flagConfig == startingConfig); //Should be the same object, unchanged -} - -- (void)testProcessedConfigSuccessWithUserDifferentUserConfig { - LDFlagConfigModel *startingConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"ldClientManagerTestConfigA"]; - XCTAssertNotNil(startingConfig); - - LDUserModel *clientUser = [[LDClient sharedInstance] ldUser]; - clientUser.flagConfig = startingConfig; - - LDFlagConfigModel *endingConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"ldClientManagerTestConfigB"]; - XCTAssertNotNil(endingConfig); - - [[LDClientManager sharedInstance] processedConfig:YES jsonConfigDictionary:[endingConfig dictionaryValue]]; - XCTAssertFalse(clientUser.flagConfig == startingConfig); //Should not be the same object - XCTAssertTrue([clientUser.flagConfig isEqualToConfig:endingConfig]); -} - -- (void)testSetOnlineYes { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - OCMVerify([self.pollingManagerMock startEventPolling]); - OCMVerify([self.eventSourceMock onMessage:[OCMArg any]]); -} - -- (void)testSetOnlineNo { - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - clientManager.online = NO; - - OCMVerify([self.pollingManagerMock stopEventPolling]); - XCTAssertNil([clientManager eventSource]); -} - -- (void)testFlushEventsWhenOnline { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:mockMobileKey]; - NSArray *eventDictionaries = [LDEventModel stubEventDictionariesForUser:[LDClient sharedInstance].ldUser config:config]; - OCMStub([self.dataManagerMock allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { - void (^completion)(NSArray *) = obj; - completion(eventDictionaries); - return YES; - }]]); - - [LDClientManager sharedInstance].online = YES; - [[LDClientManager sharedInstance] flushEvents]; - - OCMVerify([self.requestManagerMock performEventRequest:[OCMArg isEqual:eventDictionaries]]); -} - -- (void)testFlushEventsWhenOffline { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:mockMobileKey]; - NSArray *eventDictionaries = [LDEventModel stubEventDictionariesForUser:[LDClient sharedInstance].ldUser config:config]; - OCMStub([self.dataManagerMock allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { - void (^completion)(NSArray *) = obj; - completion(eventDictionaries); - return YES; - }]]); - [[self.requestManagerMock reject] performEventRequest:[OCMArg any]]; - [[self.dataManagerMock reject] allEventDictionaries:[OCMArg any]]; - - [[LDClientManager sharedInstance] flushEvents]; - - [self.requestManagerMock verify]; - [self.dataManagerMock verify]; -} - -- (void)testClientUnauthorizedPosted { - id clientUnauthorizedObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:clientUnauthorizedObserver name:kLDClientUnauthorizedNotification object:nil]; - [[clientUnauthorizedObserver expect] notificationWithName: kLDClientUnauthorizedNotification object:[OCMArg any]]; - - id serverUnavailableObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:serverUnavailableObserver name:kLDServerConnectionUnavailableNotification object:nil]; - [[serverUnavailableObserver expect] notificationWithName: kLDServerConnectionUnavailableNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler errorHandler; - OCMStub([self.eventSourceMock onError:[OCMArg checkWithBlock:^BOOL(id obj) { - errorHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:clientUnauthorizedObserver]; - [[NSNotificationCenter defaultCenter] removeObserver:serverUnavailableObserver]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(errorHandler); - if (!errorHandler) { return; } - errorHandler([LDEvent stubUnauthorizedEvent]); - - [clientUnauthorizedObserver verify]; - [serverUnavailableObserver verify]; -} - -- (void)testClientUnauthorizedNotPosted { - id clientUnauthorizedObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:clientUnauthorizedObserver name:kLDClientUnauthorizedNotification object:nil]; - //it's not obvious, but by not setting expect on the mock observer, the observer will fail when verify is called IF it has received the notification - - id serverUnavailableObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:serverUnavailableObserver name:kLDServerConnectionUnavailableNotification object:nil]; - [[serverUnavailableObserver expect] notificationWithName: kLDServerConnectionUnavailableNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler errorHandler; - OCMStub([self.eventSourceMock onError:[OCMArg checkWithBlock:^BOOL(id obj) { - errorHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:clientUnauthorizedObserver]; - [[NSNotificationCenter defaultCenter] removeObserver:serverUnavailableObserver]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(errorHandler); - if (!errorHandler) { return; } - errorHandler([LDEvent stubErrorEvent]); - - [clientUnauthorizedObserver verify]; - [serverUnavailableObserver verify]; -} - -- (void)testSSEPingEvent { - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - messageHandler([LDEvent stubPingEvent]); - - OCMVerify([self.requestManagerMock performFeatureFlagRequest:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[LDUserModel class]]) { return NO; } - LDUserModel *userFromRequest = (LDUserModel*)obj; - return [user isEqual:userFromRequest]; - }]]); -} - -- (void)testSSEPutEventSuccess { - id userUpdatedNotificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:userUpdatedNotificationObserver name:kLDUserUpdatedNotification object:nil]; - [[userUpdatedNotificationObserver expect] notificationWithName:kLDUserUpdatedNotification object:[OCMArg any]]; - - id userNoChangeNotificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:userNoChangeNotificationObserver name:kLDUserNoChangeNotification object:nil]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - LDFlagConfigModel *targetFlagConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"featureFlags-excludeNulls"]; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:userUpdatedNotificationObserver]; - [[NSNotificationCenter defaultCenter] removeObserver:userNoChangeNotificationObserver]; - }; - - [LDClientManager sharedInstance].online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - [[self.dataManagerMock expect] saveUser:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[LDUserModel class]]) { return NO; } - LDUserModel *savedUser = obj; - XCTAssertTrue([savedUser.flagConfig isEqualToConfig:targetFlagConfig]); - return YES; - }]]; - - LDEvent *put = [LDEvent stubEvent:kLDEventTypePut fromJsonFileNamed:@"featureFlags-excludeNulls"]; - - messageHandler(put); - - [self.dataManagerMock verify]; -} - -- (void)testSSEPutResultedInNoChange { - id userUpdatedNotificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:userUpdatedNotificationObserver name:kLDUserUpdatedNotification object:nil]; - - id userNoChangeNotificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:userNoChangeNotificationObserver name:kLDUserNoChangeNotification object:nil]; - [[userNoChangeNotificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - id flagConfigMock = OCMClassMock([LDFlagConfigModel class]); - OCMStub([flagConfigMock isEqualToConfig:[OCMArg any]]).andReturn(YES); - user.flagConfig = flagConfigMock; - - [[self.dataManagerMock expect] saveUser:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[LDUserModel class]]) { return NO; } - LDUserModel *savedUser = obj; - XCTAssertEqualObjects(savedUser, user); - return YES; - }]]; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:userUpdatedNotificationObserver]; - [[NSNotificationCenter defaultCenter] removeObserver:userNoChangeNotificationObserver]; - [flagConfigMock stopMocking]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - //NOTE: Because the flag config mock will return YES on a config comparison, the put here doesn't matter - LDEvent *put = [LDEvent stubEvent:kLDEventTypePut fromJsonFileNamed:@"featureFlags"]; - - messageHandler(put); - - [self.dataManagerMock verify]; -} - -- (void)testSSEPutEventFailedNilData { - id notificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; - [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - LDFlagConfigModel *targetFlagConfig = user.flagConfig; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - LDEvent *put = [LDEvent stubEvent:kLDEventTypePut fromJsonFileNamed:@"featureFlags"]; - put.data = nil; - - messageHandler(put); - - XCTAssertTrue([user.flagConfig isEqualToConfig: targetFlagConfig]); - OCMVerifyAll(notificationObserver); - OCMVerify(self.dataManagerMock); -} - -- (void)testSSEPutEventFailedEmptyData { - id notificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; - [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - LDFlagConfigModel *targetFlagConfig = user.flagConfig; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - LDEvent *put = [LDEvent stubEvent:kLDEventTypePut fromJsonFileNamed:@"featureFlags"]; - put.data = @""; - - messageHandler(put); - - XCTAssertTrue([user.flagConfig isEqualToConfig: targetFlagConfig]); - OCMVerifyAll(notificationObserver); - OCMVerify(self.dataManagerMock); -} - -- (void)testSSEPutEventFailedInvalidData { - id notificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; - [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - LDFlagConfigModel *targetFlagConfig = user.flagConfig; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - LDEvent *put = [LDEvent stubEvent:kLDEventTypePut fromJsonFileNamed:@"featureFlags"]; - put.data = @"{\"someInvalidData\":}"; - - messageHandler(put); - - XCTAssertTrue([user.flagConfig isEqualToConfig: targetFlagConfig]); - OCMVerifyAll(notificationObserver); - OCMVerify(self.dataManagerMock); -} - -- (void)testSSEUnrecognizedEvent { - id notificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; - //it's not obvious, but by not setting expect on the mock observer, the observer will fail when verify is called IF it has received the notification - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - LDFlagConfigModel *targetFlagConfig = user.flagConfig; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - LDEvent *put = [LDEvent stubEvent:@"someUnrecognizedEvent" fromJsonFileNamed:@"featureFlags"]; - - messageHandler(put); - - XCTAssertTrue([user.flagConfig isEqualToConfig: targetFlagConfig]); - OCMVerifyAll(notificationObserver); - OCMVerify(self.dataManagerMock); -} - -- (void)testSSEPatchEventSuccess { - id notificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; - [[notificationObserver expect] notificationWithName:kLDUserUpdatedNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - id flagConfigMock = OCMClassMock([LDFlagConfigModel class]); - OCMStub([flagConfigMock hasFeaturesEqualToDictionary:[OCMArg any]]).andReturn(NO); - - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - user.flagConfig = flagConfigMock; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; - [flagConfigMock stopMocking]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - LDEvent *patch = [LDEvent stubEvent:kLDEventTypePatch fromJsonFileNamed:@"ldClientManagerTestPatchIsANumber"]; - NSDictionary *targetPatchDictionary = [NSJSONSerialization jsonObjectFromFileNamed:@"ldClientManagerTestPatchIsANumber"]; - - messageHandler(patch); - - __block NSDictionary *patchDictionary; - OCMVerify([flagConfigMock addOrReplaceFromDictionary:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[NSDictionary class]]) { return NO; } - patchDictionary = obj; - return YES; - }]]); - XCTAssertTrue([patchDictionary isEqualToDictionary:targetPatchDictionary]); - __block LDUserModel *savedUser; - OCMVerify([self.dataManagerMock saveUser:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[LDUserModel class]]) { return NO; } - savedUser = obj; - return YES; - }]]); - XCTAssertTrue([savedUser isEqual:user]); - OCMVerifyAll(notificationObserver); -} - -- (void)testSSEPatchResultedInNoChange { - id notificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; - [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - id flagConfigMock = OCMClassMock([LDFlagConfigModel class]); - __block NSDictionary *patchDictionary; - [[flagConfigMock expect] addOrReplaceFromDictionary:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[NSDictionary class]]) { return NO; } - patchDictionary = obj; - return YES; - }]]; - OCMStub([flagConfigMock hasFeaturesEqualToDictionary:[OCMArg any]]).andReturn(YES); - user.flagConfig = flagConfigMock; - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; - [flagConfigMock stopMocking]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - //NOTE: Because the flag config mock will return YES on a dictionary comparison, the patch here doesn't matter - LDEvent *patch = [LDEvent stubEvent:kLDEventTypePatch fromJsonFileNamed:@"ldClientManagerTestPatchIsANumber"]; - - messageHandler(patch); - - OCMVerifyAll(notificationObserver); - OCMVerify(self.dataManagerMock); -} - -- (void)testSSEPatchFailedNilData { - id notificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; - [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - id flagConfigMock = OCMClassMock([LDFlagConfigModel class]); - [[flagConfigMock reject] addOrReplaceFromDictionary:[OCMArg any]]; - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - user.flagConfig = flagConfigMock; - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; - [flagConfigMock stopMocking]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - LDEvent *patch = [LDEvent stubEvent:kLDEventTypePatch fromJsonFileNamed:@"ldClientManagerTestPatchIsANumber"]; - patch.data = nil; - - messageHandler(patch); - - OCMVerifyAll(notificationObserver); - OCMVerify(self.dataManagerMock); -} - -- (void)testSSEPatchFailedEmptyData { - id notificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; - [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - id flagConfigMock = OCMClassMock([LDFlagConfigModel class]); - [[flagConfigMock reject] addOrReplaceFromDictionary:[OCMArg any]]; - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - user.flagConfig = flagConfigMock; - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; - [flagConfigMock stopMocking]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - LDEvent *patch = [LDEvent stubEvent:kLDEventTypePatch fromJsonFileNamed:@"ldClientManagerTestPatchIsANumber"]; - patch.data = @""; - - messageHandler(patch); - - OCMVerifyAll(notificationObserver); - OCMVerify(self.dataManagerMock); -} - -- (void)testSSEPatchFailedInvalidData { - id notificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; - [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - id flagConfigMock = OCMClassMock([LDFlagConfigModel class]); - [[flagConfigMock reject] addOrReplaceFromDictionary:[OCMArg any]]; - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - user.flagConfig = flagConfigMock; - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; - [flagConfigMock stopMocking]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - LDEvent *patch = [LDEvent stubEvent:kLDEventTypePatch fromJsonFileNamed:@"ldClientManagerTestPatchIsANumber"]; - patch.data = @"{\"someInvalidData\":}"; - - messageHandler(patch); - - OCMVerifyAll(notificationObserver); - OCMVerify(self.dataManagerMock); -} - -- (void)testSSEDeleteEventSuccess { - id notificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; - [[notificationObserver expect] notificationWithName:kLDUserUpdatedNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - id flagConfigMock = OCMClassMock([LDFlagConfigModel class]); - OCMStub([flagConfigMock hasFeaturesEqualToDictionary:[OCMArg any]]).andReturn(NO); - - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - user.flagConfig = flagConfigMock; - - self.cleanup = ^{ - [flagConfigMock stopMocking]; - [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - LDEvent *delete = [LDEvent stubEvent:kLDEventTypeDelete fromJsonFileNamed:@"ldClientManagerTestDeleteIsANumber"]; - NSDictionary *targetDeleteDictionary = [NSJSONSerialization jsonObjectFromFileNamed:@"ldClientManagerTestDeleteIsANumber"]; - - messageHandler(delete); - - __block NSDictionary *deleteDictionary; - OCMVerify([flagConfigMock deleteFromDictionary:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[NSDictionary class]]) { return NO; } - deleteDictionary = obj; - return YES; - }]]); - XCTAssertTrue([deleteDictionary isEqualToDictionary:targetDeleteDictionary]); - __block LDUserModel *savedUser; - OCMVerify([self.dataManagerMock saveUser:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[LDUserModel class]]) { return NO; } - savedUser = obj; - return YES; - }]]); - XCTAssertNotNil(savedUser); - XCTAssertTrue([savedUser isEqual:user]); - OCMVerifyAll(notificationObserver); -} - -- (void)testSSEDeleteResultedInNoChange { - id notificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; - [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - id flagConfigMock = OCMClassMock([LDFlagConfigModel class]); - __block NSDictionary *deleteDictionary; - [[flagConfigMock expect] deleteFromDictionary:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[NSDictionary class]]) { return NO; } - deleteDictionary = obj; - return YES; - }]]; - OCMStub([flagConfigMock hasFeaturesEqualToDictionary:[OCMArg any]]).andReturn(YES); - user.flagConfig = flagConfigMock; - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; - [flagConfigMock stopMocking]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - //NOTE: Because the flag config mock will return YES on a dictionary comparison, the delete here doesn't matter - LDEvent *delete = [LDEvent stubEvent:kLDEventTypeDelete fromJsonFileNamed:@"ldClientManagerTestDeleteIsANumber"]; - - messageHandler(delete); - - OCMVerifyAll(notificationObserver); - OCMVerify(self.dataManagerMock); -} - -- (void)testSSEDeleteFailedNilData { - id notificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; - [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - id flagConfigMock = OCMClassMock([LDFlagConfigModel class]); - [[flagConfigMock reject] deleteFromDictionary:[OCMArg any]]; - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - user.flagConfig = flagConfigMock; - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; - [flagConfigMock stopMocking]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - LDEvent *delete = [LDEvent stubEvent:kLDEventTypeDelete fromJsonFileNamed:@"ldClientManagerTestDeleteIsANumber"]; - delete.data = nil; - - messageHandler(delete); - - OCMVerifyAll(notificationObserver); - OCMVerify(self.dataManagerMock); -} - -- (void)testSSEDeleteFailedEmptyData { - id notificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; - [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - id flagConfigMock = OCMClassMock([LDFlagConfigModel class]); - [[flagConfigMock reject] deleteFromDictionary:[OCMArg any]]; - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - user.flagConfig = flagConfigMock; - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; - [flagConfigMock stopMocking]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - LDEvent *delete = [LDEvent stubEvent:kLDEventTypeDelete fromJsonFileNamed:@"ldClientManagerTestDeleteIsANumber"]; - delete.data = @""; - - messageHandler(delete); - - OCMVerifyAll(notificationObserver); - OCMVerify(self.dataManagerMock); -} - -- (void)testSSEDeleteFailedInvalidData { - id notificationObserver = OCMObserverMock(); - [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; - [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any]]; - - __block LDEventSourceEventHandler messageHandler; - OCMStub([self.eventSourceMock onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { - messageHandler = (LDEventSourceEventHandler)obj; - return YES; - }]]); - - id flagConfigMock = OCMClassMock([LDFlagConfigModel class]); - [[flagConfigMock reject] deleteFromDictionary:[OCMArg any]]; - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - user.flagConfig = flagConfigMock; - - [[self.dataManagerMock reject] saveUser:[OCMArg any]]; - - self.cleanup = ^{ - [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; - [flagConfigMock stopMocking]; - }; - - LDClientManager *clientManager = [LDClientManager sharedInstance]; - clientManager.online = YES; - - XCTAssertNotNil(messageHandler); - if (!messageHandler) { return; } - - LDEvent *delete = [LDEvent stubEvent:kLDEventTypeDelete fromJsonFileNamed:@"ldClientManagerTestDeleteIsANumber"]; - delete.data = @"{\"someInvalidData\":}"; - - messageHandler(delete); - - OCMVerifyAll(notificationObserver); - OCMVerify(self.dataManagerMock); -} - -#pragma mark - Helpers - -- (id)mockClientWithUser:(LDUserModel*)user { - id mockClient = OCMClassMock([LDClient class]); - OCMStub(ClassMethod([mockClient sharedInstance])).andReturn(mockClient); - OCMStub([mockClient ldUser]).andReturn(user); - XCTAssertEqual([LDClient sharedInstance].ldUser, user); - - LDConfig *config = [[LDConfig alloc] initWithMobileKey:@"testMobileKey"]; - config.flushInterval = [NSNumber numberWithInt:30]; - OCMStub([mockClient ldConfig]).andReturn(config); - XCTAssertEqual([LDClient sharedInstance].ldConfig, config); - - return mockClient; -} - -@end diff --git a/DarklyTests/LDClientTest.m b/DarklyTests/LDClientTest.m index 15341cfa..d898430f 100644 --- a/DarklyTests/LDClientTest.m +++ b/DarklyTests/LDClientTest.m @@ -4,6 +4,9 @@ #import "DarklyXCTestCase.h" #import "LDClient.h" +#import "LDEnvironment.h" +#import "LDEnvironmentMock.h" +#import "LDEnvironmentController.h" #import "LDDataManager.h" #import "LDUserModel.h" #import "LDUserModel+Testable.h" @@ -18,57 +21,33 @@ #import "NSJSONSerialization+Testable.h" #import "LDThrottler.h" #import "LDFlagConfigValue+Testable.h" +#import "ClientDelegateMock.h" +#import "LDConfig+Testable.h" +#import "NSURLSession+LaunchDarkly.h" #import "OCMock.h" -#import -typedef void(^MockLDClientDelegateCallbackBlock)(void); - -@interface MockLDClientDelegate : NSObject -@property (nonatomic, assign) NSInteger userDidUpdateCallCount; -@property (nonatomic, assign) NSInteger userUnchangedCallCount; -@property (nonatomic, assign) NSInteger serverConnectionUnavailableCallCount; -@property (nonatomic, strong) MockLDClientDelegateCallbackBlock userDidUpdateCallback; -@property (nonatomic, strong) MockLDClientDelegateCallbackBlock userUnchangedCallback; -@property (nonatomic, strong) MockLDClientDelegateCallbackBlock serverUnavailableCallback; +@interface LDClient (LDClientTest) +@property (nonatomic, strong) LDUserModel *ldUser; +@property (nonatomic, strong) NSMutableDictionary *secondaryEnvironments; // @end -@implementation MockLDClientDelegate --(instancetype)init { - self = [super init]; - - return self; -} - --(void)userDidUpdate { - self.userDidUpdateCallCount = [self processCallbackWithCount:self.userDidUpdateCallCount block:self.userDidUpdateCallback]; -} - --(void)userUnchanged { - self.userUnchangedCallCount = [self processCallbackWithCount:self.userUnchangedCallCount block:self.userUnchangedCallback]; -} - --(void)serverConnectionUnavailable { - self.serverConnectionUnavailableCallCount = [self processCallbackWithCount:self.serverConnectionUnavailableCallCount block:self.serverUnavailableCallback]; -} - --(NSInteger)processCallbackWithCount:(NSInteger)callbackCount block:(MockLDClientDelegateCallbackBlock)callbackBlock { - callbackCount += 1; - if (!callbackBlock) { return callbackCount; } - callbackBlock(); - return callbackCount; -} +@implementation LDClient (LDClientTest) +@dynamic ldUser; +@dynamic secondaryEnvironments; @end @interface LDClientTest : DarklyXCTestCase @property (nonatomic, strong) XCTestExpectation *userConfigUpdatedNotificationExpectation; -@property (nonatomic, strong) id clientManagerMock; -@property (nonatomic, strong) id dataManagerMock; -@property (nonatomic, strong) id requestManagerMock; +@property (nonatomic, strong) id primaryEnvironmentMock; @property (nonatomic, strong) id throttlerMock; -@property (nonatomic, strong) id userBuilderMock; +@property (nonatomic, strong) id nsUrlSessionMock; +@property (nonatomic, strong) LDUserBuilder *userBuilder; @property (nonatomic, strong) LDUserModel *user; @property (nonatomic, strong) LDConfig *config; +@property (nonatomic, strong) NSDictionary *secondaryMobileKeys; // +@property (nonatomic, strong) NSDictionary *secondaryEnvironmentMocks; // +@property (nonatomic, strong) NSArray *ignoredUserAttributes; @end NSString *const kFallbackString = @"fallbackString"; @@ -80,629 +59,353 @@ @implementation LDClientTest - (void)setUp { [super setUp]; - id mockClientManager = OCMClassMock([LDClientManager class]); - OCMStub(ClassMethod([mockClientManager sharedInstance])).andReturn(mockClientManager); - self.clientManagerMock = mockClientManager; - - id mockDataManager = OCMClassMock([LDDataManager class]); - OCMStub(ClassMethod([mockDataManager sharedManager])).andReturn(mockDataManager); - self.dataManagerMock = mockDataManager; - - id mockRequestManager = OCMClassMock([LDRequestManager class]); - OCMStub(ClassMethod([mockRequestManager sharedInstance])).andReturn(mockRequestManager); - self.requestManagerMock = mockRequestManager; + self.ignoredUserAttributes = @[kUserAttributeUpdatedAt, kUserAttributeConfig]; - self.throttlerMock = OCMClassMock([LDThrottler class]); - OCMStub([self.throttlerMock runThrottled:[OCMArg invokeBlock]]); + self.throttlerMock = [OCMockObject niceMockForClass:[LDThrottler class]]; + [[self.throttlerMock stub] runThrottled:[OCMArg invokeBlock]]; [LDClient sharedInstance].throttler = self.throttlerMock; self.user = [LDUserModel stubWithKey:nil]; - - self.userBuilderMock = OCMClassMock([LDUserBuilder class]); - OCMStub([self.userBuilderMock build]).andReturn(self.user); + self.userBuilder = [LDUserBuilder currentBuilder:self.user]; self.config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; + + self.nsUrlSessionMock = [OCMockObject niceMockForClass:[NSURLSession class]]; + [[[self.nsUrlSessionMock stub] andReturn:self] sharedSession]; + [[self.nsUrlSessionMock stub] setSharedLDSessionForConfig:self.config]; + + self.primaryEnvironmentMock = [OCMockObject niceMockForClass:[LDEnvironment class]]; + [[[self.primaryEnvironmentMock stub] andReturn:self.primaryEnvironmentMock] environmentForMobileKey:kTestMobileKey config:self.config user:[OCMArg checkWithBlock:^BOOL(id obj) { + return [obj isEqual:self.user ignoringAttributes:self.ignoredUserAttributes]; + }]]; +} + +- (void)setupSecondaryEnvironments { + self.secondaryMobileKeys = [LDConfig secondaryMobileKeysStub]; + self.config.secondaryMobileKeys = self.secondaryMobileKeys; + NSMutableDictionary *secondaryEnvironmentMocks = [NSMutableDictionary dictionaryWithCapacity:self.secondaryMobileKeys.count]; + for (NSString *mobileKey in self.secondaryMobileKeys.allValues) { + LDEnvironmentMock *secondaryEnvironmentMock = [LDEnvironmentMock environmentMockForMobileKey:mobileKey config:self.config user:self.user]; + secondaryEnvironmentMocks[mobileKey] = secondaryEnvironmentMock; + [[[self.primaryEnvironmentMock stub] andReturn:secondaryEnvironmentMock] environmentForMobileKey:[OCMArg checkWithBlock:^BOOL(id obj) { + if (![obj isKindOfClass:[NSString class]]) { + return NO; + } + NSString *environmentMobileKey = obj; + if (![environmentMobileKey isEqualToString:mobileKey]) { + return NO; + } + secondaryEnvironmentMock.environmentMockCallCount += 1; + return YES; + }] config:self.config user:[OCMArg checkWithBlock:^BOOL(id obj) { + return [obj isEqual:self.user ignoringAttributes:self.ignoredUserAttributes]; + }]]; + } + self.secondaryEnvironmentMocks = [secondaryEnvironmentMocks copy]; } - (void)tearDown { + [LDClient sharedInstance].primaryEnvironment = nil; [[LDClient sharedInstance] stopClient]; + [LDClient sharedInstance].ldUser = nil; + [LDClient sharedInstance].secondaryEnvironments = nil; [LDClient sharedInstance].delegate = nil; - [OHHTTPStubs removeAllStubs]; [[NSNotificationCenter defaultCenter] removeObserver:self]; self.user = nil; self.config = nil; - [self.clientManagerMock stopMocking]; - self.clientManagerMock = nil; - [self.dataManagerMock stopMocking]; - self.dataManagerMock = nil; - [self.requestManagerMock stopMocking]; - self.requestManagerMock = nil; - [self.throttlerMock stopMocking]; + self.nsUrlSessionMock = nil; self.throttlerMock = nil; - [self.userBuilderMock stopMocking]; - self.userBuilderMock = nil; + self.userBuilder = nil; self.userConfigUpdatedNotificationExpectation = nil; [super tearDown]; } +#pragma mark - Properties + - (void)testSharedInstance { LDClient *first = [LDClient sharedInstance]; LDClient *second = [LDClient sharedInstance]; XCTAssertEqual(first, second); } -- (void)testStartWithoutConfig { - [[self.dataManagerMock reject] createIdentifyEventWithUser:[OCMArg any] config:[OCMArg any]]; - XCTAssertFalse([[LDClient sharedInstance] start:nil withUserBuilder:nil]); - [self.dataManagerMock verify]; -} - -- (void)testStartWithValidConfig { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - LDUserBuilder *userBuilder = [[LDUserBuilder alloc] init]; - userBuilder.key = [[NSUUID UUID] UUIDString]; - [[self.dataManagerMock expect] createIdentifyEventWithUser:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[LDUserModel class]]) { return NO; } - return [((LDUserModel*)obj).key isEqualToString:userBuilder.key]; - }] config:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[LDConfig class]]) { return NO; } - return [((LDConfig*)obj).mobileKey isEqualToString:config.mobileKey]; - }]]; - - BOOL didStart = [[LDClient sharedInstance] start:config withUserBuilder:userBuilder]; - XCTAssertTrue(didStart); - [self.dataManagerMock verify]; -} - -- (void)testStartWithValidConfigMultipleTimes { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - LDUserBuilder *userBuilder = [[LDUserBuilder alloc] init]; - userBuilder.key = [[NSUUID UUID] UUIDString]; - __block NSInteger createIdentifyEventCallCount = 0; - [[self.dataManagerMock expect] createIdentifyEventWithUser:[OCMArg checkWithBlock:^BOOL(id obj) { - if (createIdentifyEventCallCount > 0) { return NO; } //Make sure the client only records one identify event - if (![obj isKindOfClass:[LDUserModel class]]) { return NO; } - if (![((LDUserModel*)obj).key isEqualToString:userBuilder.key]) { return NO; } - createIdentifyEventCallCount += 1; - return [((LDUserModel*)obj).key isEqualToString:userBuilder.key]; - }] config:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[LDConfig class]]) { return NO; } - return [((LDConfig*)obj).mobileKey isEqualToString:config.mobileKey]; - }]]; - XCTAssertTrue([[LDClient sharedInstance] start:config withUserBuilder:userBuilder]); - XCTAssertFalse([[LDClient sharedInstance] start:config withUserBuilder:userBuilder]); - - [self.dataManagerMock verify]; +-(void)testEnvironmentName { + XCTAssertEqualObjects([LDClient sharedInstance].environmentName, kLDPrimaryEnvironmentName); } -#pragma mark - Variations -#pragma mark Bool Variation -- (void)testBoolVariation_knownFlag { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = @"isABool"; - id defaultFlagValue = @(NO); - LDFlagConfigModel *flagConfigModel = [self configureUserWithFlagConfigModelFromJsonFileNamed:@"boolConfigIsABool-true"]; - LDFlagConfigValue *flagConfigValue = [flagConfigModel flagConfigValueForFlagKey:flagKey]; - id targetFlagValue = flagConfigValue.value; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:targetFlagValue - flagConfigValue:flagConfigValue - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - BOOL flagValue = [[LDClient sharedInstance] boolVariation:flagKey fallback:[defaultFlagValue boolValue]]; +- (void)testDelegateSet { + LDClient *ldClient = [LDClient sharedInstance]; - XCTAssertEqual(flagValue, [targetFlagValue boolValue]); - [self.dataManagerMock verify]; + ldClient.delegate = (id)self; + XCTAssertEqualObjects(self, ldClient.delegate); } -- (void)testBoolVariation_knownFlag_nullValue { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = kLDFlagKeyIsABool; - id defaultFlagValue = [LDFlagConfigValue defaultValueForFlagKey:flagKey]; - LDFlagConfigModel *flagConfigModel = [self configureUserWithFlagConfigModelFromJsonFileNamed:@"boolConfigIsABool-false"]; - LDFlagConfigValue *flagConfigValue = [flagConfigModel flagConfigValueForFlagKey:flagKey]; - flagConfigValue.value = [NSNull null]; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:defaultFlagValue - flagConfigValue:flagConfigValue - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - BOOL flagValue = [[LDClient sharedInstance] boolVariation:flagKey fallback:[defaultFlagValue boolValue]]; - - XCTAssertEqualObjects(@(flagValue), defaultFlagValue); - [self.dataManagerMock verify]; -} +#pragma mark - SDK Control -- (void)testBoolVariation_unknownFlag { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = @"dummy-flag-key"; - id defaultFlagValue = @(YES); - id targetFlagValue = defaultFlagValue; - [self configureUserWithFlagConfigModelFromJsonFileNamed:@"boolConfigIsABool-false"]; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey reportedFlagValue:defaultFlagValue flagConfigValue:nil defaultFlagValue:defaultFlagValue user:self.user config:self.config]; +- (void)testStart { + self.nsUrlSessionMock = [OCMockObject niceMockForClass:[NSURLSession class]]; + [[[self.nsUrlSessionMock stub] andReturn:self] sharedSession]; + [[self.nsUrlSessionMock expect] setSharedLDSessionForConfig:self.config]; + [(LDEnvironment*)[self.primaryEnvironmentMock expect] start]; + [[self.primaryEnvironmentMock expect] setOnline:YES]; - BOOL flagValue = [[LDClient sharedInstance] boolVariation:flagKey fallback:[defaultFlagValue boolValue]]; + XCTAssertTrue([[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]); - XCTAssertEqual(flagValue, [targetFlagValue boolValue]); - [self.dataManagerMock verify]; + XCTAssertTrue([[LDClient sharedInstance].ldUser isEqual:self.user ignoringAttributes:self.ignoredUserAttributes]); + XCTAssertEqualObjects([LDClient sharedInstance].primaryEnvironment, self.primaryEnvironmentMock); + XCTAssertEqual([LDClient sharedInstance].secondaryEnvironments.count, 0); + [self.nsUrlSessionMock verify]; + [self.primaryEnvironmentMock verify]; } -- (void)testBoolVariation_knownFlag_withoutStart { - NSString *flagKey = @"isABool"; - id defaultFlagValue = @(YES); - id targetFlagValue = defaultFlagValue; - [self configureUserWithFlagConfigModelFromJsonFileNamed:@"boolConfigIsABool-false"]; - [[self.dataManagerMock reject] createFlagEvaluationEventsWithFlagKey:[OCMArg any] reportedFlagValue:[OCMArg any] flagConfigValue:[OCMArg any] defaultFlagValue:[OCMArg any] user:self.user config:self.config]; +- (void)testStart_withSecondaryEnvironments { + self.nsUrlSessionMock = [OCMockObject niceMockForClass:[NSURLSession class]]; + [[[self.nsUrlSessionMock stub] andReturn:self] sharedSession]; + [self setupSecondaryEnvironments]; + [[self.nsUrlSessionMock expect] setSharedLDSessionForConfig:self.config]; + [(LDEnvironment*)[self.primaryEnvironmentMock expect] start]; + [[self.primaryEnvironmentMock expect] setOnline:YES]; - BOOL flagValue = [[LDClient sharedInstance] boolVariation:flagKey fallback:[defaultFlagValue boolValue]]; + XCTAssertTrue([[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]); - XCTAssertEqual(flagValue, [targetFlagValue boolValue]); - [self.dataManagerMock verify]; + XCTAssertTrue([[LDClient sharedInstance].ldUser isEqual:self.user ignoringAttributes:self.ignoredUserAttributes]); + XCTAssertEqualObjects([LDClient sharedInstance].primaryEnvironment, self.primaryEnvironmentMock); //self.environmentMock is configured to return self.environmentMock on a environmentForMobileKey msg + [self.nsUrlSessionMock verify]; + [self.primaryEnvironmentMock verify]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + XCTAssertEqual(environmentMock.environmentMockCallCount, 1); + XCTAssertEqual(environmentMock.startCallCount, 1); + XCTAssertEqual(environmentMock.setOnlineCallCount, 1); + XCTAssertEqual(environmentMock.setOnlineCalledValueOnline, YES); + } } -#pragma mark Number Variation -- (void)testNumberVariation_knownFlag { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = @"isANumber"; - id defaultFlagValue = @5; - LDFlagConfigModel *flagConfigModel = [self configureUserWithFlagConfigModelFromJsonFileNamed:@"numberConfigIsANumber-2"]; - LDFlagConfigValue *flagConfigValue = [flagConfigModel flagConfigValueForFlagKey:flagKey]; - id targetFlagValue = flagConfigValue.value; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:targetFlagValue - flagConfigValue:flagConfigValue - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - NSNumber *flagValue = [[LDClient sharedInstance] numberVariation:flagKey fallback:defaultFlagValue]; +- (void)testStart_withoutConfig { + self.nsUrlSessionMock = [OCMockObject niceMockForClass:[NSURLSession class]]; + [[[self.nsUrlSessionMock stub] andReturn:self] sharedSession]; + [[self.nsUrlSessionMock reject] setSharedLDSessionForConfig:[OCMArg any]]; + [(LDEnvironment*)[self.primaryEnvironmentMock reject] start]; + [[self.primaryEnvironmentMock reject] setOnline:[OCMArg any]]; - XCTAssertEqualObjects(flagValue, targetFlagValue); - [self.dataManagerMock verify]; -} - -- (void)testNumberVariation_knownFlag_nullValue { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = kLDFlagKeyIsANumber; - id defaultFlagValue = [LDFlagConfigValue defaultValueForFlagKey:flagKey]; - LDFlagConfigModel *flagConfigModel = [self configureUserWithFlagConfigModelFromJsonFileNamed:@"numberConfigIsANumber-2"]; - LDFlagConfigValue *flagConfigValue = [flagConfigModel flagConfigValueForFlagKey:flagKey]; - flagConfigValue.value = [NSNull null]; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:defaultFlagValue - flagConfigValue:flagConfigValue - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - NSNumber *flagValue = [[LDClient sharedInstance] numberVariation:flagKey fallback:defaultFlagValue]; - - XCTAssertEqualObjects(flagValue, defaultFlagValue); - [self.dataManagerMock verify]; -} - -- (void)testNumberVariation_unknownFlag { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = @"dummy-flag-key"; - id defaultFlagValue = @5; - id targetFlagValue = defaultFlagValue; - [self configureUserWithFlagConfigModelFromJsonFileNamed:@"numberConfigIsANumber-2"]; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:defaultFlagValue - flagConfigValue:nil - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - NSNumber *flagValue = [[LDClient sharedInstance] numberVariation:flagKey fallback:defaultFlagValue]; + XCTAssertFalse([[LDClient sharedInstance] start:nil withUserBuilder:nil]); - XCTAssertEqualObjects(flagValue, targetFlagValue); - [self.dataManagerMock verify]; + XCTAssertNil([LDClient sharedInstance].primaryEnvironment); + [self.nsUrlSessionMock verify]; + [self.primaryEnvironmentMock verify]; } -- (void)testNumberVariation_knownFlag_withoutStart { - NSString *flagKey = @"isANumber"; - id defaultFlagValue = @5; - id targetFlagValue = defaultFlagValue; - [self configureUserWithFlagConfigModelFromJsonFileNamed:@"numberConfigIsANumber-2"]; - [[self.dataManagerMock reject] createFlagEvaluationEventsWithFlagKey:[OCMArg any] - reportedFlagValue:[OCMArg any] - flagConfigValue:[OCMArg any] - defaultFlagValue:[OCMArg any] - user:self.user - config:self.config]; +- (void)testStart_multipleStartCalls { + XCTAssertTrue([[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]); + [[self.nsUrlSessionMock reject] setSharedLDSessionForConfig:[OCMArg any]]; + [(LDEnvironment*)[self.primaryEnvironmentMock reject] start]; + [[self.primaryEnvironmentMock reject] setOnline:[OCMArg any]]; - NSNumber *flagValue = [[LDClient sharedInstance] numberVariation:flagKey fallback:defaultFlagValue]; + XCTAssertFalse([[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]); - XCTAssertEqualObjects(flagValue, targetFlagValue); - [self.dataManagerMock verify]; + XCTAssertEqualObjects([LDClient sharedInstance].primaryEnvironment, self.primaryEnvironmentMock); + [self.nsUrlSessionMock verify]; + [self.primaryEnvironmentMock verify]; } -#pragma mark Double Variation -- (void)testDoubleVariation_knownFlag { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = @"isADouble"; - id defaultFlagValue = @(2.71828); - LDFlagConfigModel *flagConfigModel = [self configureUserWithFlagConfigModelFromJsonFileNamed:@"doubleConfigIsADouble-Pi"]; - LDFlagConfigValue *flagConfigValue = [flagConfigModel flagConfigValueForFlagKey:flagKey]; - id targetFlagValue = flagConfigValue.value; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:targetFlagValue - flagConfigValue:flagConfigValue - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - double flagValue = [[LDClient sharedInstance] doubleVariation:flagKey fallback:[defaultFlagValue doubleValue]]; - - XCTAssertEqual(flagValue, [targetFlagValue doubleValue]); - [self.dataManagerMock verify]; -} - -- (void)testDoubleVariation_knownFlag_nullValue { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = kLDFlagKeyIsADouble; - id defaultFlagValue = [LDFlagConfigValue defaultValueForFlagKey:flagKey]; - LDFlagConfigModel *flagConfigModel = [self configureUserWithFlagConfigModelFromJsonFileNamed:@"doubleConfigIsADouble-Pi"]; - LDFlagConfigValue *flagConfigValue = [flagConfigModel flagConfigValueForFlagKey:flagKey]; - flagConfigValue.value = [NSNull null]; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:defaultFlagValue - flagConfigValue:flagConfigValue - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - double flagValue = [[LDClient sharedInstance] doubleVariation:flagKey fallback:[defaultFlagValue doubleValue]]; - - XCTAssertEqualObjects(@(flagValue), defaultFlagValue); - [self.dataManagerMock verify]; -} - -- (void)testDoubleVariation_unknownFlag { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = @"dummy-flag-key"; - id defaultFlagValue = @(2.71828); - id targetFlagValue = defaultFlagValue; - [self configureUserWithFlagConfigModelFromJsonFileNamed:@"doubleConfigIsADouble-Pi"]; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:defaultFlagValue - flagConfigValue:nil - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - double flagValue = [[LDClient sharedInstance] doubleVariation:flagKey fallback:[defaultFlagValue doubleValue]]; - - XCTAssertEqual(flagValue, [targetFlagValue doubleValue]); - [self.dataManagerMock verify]; -} - -- (void)testDoubleVariation_knownFlag_withoutStart { - NSString *flagKey = @"isADouble"; - id defaultFlagValue = @(2.71828); - id targetFlagValue = defaultFlagValue; - [self configureUserWithFlagConfigModelFromJsonFileNamed:@"doubleConfigIsADouble-Pi"]; - [[self.dataManagerMock reject] createFlagEvaluationEventsWithFlagKey:[OCMArg any] - reportedFlagValue:[OCMArg any] - flagConfigValue:[OCMArg any] - defaultFlagValue:[OCMArg any] - user:self.user - config:self.config]; - - double flagValue = [[LDClient sharedInstance] doubleVariation:flagKey fallback:[defaultFlagValue doubleValue]]; - - XCTAssertEqual(flagValue, [targetFlagValue doubleValue]); - [self.dataManagerMock verify]; -} +- (void)testStart_withoutUser { + self.nsUrlSessionMock = [OCMockObject niceMockForClass:[NSURLSession class]]; + [[[self.nsUrlSessionMock stub] andReturn:self] sharedSession]; + [[self.nsUrlSessionMock expect] setSharedLDSessionForConfig:self.config]; + self.primaryEnvironmentMock = [OCMockObject niceMockForClass:[LDEnvironment class]]; + [[[self.primaryEnvironmentMock stub] andReturn:self.primaryEnvironmentMock] environmentForMobileKey:kTestMobileKey config:self.config user:[OCMArg isKindOfClass:[LDUserModel class]]]; + [(LDEnvironment*)[self.primaryEnvironmentMock expect] start]; + [[self.primaryEnvironmentMock expect] setOnline:YES]; -#pragma mark String Variation -- (void)testStringVariation_knownFlag { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = @"isAString"; - id defaultFlagValue = kFallbackString; - LDFlagConfigModel *flagConfigModel = [self configureUserWithFlagConfigModelFromJsonFileNamed:@"stringConfigIsAString-someString"]; - LDFlagConfigValue *flagConfigValue = [flagConfigModel flagConfigValueForFlagKey:flagKey]; - id targetFlagValue = flagConfigValue.value; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:targetFlagValue - flagConfigValue:flagConfigValue - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - NSString *flagValue = [[LDClient sharedInstance] stringVariation:flagKey fallback:defaultFlagValue]; + XCTAssertTrue([[LDClient sharedInstance] start:self.config withUserBuilder:nil]); - XCTAssertEqualObjects(flagValue, targetFlagValue); - [self.dataManagerMock verify]; -} - -- (void)testStringVariation_knownFlag_nullValue { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = kLDFlagKeyIsAString; - id defaultFlagValue = [LDFlagConfigValue defaultValueForFlagKey:flagKey]; - LDFlagConfigModel *flagConfigModel = [self configureUserWithFlagConfigModelFromJsonFileNamed:@"stringConfigIsAString-someString"]; - LDFlagConfigValue *flagConfigValue = [flagConfigModel flagConfigValueForFlagKey:flagKey]; - flagConfigValue.value = [NSNull null]; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:defaultFlagValue - flagConfigValue:flagConfigValue - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - NSString *flagValue = [[LDClient sharedInstance] stringVariation:flagKey fallback:defaultFlagValue]; - - XCTAssertEqualObjects(flagValue, defaultFlagValue); - [self.dataManagerMock verify]; -} - -- (void)testStringVariation_unknownFlag { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = @"dummy-flag-key"; - id defaultFlagValue = kFallbackString; - id targetFlagValue = defaultFlagValue; - [self configureUserWithFlagConfigModelFromJsonFileNamed:@"stringConfigIsAString-someString"]; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:defaultFlagValue - flagConfigValue:nil - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - NSString *flagValue = [[LDClient sharedInstance] stringVariation:flagKey fallback:defaultFlagValue]; - - XCTAssertEqualObjects(flagValue, targetFlagValue); - [self.dataManagerMock verify]; + XCTAssertNotNil([LDClient sharedInstance].ldUser); + XCTAssertEqualObjects([LDClient sharedInstance].primaryEnvironment, self.primaryEnvironmentMock); + XCTAssertEqual([LDClient sharedInstance].secondaryEnvironments.count, 0); + [self.nsUrlSessionMock verify]; + [self.primaryEnvironmentMock verify]; } -- (void)testStringVariation_knownFlag_withoutStart { - NSString *flagKey = @"isAString"; - id defaultFlagValue = kFallbackString; - id targetFlagValue = defaultFlagValue; - [self configureUserWithFlagConfigModelFromJsonFileNamed:@"stringConfigIsAString-someString"]; - [[self.dataManagerMock reject] createFlagEvaluationEventsWithFlagKey:[OCMArg any] - reportedFlagValue:[OCMArg any] - flagConfigValue:[OCMArg any] - defaultFlagValue:[OCMArg any] - user:self.user - config:self.config]; +- (void)testSetOnline_YES { + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[LDClient sharedInstance] setOnline:NO]; + //The throttler mock is set to execute blocks. Setting the expectation on the environment mock verifies that the client is calling the throttler + [[self.primaryEnvironmentMock expect] setOnline:YES]; + __block NSInteger completionCallCount = 0; - NSString *flagValue = [[LDClient sharedInstance] stringVariation:flagKey fallback:defaultFlagValue]; + [[LDClient sharedInstance] setOnline:YES completion: ^{ + completionCallCount += 1; + }]; - XCTAssertEqualObjects(flagValue, targetFlagValue); - [self.dataManagerMock verify]; + XCTAssertTrue([LDClient sharedInstance].isOnline); + XCTAssertEqual(completionCallCount, 1); + [self.primaryEnvironmentMock verify]; } -#pragma mark Array Variation -- (void)testArrayVariation_knownFlag { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = @"isAnArray"; - id defaultFlagValue = @[@1, @2]; - LDFlagConfigModel *flagConfigModel = [self configureUserWithFlagConfigModelFromJsonFileNamed:@"arrayConfigIsAnArray-123"]; - LDFlagConfigValue *flagConfigValue = [flagConfigModel flagConfigValueForFlagKey:flagKey]; - id targetFlagValue = flagConfigValue.value; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:targetFlagValue - flagConfigValue:flagConfigValue - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - NSArray *flagValue = [[LDClient sharedInstance] arrayVariation:flagKey fallback:defaultFlagValue]; +- (void)testSetOnline_YES_withMultipleEnvironments { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[LDClient sharedInstance] setOnline:NO]; + //The throttler mock is set to execute blocks. Setting the expectation on the environment mock verifies that the client is calling the throttler + [[self.primaryEnvironmentMock expect] setOnline:YES]; + __block NSInteger completionCallCount = 0; - XCTAssertEqualObjects(flagValue, targetFlagValue); - [self.dataManagerMock verify]; -} - -- (void)testArrayVariation_knownFlag_nullValue { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = kLDFlagKeyIsAnArray; - id defaultFlagValue = [LDFlagConfigValue defaultValueForFlagKey:flagKey]; - LDFlagConfigModel *flagConfigModel = [self configureUserWithFlagConfigModelFromJsonFileNamed:@"arrayConfigIsAnArray-123"]; - LDFlagConfigValue *flagConfigValue = [flagConfigModel flagConfigValueForFlagKey:flagKey]; - flagConfigValue.value = [NSNull null]; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:defaultFlagValue - flagConfigValue:flagConfigValue - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - NSArray *flagValue = [[LDClient sharedInstance] arrayVariation:flagKey fallback:defaultFlagValue]; - - XCTAssertEqualObjects(flagValue, defaultFlagValue); - [self.dataManagerMock verify]; -} - -- (void)testArrayVariation_unknownFlag { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = @"dummy-flag-key"; - id defaultFlagValue = @[@1, @2]; - id targetFlagValue = defaultFlagValue; - [self configureUserWithFlagConfigModelFromJsonFileNamed:@"arrayConfigIsAnArray-123"]; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:defaultFlagValue - flagConfigValue:nil - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - NSArray *flagValue = [[LDClient sharedInstance] arrayVariation:flagKey fallback:defaultFlagValue]; + [[LDClient sharedInstance] setOnline:YES completion: ^{ + completionCallCount += 1; + }]; - XCTAssertEqualObjects(flagValue, targetFlagValue); - [self.dataManagerMock verify]; -} + XCTAssertTrue([LDClient sharedInstance].isOnline); + XCTAssertEqual(completionCallCount, 1); + [self.primaryEnvironmentMock verify]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + XCTAssertEqual(environmentMock.setOnlineCallCount, 3); //1st call from start, 2nd call from setOnline:NO in setup, 3rd call from setOnline:YES under test + XCTAssertEqual(environmentMock.setOnlineCalledValueOnline, YES); //measures only the last setOnline call + } +} + +- (void)testSetOnline_YES_withMultipleEnvironments_mismatchedPrimaryEnvironment { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[[self.primaryEnvironmentMock stub] andReturnValue:@NO] isOnline]; + //The throttler mock is set to execute blocks. Setting the expectation on the environment mock verifies that the client is calling the throttler + [[self.primaryEnvironmentMock expect] setOnline:YES]; + __block NSInteger completionCallCount = 0; -- (void)testArrayVariation_knownFlag_withoutStart { - NSString *flagKey = @"isAnArray"; - id defaultFlagValue = @[@1, @2]; - id targetFlagValue = defaultFlagValue; - [self configureUserWithFlagConfigModelFromJsonFileNamed:@"arrayConfigIsAnArray-123"]; - [[self.dataManagerMock reject] createFlagEvaluationEventsWithFlagKey:[OCMArg any] - reportedFlagValue:[OCMArg any] - flagConfigValue:[OCMArg any] - defaultFlagValue:[OCMArg any] - user:self.user - config:self.config]; + [[LDClient sharedInstance] setOnline:YES completion: ^{ + completionCallCount += 1; + }]; - NSArray *flagValue = [[LDClient sharedInstance] arrayVariation:flagKey fallback:defaultFlagValue]; + XCTAssertTrue([LDClient sharedInstance].isOnline); + XCTAssertEqual(completionCallCount, 1); + [self.primaryEnvironmentMock verify]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + XCTAssertEqual(environmentMock.setOnlineCallCount, 2); //1st call from start, 2nd call from setOnline:YES under test + XCTAssertEqual(environmentMock.setOnlineCalledValueOnline, YES); //measures only the last setOnline call + } +} + +- (void)testSetOnline_YES_withMultipleEnvironments_mismatchedSecondaryEnvironment { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[[self.primaryEnvironmentMock stub] andReturnValue:@YES] isOnline]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + environmentMock.reportOnline = YES; + } + self.secondaryEnvironmentMocks.allValues.lastObject.reportOnline = NO; + //The throttler mock is set to execute blocks. Setting the expectation on the environment mock verifies that the client is calling the throttler + [[self.primaryEnvironmentMock expect] setOnline:YES]; + __block NSInteger completionCallCount = 0; - XCTAssertEqualObjects(flagValue, targetFlagValue); - [self.dataManagerMock verify]; -} + [[LDClient sharedInstance] setOnline:YES completion: ^{ + completionCallCount += 1; + }]; -#pragma mark Dictionary Variation -- (void)testDictionaryVariation_knownFlag { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = @"isADictionary"; - id defaultFlagValue = @{@"key1": @"value1", @"key2": @[@1, @2]}; - LDFlagConfigModel *flagConfigModel = [self configureUserWithFlagConfigModelFromJsonFileNamed:@"dictionaryConfigIsADictionary-3Key"]; - LDFlagConfigValue *flagConfigValue = [flagConfigModel flagConfigValueForFlagKey:flagKey]; - id targetFlagValue = flagConfigValue.value; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:targetFlagValue - flagConfigValue:flagConfigValue - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - NSDictionary *flagValue = [[LDClient sharedInstance] dictionaryVariation:flagKey fallback:defaultFlagValue]; + XCTAssertTrue([LDClient sharedInstance].isOnline); + XCTAssertEqual(completionCallCount, 1); + [self.primaryEnvironmentMock verify]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + XCTAssertEqual(environmentMock.setOnlineCallCount, 2); //1st call from start, 2nd call from setOnline:YES under test + XCTAssertEqual(environmentMock.setOnlineCalledValueOnline, YES); //measures only the last setOnline call + } +} + +- (void)testSetOnline_YES_withMultipleEnvironments_alreadyOnline { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[[self.primaryEnvironmentMock stub] andReturnValue:@YES] isOnline]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + environmentMock.reportOnline = YES; + } + //The throttler mock is set to execute blocks. Setting the expectation on the environment mock verifies that the client is calling the throttler + [[self.primaryEnvironmentMock reject] setOnline:[OCMArg any]]; + __block NSInteger completionCallCount = 0; - XCTAssertEqualObjects(flagValue, targetFlagValue); - [self.dataManagerMock verify]; -} - -- (void)testDictionaryVariation_knownFlag_nullValue { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = kLDFlagKeyIsADictionary; - id defaultFlagValue = [LDFlagConfigValue defaultValueForFlagKey:flagKey]; - LDFlagConfigModel *flagConfigModel = [self configureUserWithFlagConfigModelFromJsonFileNamed:@"dictionaryConfigIsADictionary-3Key"]; - LDFlagConfigValue *flagConfigValue = [flagConfigModel flagConfigValueForFlagKey:flagKey]; - flagConfigValue.value = [NSNull null]; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:defaultFlagValue - flagConfigValue:flagConfigValue - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - NSDictionary *flagValue = [[LDClient sharedInstance] dictionaryVariation:flagKey fallback:defaultFlagValue]; - - XCTAssertEqualObjects(flagValue, defaultFlagValue); - [self.dataManagerMock verify]; -} - -- (void)testDictionaryVariation_unknownFlag { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - NSString *flagKey = @"dummy-flag-key"; - id defaultFlagValue = @{@"key1": @"value1", @"key2": @[@1, @2]}; - id targetFlagValue = defaultFlagValue; - [self configureUserWithFlagConfigModelFromJsonFileNamed:@"dictionaryConfigIsADictionary-3Key"]; - [[self.dataManagerMock expect] createFlagEvaluationEventsWithFlagKey:flagKey - reportedFlagValue:defaultFlagValue - flagConfigValue:nil - defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; - - NSDictionary *flagValue = [[LDClient sharedInstance] dictionaryVariation:flagKey fallback:defaultFlagValue]; + [[LDClient sharedInstance] setOnline:YES completion: ^{ + completionCallCount += 1; + }]; - XCTAssertEqualObjects(flagValue, targetFlagValue); - [self.dataManagerMock verify]; + XCTAssertTrue([LDClient sharedInstance].isOnline); + XCTAssertEqual(completionCallCount, 1); + [self.primaryEnvironmentMock verify]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + XCTAssertEqual(environmentMock.setOnlineCallCount, 1); //1st call from start + XCTAssertEqual(environmentMock.setOnlineCalledValueOnline, YES); //measures only the last setOnline call + } } -- (void)testDictionaryVariation_knownFlag_withoutStart { - NSString *flagKey = @"isADictionary"; - id defaultFlagValue = @{@"key1": @"value1", @"key2": @[@1, @2]}; - id targetFlagValue = defaultFlagValue; - [self configureUserWithFlagConfigModelFromJsonFileNamed:@"dictionaryConfigIsADictionary-3Key"]; - [[self.dataManagerMock reject] createFlagEvaluationEventsWithFlagKey:[OCMArg any] - reportedFlagValue:[OCMArg any] - flagConfigValue:[OCMArg any] - defaultFlagValue:[OCMArg any] - user:self.user - config:self.config]; +- (void)testSetOnline_NO { + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[self.throttlerMock reject] runThrottled:[OCMArg any]]; + [[self.primaryEnvironmentMock expect] setOnline:NO]; + __block NSInteger completionCallCount = 0; - NSDictionary *flagValue = [[LDClient sharedInstance] dictionaryVariation:flagKey fallback:defaultFlagValue]; + [[LDClient sharedInstance] setOnline:NO completion: ^{ + completionCallCount += 1; + }]; - XCTAssertEqualObjects(flagValue, targetFlagValue); - [self.dataManagerMock verify]; + XCTAssertFalse([LDClient sharedInstance].isOnline); + XCTAssertEqual(completionCallCount, 1); + [self.throttlerMock verify]; + [self.primaryEnvironmentMock verify]; } -#pragma mark - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" -- (void)testDeprecatedStartWithValidConfig { - LDConfigBuilder *builder = [[LDConfigBuilder alloc] init]; - [builder withMobileKey:kTestMobileKey]; - LDClient *client = [LDClient sharedInstance]; - BOOL didStart = [client start:builder userBuilder:nil]; - XCTAssertTrue(didStart); -} +- (void)testSetOnline_NO_withMultipleEnvironments { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[self.throttlerMock reject] runThrottled:[OCMArg any]]; + [[self.primaryEnvironmentMock expect] setOnline:NO]; + __block NSInteger completionCallCount = 0; -- (void)testDeprecatedStartWithValidConfigMultipleTimes { - LDConfigBuilder *builder = [[LDConfigBuilder alloc] init]; - [builder withMobileKey:kTestMobileKey]; - XCTAssertTrue([[LDClient sharedInstance] start:builder userBuilder:nil]); - XCTAssertFalse([[LDClient sharedInstance] start:builder userBuilder:nil]); -} + [[LDClient sharedInstance] setOnline:NO completion: ^{ + completionCallCount += 1; + }]; -- (void)testDeprecatedBoolVariationWithStart { - LDConfigBuilder *builder = [[LDConfigBuilder alloc] init]; - [builder withMobileKey:kTestMobileKey]; - [[LDClient sharedInstance] start:builder userBuilder:nil]; - BOOL boolValue = [[LDClient sharedInstance] boolVariation:@"test" fallback:YES]; - XCTAssertTrue(boolValue); + XCTAssertFalse([LDClient sharedInstance].isOnline); + XCTAssertEqual(completionCallCount, 1); + [self.throttlerMock verify]; + [self.primaryEnvironmentMock verify]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + XCTAssertEqual(environmentMock.setOnlineCallCount, 2); //1st call from start, 2nd call from setOnline:NO under test + XCTAssertEqual(environmentMock.setOnlineCalledValueOnline, NO); //measures only the last setOnline call + } } -#pragma clang diagnostic pop -- (void)testUserPersisted { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - - LDUserBuilder *userBuilder = [[LDUserBuilder alloc] init]; - userBuilder.key = @"myKey"; - userBuilder.email = @"my@email.com"; - - [[LDClient sharedInstance] start:config withUserBuilder:userBuilder]; - BOOL toggleValue = [[LDClient sharedInstance] boolVariation:@"test" fallback:YES]; - XCTAssertTrue(toggleValue); - - LDUserBuilder *anotherUserBuilder = [[LDUserBuilder alloc] init]; - anotherUserBuilder.key = @"myKey"; - - LDUserModel *user = [[LDClient sharedInstance] ldUser]; - OCMStub([self.dataManagerMock findUserWithkey:[OCMArg any]]).andReturn(user); - - [[LDClient sharedInstance] start:config withUserBuilder:anotherUserBuilder]; - user = [[LDClient sharedInstance] ldUser]; - - XCTAssertEqual(user.email, @"my@email.com"); -} - -- (void)testTrackWithoutStart { - XCTAssertFalse([[LDClient sharedInstance] track:@"test" data:nil]); -} +- (void)testSetOnline_NO_withMultipleEnvironments_mismatchedPrimaryEnvironment { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[LDClient sharedInstance] setOnline:NO]; + [[[self.primaryEnvironmentMock stub] andReturnValue:@YES] isOnline]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + environmentMock.reportOnline = NO; + } + [[self.throttlerMock reject] runThrottled:[OCMArg any]]; + [[self.primaryEnvironmentMock expect] setOnline:NO]; + __block NSInteger completionCallCount = 0; -- (void)testTrackWithStart { - NSDictionary *customData = @{@"key": @"value"}; - LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - [[LDClient sharedInstance] start:config withUserBuilder:nil]; - - OCMStub([self.dataManagerMock createCustomEventWithKey:[OCMArg isKindOfClass:[NSString class]] customData:[OCMArg isKindOfClass:[NSDictionary class]] user:[OCMArg any] config:[OCMArg any]]); - - XCTAssertTrue([[LDClient sharedInstance] track:@"test" data:customData]); - - OCMVerify([self.dataManagerMock createCustomEventWithKey: @"test" customData: customData user:[OCMArg isKindOfClass:[LDUserModel class]] config:config]); + [[LDClient sharedInstance] setOnline:NO completion: ^{ + completionCallCount += 1; + }]; + + XCTAssertFalse([LDClient sharedInstance].isOnline); + XCTAssertEqual(completionCallCount, 1); + [self.throttlerMock verify]; + [self.primaryEnvironmentMock verify]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + XCTAssertEqual(environmentMock.setOnlineCallCount, 3); //1st call from start, 2nd call from setOnline:NO in setup, 3rd call from setOnline:NO under test + XCTAssertEqual(environmentMock.setOnlineCalledValueOnline, NO); //measures only the last setOnline call + } } -- (void)testSetOnline_NO_beforeStart { - [[self.clientManagerMock reject] setOnline:[OCMArg any]]; +- (void)testSetOnline_NO_withMultipleEnvironments_mismatchedSecondaryEnvironment { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[LDClient sharedInstance] setOnline:NO]; + [[[self.primaryEnvironmentMock stub] andReturnValue:@NO] isOnline]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + environmentMock.reportOnline = NO; + } + self.secondaryEnvironmentMocks.allValues.lastObject.reportOnline = YES; + [[self.throttlerMock reject] runThrottled:[OCMArg any]]; + [[self.primaryEnvironmentMock expect] setOnline:NO]; __block NSInteger completionCallCount = 0; [[LDClient sharedInstance] setOnline:NO completion: ^{ @@ -710,15 +413,25 @@ - (void)testSetOnline_NO_beforeStart { }]; XCTAssertFalse([LDClient sharedInstance].isOnline); - [self.clientManagerMock verify]; XCTAssertEqual(completionCallCount, 1); + [self.throttlerMock verify]; + [self.primaryEnvironmentMock verify]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + XCTAssertEqual(environmentMock.setOnlineCallCount, 3); //1st call from start, 2nd call from setOnline:NO in setup, 3rd call from setOnline:NO under test + XCTAssertEqual(environmentMock.setOnlineCalledValueOnline, NO); //measures only the last setOnline call + } } -- (void)testSetOnline_NO_afterStart { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - [[LDClient sharedInstance] start:config withUserBuilder:nil]; +- (void)testSetOnline_NO_withMultipleEnvironments_alreadyOffline { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[LDClient sharedInstance] setOnline:NO]; + [[[self.primaryEnvironmentMock stub] andReturnValue:@NO] isOnline]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + environmentMock.reportOnline = NO; + } [[self.throttlerMock reject] runThrottled:[OCMArg any]]; - [[self.clientManagerMock expect] setOnline:NO]; + [[self.primaryEnvironmentMock reject] setOnline:[OCMArg any]]; __block NSInteger completionCallCount = 0; [[LDClient sharedInstance] setOnline:NO completion: ^{ @@ -726,13 +439,18 @@ - (void)testSetOnline_NO_afterStart { }]; XCTAssertFalse([LDClient sharedInstance].isOnline); - [self.clientManagerMock verify]; - [self.throttlerMock verify]; XCTAssertEqual(completionCallCount, 1); + [self.throttlerMock verify]; + [self.primaryEnvironmentMock verify]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + XCTAssertEqual(environmentMock.setOnlineCallCount, 2); //1st call from start, 2nd call from setOnline:NO in setup + XCTAssertEqual(environmentMock.setOnlineCalledValueOnline, NO); //measures only the last setOnline call + } } - (void)testSetOnline_YES_beforeStart { - [[self.clientManagerMock reject] setOnline:[OCMArg any]]; + [[self.throttlerMock reject] runThrottled:[OCMArg any]]; + [[self.primaryEnvironmentMock reject] setOnline:[OCMArg any]]; __block NSInteger completionCallCount = 0; [[LDClient sharedInstance] setOnline:YES completion: ^{ @@ -740,247 +458,433 @@ - (void)testSetOnline_YES_beforeStart { }]; XCTAssertFalse([LDClient sharedInstance].isOnline); - [self.clientManagerMock verify]; XCTAssertEqual(completionCallCount, 1); + [self.throttlerMock verify]; + [self.primaryEnvironmentMock verify]; } -- (void)testSetOnline_YES_afterStart { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - [[LDClient sharedInstance] start:config withUserBuilder:nil]; - [[LDClient sharedInstance] setOnline:NO]; - [[self.clientManagerMock expect] setOnline:YES]; +- (void)testSetOnline_NO_beforeStart { + [[self.throttlerMock reject] runThrottled:[OCMArg any]]; + [[self.primaryEnvironmentMock reject] setOnline:[OCMArg any]]; __block NSInteger completionCallCount = 0; - //The throttler mock expectation is not getting fulfilled even though the LDClient does invoke it. - //Since the throttler mock is set to execute blocks, setting the expectation on the client manager mock verifies that the client is calling the throttler - [[LDClient sharedInstance] setOnline:YES completion: ^{ + [[LDClient sharedInstance] setOnline:NO completion: ^{ completionCallCount += 1; }]; - [self.clientManagerMock verify]; + XCTAssertFalse([LDClient sharedInstance].isOnline); XCTAssertEqual(completionCallCount, 1); + [self.throttlerMock verify]; + [self.primaryEnvironmentMock verify]; } -- (void)testFlushWithoutStart { - XCTAssertFalse([[LDClient sharedInstance] flush]); +- (void)testFlush { + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[[self.primaryEnvironmentMock expect] andReturnValue:@YES] flush]; + + XCTAssertTrue([[LDClient sharedInstance] flush]); + + [self.primaryEnvironmentMock verify]; } -- (void)testFlushWithStart { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - [[LDClient sharedInstance] start:config withUserBuilder:nil]; +- (void)testFlush_multipleEnvironments { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[[self.primaryEnvironmentMock expect] andReturnValue:@YES] flush]; + XCTAssertTrue([[LDClient sharedInstance] flush]); + + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + XCTAssertEqual(environmentMock.flushCallCount, 1); + } + [self.primaryEnvironmentMock verify]; +} + +- (void)testFlush_withoutStart { + [[self.primaryEnvironmentMock reject] flush]; + + XCTAssertFalse([[LDClient sharedInstance] flush]); + + [self.primaryEnvironmentMock verify]; } - (void)testStopClient { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - [[LDClient sharedInstance] start:config withUserBuilder:nil]; - [[self.clientManagerMock expect] setOnline:NO]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[self.primaryEnvironmentMock expect] setOnline:NO]; + [[self.primaryEnvironmentMock expect] stop]; XCTAssertTrue([[LDClient sharedInstance] stopClient]); - XCTAssertFalse([[LDClient sharedInstance] clientStarted]); - OCMVerifyAll(self.clientManagerMock); + + XCTAssertEqual([LDClient sharedInstance].clientStarted, NO); + XCTAssertEqual([LDClient sharedInstance].isOnline, NO); + XCTAssertNil([LDClient sharedInstance].primaryEnvironment); + [self.primaryEnvironmentMock verify]; } -- (void)testUpdateUser_withoutStart { - [[self.clientManagerMock reject] updateUser]; - [[self.dataManagerMock reject] createIdentifyEventWithUser:[OCMArg any] config:[OCMArg any]]; +- (void)testStopClient_withSecondaryEnvironments { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[self.primaryEnvironmentMock expect] setOnline:NO]; + [[self.primaryEnvironmentMock expect] stop]; - XCTAssertFalse([[LDClient sharedInstance] updateUser:[[LDUserBuilder alloc] init]]); + XCTAssertTrue([[LDClient sharedInstance] stopClient]); - [self.clientManagerMock verify]; - [self.dataManagerMock verify]; + XCTAssertEqual([LDClient sharedInstance].clientStarted, NO); + XCTAssertEqual([LDClient sharedInstance].isOnline, NO); + XCTAssertNil([LDClient sharedInstance].primaryEnvironment); + [self.primaryEnvironmentMock verify]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + XCTAssertEqual(environmentMock.stopCallCount, 1); + XCTAssertEqual(environmentMock.setOnlineCallCount, 2); //1st call from start. 2nd call from stop. + XCTAssertEqual(environmentMock.setOnlineCalledValueOnline, NO); //Measures last setOnline call + } + XCTAssertEqual([LDClient sharedInstance].secondaryEnvironments.count, 0); //Secondary environments removed } -- (void)testUpdateUser_withoutBuilder { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - [[self.clientManagerMock reject] updateUser]; - [[self.dataManagerMock reject] createIdentifyEventWithUser:[OCMArg any] config:[OCMArg any]]; +-(void)testStopClient_withoutStart { + [[self.primaryEnvironmentMock reject] setOnline:[OCMArg any]]; + [[self.primaryEnvironmentMock reject] stop]; - XCTAssertFalse([[LDClient sharedInstance] updateUser:nil]); + XCTAssertFalse([[LDClient sharedInstance] stopClient]); - [self.clientManagerMock verify]; - [self.dataManagerMock verify]; -} - --(void)testUpdateUser_sameUser { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - //Configure the builder with the same user key, and an updated name - [[[self.userBuilderMock stub] andReturn:self.user.key] key]; - NSString *newUserName = @"George Gershwin"; - [[[self.userBuilderMock stub] andReturn:newUserName] name]; - [[[self.userBuilderMock expect] andReturn:self.user] compareNewBuilder:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[LDUserBuilder class]]) { - return NO; - } - LDUserBuilder *userBuilder = obj; - self.user.name = userBuilder.name; //Simulates the compare copying builder properties into the existing user - return [userBuilder isEqual:self.userBuilderMock]; - }] withUser:self.user]; - [[self.dataManagerMock expect] saveUser:self.user]; - [[self.clientManagerMock expect] updateUser]; - [[self.dataManagerMock expect] createIdentifyEventWithUser:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[LDUserModel class]]) { - return NO; - } - LDUserModel *user = obj; - return [user.key isEqualToString:self.user.key] && [user.name isEqualToString:newUserName]; - }] config:self.config]; - - XCTAssertTrue([[LDClient sharedInstance] updateUser:self.userBuilderMock]); - - [self.userBuilderMock verify]; - XCTAssertEqualObjects([LDClient sharedInstance].ldUser, self.user); - [self.clientManagerMock verify]; - [self.dataManagerMock verify]; -} - --(void)testUpdateUser_differentUser { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - LDUserModel *newUser = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; - self.userBuilderMock = nil; //Stop the originally set up builder from mocking because it's set to build into the existing user - id newUserBuilderMock = [OCMockObject niceMockForClass:[LDUserBuilder class]]; - [[[newUserBuilderMock expect] andReturn:newUser] build]; - [[[newUserBuilderMock stub] andReturn:newUser.key] key]; //Give the builder the key for the newUser to trigger the 'different user' part of updateUser - [[self.clientManagerMock expect] updateUser]; - [[self.dataManagerMock expect] createIdentifyEventWithUser:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[LDUserModel class]]) { - return NO; - } - LDUserModel *user = obj; - return [user.key isEqualToString:newUser.key]; - }] config:self.config]; - - XCTAssertTrue([[LDClient sharedInstance] updateUser:newUserBuilderMock]); - - [newUserBuilderMock verify]; - XCTAssertEqualObjects([LDClient sharedInstance].ldUser, newUser); - [self.clientManagerMock verify]; - [self.dataManagerMock verify]; -} - --(void)testUpdateUser_differentUser_builderKeyOmitted { - [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilderMock]; - LDUserModel *newUser = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; - self.userBuilderMock = nil; //Stop the originally set up builder from mocking because it's set to build into the existing user. Omit the key to trigger the 'different user' part of updateUser - id newUserBuilderMock = [OCMockObject niceMockForClass:[LDUserBuilder class]]; - [[[newUserBuilderMock expect] andReturn:newUser] build]; - [[self.clientManagerMock expect] updateUser]; - [[self.dataManagerMock expect] createIdentifyEventWithUser:[OCMArg checkWithBlock:^BOOL(id obj) { - if (![obj isKindOfClass:[LDUserModel class]]) { - return NO; - } - LDUserModel *user = obj; - return [user.key isEqualToString:newUser.key]; - }] config:self.config]; - - XCTAssertTrue([[LDClient sharedInstance] updateUser:newUserBuilderMock]); - - [newUserBuilderMock verify]; - XCTAssertEqualObjects([LDClient sharedInstance].ldUser, newUser); - [self.clientManagerMock verify]; - [self.dataManagerMock verify]; -} - -- (void)testCurrentUserBuilderWithoutStart { - XCTAssertNil([[LDClient sharedInstance] currentUserBuilder]); + XCTAssertEqual([LDClient sharedInstance].clientStarted, NO); + XCTAssertEqual([LDClient sharedInstance].isOnline, NO); + XCTAssertNil([LDClient sharedInstance].primaryEnvironment); + [self.primaryEnvironmentMock verify]; } --(void)testCurrentUserBuilderWithStart { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - LDUserBuilder *userBuilder = [[LDUserBuilder alloc] init]; - - LDClient *ldClient = [LDClient sharedInstance]; - [ldClient start:config withUserBuilder:userBuilder]; - - XCTAssertNotNil([[LDClient sharedInstance] currentUserBuilder]); +#pragma mark Deprecated +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (void)testDeprecatedStartWithValidConfig { + id configBuilderMock = [OCMockObject niceMockForClass:[LDConfigBuilder class]]; + [[[configBuilderMock expect] andReturn:self.config] build]; + [(LDEnvironment*)[self.primaryEnvironmentMock expect] start]; + [[self.primaryEnvironmentMock expect] setOnline:YES]; + + BOOL didStart = [[LDClient sharedInstance] start:configBuilderMock userBuilder:self.userBuilder]; + + XCTAssertTrue(didStart); + XCTAssertTrue([[LDClient sharedInstance].ldUser isEqual:self.user ignoringAttributes:self.ignoredUserAttributes]); + XCTAssertEqualObjects([LDClient sharedInstance].primaryEnvironment, self.primaryEnvironmentMock); //self.environmentMock is configured to return self.environmentMock on a environmentForMobileKey msg + [configBuilderMock verify]; + [self.primaryEnvironmentMock verify]; } -- (void)testDelegateSet { - LDClient *ldClient = [LDClient sharedInstance]; +- (void)testDeprecatedStartWithValidConfigMultipleTimes { + XCTAssertTrue([[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]); + id configBuilderMock = [OCMockObject niceMockForClass:[LDConfigBuilder class]]; + [[[configBuilderMock expect] andReturn:self.config] build]; + [(LDEnvironment*)[self.primaryEnvironmentMock reject] start]; + [[self.primaryEnvironmentMock reject] setOnline:[OCMArg any]]; - ldClient.delegate = (id)self; - XCTAssertEqualObjects(self, ldClient.delegate); + XCTAssertFalse([[LDClient sharedInstance] start:configBuilderMock userBuilder:self.userBuilder]); + + [configBuilderMock verify]; + [self.primaryEnvironmentMock verify]; } -- (void)testServerUnavailableCalled { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; +- (void)testDeprecatedBoolVariationWithStart { + id configBuilderMock = [OCMockObject niceMockForClass:[LDConfigBuilder class]]; + [[[configBuilderMock expect] andReturn:self.config] build]; + [(LDEnvironment*)[self.primaryEnvironmentMock expect] start]; + [[self.primaryEnvironmentMock expect] setOnline:YES]; + [[LDClient sharedInstance] start:configBuilderMock userBuilder:self.userBuilder]; + NSString *flagKey = @"test"; + [[[self.primaryEnvironmentMock expect] andReturnValue:@YES] boolVariation:flagKey fallback:NO]; + + BOOL boolValue = [[LDClient sharedInstance] boolVariation:flagKey fallback:NO]; + + XCTAssertTrue(boolValue); + [configBuilderMock verify]; + [self.primaryEnvironmentMock verify]; +} +#pragma clang diagnostic pop + +#pragma mark - Variations +#pragma mark Bool Variation +- (void)testBoolVariation { + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[[self.primaryEnvironmentMock expect] andReturnValue:@YES] boolVariation:kLDFlagKeyIsABool fallback:NO]; + + BOOL flagValue = [[LDClient sharedInstance] boolVariation:kLDFlagKeyIsABool fallback:NO]; + + XCTAssertEqual(flagValue, YES); + [self.primaryEnvironmentMock verify]; +} + +- (void)testBoolVariation_withoutStart { + [[self.primaryEnvironmentMock reject] boolVariation:[OCMArg any] fallback:[OCMArg any]]; + + BOOL flagValue = [[LDClient sharedInstance] boolVariation:kLDFlagKeyIsABool fallback:YES]; + + XCTAssertEqual(flagValue, YES); + [self.primaryEnvironmentMock verify]; +} + +#pragma mark Number Variation +- (void)testNumberVariation { + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[[self.primaryEnvironmentMock expect] andReturn:@7] numberVariation:kLDFlagKeyIsANumber fallback:@3]; - LDUserBuilder *user = [[LDUserBuilder alloc] init]; - user.key = [[NSUUID UUID] UUIDString]; + NSNumber *flagValue = [[LDClient sharedInstance] numberVariation:kLDFlagKeyIsANumber fallback:@3]; - //Configure the mock delegate to fulfill the flag request expectation - MockLDClientDelegate *delegateMock = [[MockLDClientDelegate alloc] init]; - [LDClient sharedInstance].delegate = delegateMock; + XCTAssertEqualObjects(flagValue, @7); + [self.primaryEnvironmentMock verify]; +} - [[LDClient sharedInstance] start:config withUserBuilder:user]; +- (void)testNumberVariation_withoutStart { + [[self.primaryEnvironmentMock reject] numberVariation:[OCMArg any] fallback:[OCMArg any]]; - [[NSNotificationCenter defaultCenter] postNotificationName: kLDServerConnectionUnavailableNotification object: nil]; + NSNumber *flagValue = [[LDClient sharedInstance] numberVariation:kLDFlagKeyIsANumber fallback:@3]; - XCTAssertTrue(delegateMock.serverConnectionUnavailableCallCount == 1); + XCTAssertEqualObjects(flagValue, @3); + [self.primaryEnvironmentMock verify]; } -- (void)testServerUnavailableNotCalled { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; +#pragma mark Double Variation +- (void)testDoubleVariation { + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[[self.primaryEnvironmentMock expect] andReturnValue:@(3.14159)] doubleVariation:kLDFlagKeyIsADouble fallback:2.71828]; - LDUserBuilder *user = [[LDUserBuilder alloc] init]; - user.key = [[NSUUID UUID] UUIDString]; + double flagValue = [[LDClient sharedInstance] doubleVariation:kLDFlagKeyIsADouble fallback:2.71828]; - //Configure the mock delegate to fulfill the flag request expectation - MockLDClientDelegate *delegateMock = [[MockLDClientDelegate alloc] init]; - [LDClient sharedInstance].delegate = delegateMock; + XCTAssertEqual(flagValue, 3.14159); + [self.primaryEnvironmentMock verify]; +} - [[LDClient sharedInstance] start:config withUserBuilder:user]; +- (void)testDoubleVariation_withoutStart { + [[[self.primaryEnvironmentMock reject] ignoringNonObjectArgs] doubleVariation:[OCMArg any] fallback:0]; - [[NSNotificationCenter defaultCenter] postNotificationName: kLDUserUpdatedNotification object: nil]; + double flagValue = [[LDClient sharedInstance] doubleVariation:kLDFlagKeyIsADouble fallback:2.71828]; - XCTAssertTrue(delegateMock.serverConnectionUnavailableCallCount == 0); - XCTAssertTrue(delegateMock.userDidUpdateCallCount == 1); + XCTAssertEqual(flagValue, 2.71828); + [self.primaryEnvironmentMock verify]; } -- (void)testOfflineOnClientUnauthorizedNotification { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - [[LDClient sharedInstance] start:config withUserBuilder:nil]; +#pragma mark String Variation +- (void)testStringVariation { + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[[self.primaryEnvironmentMock expect] andReturn:@"targetValue"] stringVariation:kLDFlagKeyIsAString fallback:@"fallbackValue"]; - [[self.clientManagerMock expect] setOnline:NO]; + NSString *flagValue = [[LDClient sharedInstance] stringVariation:kLDFlagKeyIsAString fallback:@"fallbackValue"]; - [[NSNotificationCenter defaultCenter] postNotificationName:kLDClientUnauthorizedNotification object:nil]; + XCTAssertEqualObjects(flagValue, @"targetValue"); + [self.primaryEnvironmentMock verify]; +} + +- (void)testStringVariation_withoutStart { + [[[self.primaryEnvironmentMock reject] ignoringNonObjectArgs] stringVariation:[OCMArg any] fallback:@"fallbackValue"]; - [self.clientManagerMock verify]; + NSString *flagValue = [[LDClient sharedInstance] stringVariation:kLDFlagKeyIsAString fallback:@"fallbackValue"]; + + XCTAssertEqual(flagValue, @"fallbackValue"); + [self.primaryEnvironmentMock verify]; } -- (void)testUserUpdatedCalled { - MockLDClientDelegate *delegateMock = [[MockLDClientDelegate alloc] init]; - [LDClient sharedInstance].delegate = delegateMock; +#pragma mark Array Variation +- (void)testArrayVariation { + NSArray *targetFlagValue = @[@3, @7]; + NSArray *fallbackFlagValue = @[@1, @2]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[[self.primaryEnvironmentMock expect] andReturn:targetFlagValue] arrayVariation:kLDFlagKeyIsAnArray fallback:fallbackFlagValue]; - [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserUpdatedNotification object:nil]; + NSArray *flagValue = [[LDClient sharedInstance] arrayVariation:kLDFlagKeyIsAnArray fallback:fallbackFlagValue]; - XCTAssertTrue(delegateMock.userDidUpdateCallCount == 1); + XCTAssertEqualObjects(flagValue, targetFlagValue); + [self.primaryEnvironmentMock verify]; } -- (void)testUserUnchangedCalled { - MockLDClientDelegate *delegateMock = [[MockLDClientDelegate alloc] init]; - [LDClient sharedInstance].delegate = delegateMock; +- (void)testArrayVariation_withoutStart { + NSArray *fallbackFlagValue = @[@1, @2]; + [[self.primaryEnvironmentMock reject] arrayVariation:[OCMArg any] fallback:fallbackFlagValue]; - [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserNoChangeNotification object:nil]; + NSArray *flagValue = [[LDClient sharedInstance] arrayVariation:kLDFlagKeyIsAString fallback:fallbackFlagValue]; - XCTAssertTrue(delegateMock.userUnchangedCallCount == 1); + XCTAssertEqual(flagValue, fallbackFlagValue); + [self.primaryEnvironmentMock verify]; } -#pragma mark - Helpers -- (id)objectFromJsonFileNamed:(NSString*)jsonFileName key:(NSString*)key { - NSDictionary *jsonDictionary = [NSJSONSerialization jsonObjectFromFileNamed:jsonFileName]; - return jsonDictionary[key]; +#pragma mark Dictionary Variation +- (void)testDictionaryVariation { + NSDictionary *targetFlagValue = @{@"keyA":@YES, @"keyB":@[@1, @2, @3], @"keyC": @{@"keyD": @"someStringValue"}}; + NSDictionary *fallbackFlagValue = @{@"key1": @"value1", @"key2": @[@1, @2]}; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[[self.primaryEnvironmentMock expect] andReturn:targetFlagValue] dictionaryVariation:kLDFlagKeyIsADictionary fallback:fallbackFlagValue]; + + NSDictionary *flagValue = [[LDClient sharedInstance] dictionaryVariation:kLDFlagKeyIsADictionary fallback:fallbackFlagValue]; + + XCTAssertEqualObjects(flagValue, targetFlagValue); + [self.primaryEnvironmentMock verify]; } -- (id)valueFromJsonFileNamed:(NSString*)jsonFileName key:(NSString*)key { - return [self objectFromJsonFileNamed:jsonFileName key:key][kLDFlagConfigValueKeyValue]; +- (void)testDictionaryVariation_withoutStart { + NSDictionary *fallbackFlagValue = @{@"key1": @"value1", @"key2": @[@1, @2]}; + [[self.primaryEnvironmentMock reject] dictionaryVariation:[OCMArg any] fallback:[OCMArg any]]; + + NSDictionary *flagValue = [[LDClient sharedInstance] dictionaryVariation:kLDFlagKeyIsADictionary fallback:fallbackFlagValue]; + + XCTAssertEqualObjects(flagValue, fallbackFlagValue); + [self.primaryEnvironmentMock verify]; } --(LDFlagConfigModel*)configureUserWithFlagConfigModelFromJsonFileNamed:(NSString*)fileName { - LDFlagConfigModel *flagConfigModel = [LDFlagConfigModel flagConfigFromJsonFileNamed:fileName]; - [LDClient sharedInstance].ldUser.flagConfig = flagConfigModel; +#pragma mark All Flags +-(void)testAllFlags { + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [[[self.primaryEnvironmentMock expect] andReturn:self.user.flagConfig.allFlagValues] allFlags]; + + NSDictionary *allFlags = [LDClient sharedInstance].allFlags; + + XCTAssertEqualObjects(allFlags, self.user.flagConfig.allFlagValues); + [self.primaryEnvironmentMock verify]; +} + +-(void)testAllFlags_beforeStart { + NSDictionary *allFlags = [LDClient.sharedInstance allFlags]; + + XCTAssertNil(allFlags); +} + +#pragma mark - Event Tracking + +- (void)testTrack { + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + NSDictionary *customData = @{@"key": @"value"}; + [[[self.primaryEnvironmentMock expect] andReturnValue:@YES] track:@"test" data:customData]; + + XCTAssertTrue([[LDClient sharedInstance] track:@"test" data:customData]); + + [self.primaryEnvironmentMock verify]; +} + +- (void)testTrack_withoutStart { + [[self.primaryEnvironmentMock reject] track:[OCMArg any] data:[OCMArg any]]; + + XCTAssertFalse([[LDClient sharedInstance] track:@"test" data:nil]); + + [self.primaryEnvironmentMock verify]; +} + +#pragma mark - User + +-(void)testUpdateUser { + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + LDUserModel *newUser = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + LDUserBuilder *newUserBuilder = [LDUserBuilder currentBuilder:newUser]; + [(LDEnvironment*)[self.primaryEnvironmentMock expect] updateUser:[OCMArg checkWithBlock:^BOOL(id obj) { + return [obj isEqual:newUser ignoringAttributes:self.ignoredUserAttributes]; + }]]; + + XCTAssertTrue([[LDClient sharedInstance] updateUser:newUserBuilder]); + + XCTAssertTrue([[LDClient sharedInstance].ldUser isEqual:newUser ignoringAttributes:self.ignoredUserAttributes]); + [self.primaryEnvironmentMock verify]; +} + +-(void)testUpdateUser_withSecondaryEnvironments { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + LDUserModel *newUser = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + LDUserBuilder *newUserBuilder = [LDUserBuilder currentBuilder:newUser]; + [(LDEnvironment*)[self.primaryEnvironmentMock expect] updateUser:[OCMArg checkWithBlock:^BOOL(id obj) { + return [obj isEqual:newUser ignoringAttributes:self.ignoredUserAttributes]; + }]]; + + XCTAssertTrue([[LDClient sharedInstance] updateUser:newUserBuilder]); + + XCTAssertTrue([[LDClient sharedInstance].ldUser isEqual:newUser ignoringAttributes:self.ignoredUserAttributes]); + [self.primaryEnvironmentMock verify]; + for (LDEnvironmentMock *environmentMock in self.secondaryEnvironmentMocks.allValues) { + XCTAssertEqual(environmentMock.updateUserCallCount, 1); + XCTAssertTrue([environmentMock.updateUserCalledValueNewUser isEqual:newUser ignoringAttributes:self.ignoredUserAttributes]); + } +} + +- (void)testUpdateUser_withoutStart { + [(LDEnvironment*)[self.primaryEnvironmentMock reject] updateUser:[OCMArg any]]; + + XCTAssertFalse([[LDClient sharedInstance] updateUser:[[LDUserBuilder alloc] init]]); + + XCTAssertNil([LDClient sharedInstance].ldUser); + [self.primaryEnvironmentMock verify]; +} + +- (void)testUpdateUser_withoutBuilder { + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + [(LDEnvironment*)[self.primaryEnvironmentMock reject] updateUser:[OCMArg any]]; + + XCTAssertFalse([[LDClient sharedInstance] updateUser:nil]); + + XCTAssertTrue([[LDClient sharedInstance].ldUser isEqual:self.user ignoringAttributes:self.ignoredUserAttributes]); + [self.primaryEnvironmentMock verify]; +} + +-(void)testCurrentUserBuilder { + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + + LDUserBuilder *userBuilder = [[LDClient sharedInstance] currentUserBuilder]; + + LDUserModel *rebuiltUser = [userBuilder build]; + XCTAssertTrue([rebuiltUser isEqual:self.user ignoringAttributes:self.ignoredUserAttributes]); +} + +- (void)testCurrentUserBuilder_withoutStart { + XCTAssertNil([[LDClient sharedInstance] currentUserBuilder]); +} + +#pragma mark - Multiple Environments + +-(void)testEnvironmentForMobileKeyNamed { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + + id primaryEnvironment = [LDClient environmentForMobileKeyNamed:kLDPrimaryEnvironmentName]; + XCTAssertEqualObjects(primaryEnvironment, self.primaryEnvironmentMock); + + for (NSString *environmentName in self.secondaryMobileKeys.allKeys) { + id secondaryEnvironment = [LDClient environmentForMobileKeyNamed:environmentName]; + XCTAssertEqualObjects(secondaryEnvironment, self.secondaryEnvironmentMocks[self.secondaryMobileKeys[environmentName]]); + } +} + +-(void)testEnvironmentForMobileKeyNamed_singleEnvironment { + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + + id primaryEnvironment = [LDClient environmentForMobileKeyNamed:kLDPrimaryEnvironmentName]; + XCTAssertEqualObjects(primaryEnvironment, self.primaryEnvironmentMock); +} + +-(void)testEnvironmentForMobileKeyNamed_badEnvironmentName { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + + XCTAssertThrowsSpecificNamed([LDClient environmentForMobileKeyNamed:@"dummy-environment-name"], NSException, NSInvalidArgumentException); +} + +-(void)testEnvironmentForMobileKeyNamed_missingEnvironmentName { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + + XCTAssertThrowsSpecificNamed([LDClient environmentForMobileKeyNamed:nil], NSException, NSInvalidArgumentException); +} + +-(void)testEnvironmentForMobileKeyNamed_emptyEnvironmentName { + [self setupSecondaryEnvironments]; + [[LDClient sharedInstance] start:self.config withUserBuilder:self.userBuilder]; + + XCTAssertThrowsSpecificNamed([LDClient environmentForMobileKeyNamed:@""], NSException, NSInvalidArgumentException); +} + +-(void)testEnvironmentForMobileKeyNamed_notStarted { + [self setupSecondaryEnvironments]; + + id primaryEnvironment = [LDClient environmentForMobileKeyNamed:kLDPrimaryEnvironmentName]; + XCTAssertNil(primaryEnvironment); - return flagConfigModel; + for (NSString *environmentName in self.secondaryMobileKeys.allKeys) { + id secondaryEnvironment = [LDClient environmentForMobileKeyNamed:environmentName]; + XCTAssertNil(secondaryEnvironment); + } } @end diff --git a/DarklyTests/LDConfigTest.m b/DarklyTests/LDConfigTest.m index 187f5437..3db4ad9b 100644 --- a/DarklyTests/LDConfigTest.m +++ b/DarklyTests/LDConfigTest.m @@ -3,21 +3,24 @@ // #import "LDConfig.h" +#import "LDClient.h" #import "LDConfig+Testable.h" #import "DarklyXCTestCase.h" #import "DarklyConstants.h" +#import "LDUserModel.h" -@interface LDConfigTest : DarklyXCTestCase +NSString * const LDConfigTestMobileKey = @"testMobileKey"; +@interface LDConfigTest : DarklyXCTestCase +@property (nonatomic, strong, readonly) NSArray *environmentSuffixes; +@property (nonatomic, strong) NSDictionary *secondaryMobileKeys; @end -NSString * const LDConfigTestMobileKey = @"testMobileKey"; - @implementation LDConfigTest - (void)setUp { [super setUp]; - // Put setup code here. This method is called before the invocation of each test method in the class. + self.secondaryMobileKeys = [LDConfig secondaryMobileKeysStub]; } - (void)tearDown { @@ -49,6 +52,60 @@ - (void)testConfigBuilderDefaultValues { XCTAssertFalse([config debugEnabled]); } +-(void)testSetSecondaryMobileKeys { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; + + config.secondaryMobileKeys = self.secondaryMobileKeys; + + XCTAssertEqualObjects(config.secondaryMobileKeys, self.secondaryMobileKeys); +} + +-(void)testSetSecondaryMobileKeys_containsPrimaryMobileKey { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; + NSMutableDictionary *secondaryMobileKeys = [NSMutableDictionary dictionaryWithDictionary:self.secondaryMobileKeys]; + NSString *badEnvironmentName = self.secondaryMobileKeys.allKeys.firstObject; + secondaryMobileKeys[badEnvironmentName] = LDConfigTestMobileKey; + self.secondaryMobileKeys = [secondaryMobileKeys copy]; + + XCTAssertThrowsSpecificNamed(config.secondaryMobileKeys = self.secondaryMobileKeys, NSException, NSInvalidArgumentException); +} + +-(void)testSetSecondaryMobileKeys_containsPrimaryEnvironmentName { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; + NSMutableDictionary *secondaryMobileKeys = [NSMutableDictionary dictionaryWithDictionary:self.secondaryMobileKeys]; + secondaryMobileKeys[kLDPrimaryEnvironmentName] = [NSString stringWithFormat:@"%@.%@", LDConfigTestSecondaryMobileKeyMock, @"Z"]; + self.secondaryMobileKeys = [secondaryMobileKeys copy]; + + XCTAssertThrowsSpecificNamed(config.secondaryMobileKeys = self.secondaryMobileKeys, NSException, NSInvalidArgumentException); +} + +-(void)testSetSecondaryMobileKeys_containsSameMobileKeyTwice { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; + NSMutableDictionary *secondaryMobileKeys = [NSMutableDictionary dictionaryWithDictionary:self.secondaryMobileKeys]; + secondaryMobileKeys[[NSString stringWithFormat:@"%@.%@", LDConfigTestEnvironmentNameMock, @"Z"]] = self.secondaryMobileKeys.allValues.firstObject; + self.secondaryMobileKeys = [secondaryMobileKeys copy]; + + XCTAssertThrowsSpecificNamed(config.secondaryMobileKeys = self.secondaryMobileKeys, NSException, NSInvalidArgumentException); +} + +-(void)testSetSecondaryMobileKeys_containsEmptyEnvironmentName { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; + NSMutableDictionary *secondaryMobileKeys = [NSMutableDictionary dictionaryWithDictionary:self.secondaryMobileKeys]; + secondaryMobileKeys[@""] = [NSString stringWithFormat:@"%@.%@", LDConfigTestSecondaryMobileKeyMock, @"Z"]; + self.secondaryMobileKeys = [secondaryMobileKeys copy]; + + XCTAssertThrowsSpecificNamed(config.secondaryMobileKeys = self.secondaryMobileKeys, NSException, NSInvalidArgumentException); +} + +-(void)testSetSecondaryMobileKeys_containsEmptyMobileKey { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; + NSMutableDictionary *secondaryMobileKeys = [NSMutableDictionary dictionaryWithDictionary:self.secondaryMobileKeys]; + secondaryMobileKeys[[NSString stringWithFormat:@"%@.%@", LDConfigTestEnvironmentNameMock, @"Z"]] = @""; + self.secondaryMobileKeys = [secondaryMobileKeys copy]; + + XCTAssertThrowsSpecificNamed(config.secondaryMobileKeys = self.secondaryMobileKeys, NSException, NSInvalidArgumentException); +} + - (void)testConfigOverrideBaseUrl { NSString *testBaseUrl = @"testBaseUrl"; LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; diff --git a/DarklyTests/LDEnvironmentControllerTest.m b/DarklyTests/LDEnvironmentControllerTest.m new file mode 100644 index 00000000..6fd39d43 --- /dev/null +++ b/DarklyTests/LDEnvironmentControllerTest.m @@ -0,0 +1,1515 @@ +// +// Copyright © 2015 Catamorphic Co. All rights reserved. +// + +#import "DarklyXCTestCase.h" +#import "LDEnvironmentController.h" +#import "LDUserBuilder.h" +#import "OCMock.h" +#import "LDRequestManager.h" +#import "LDDataManager.h" +#import "LDPollingManager.h" +#import "LDEventModel.h" +#import "LDEventModel+Testable.h" +#import "LDEnvironmentController+EventSource.h" +#import "LDEvent+Testable.h" +#import "LDEvent+EventTypes.h" +#import "LDFlagConfigModel+Testable.h" +#import "LDFlagConfigValue.h" +#import "LDFlagConfigValue+Testable.h" +#import "LDEventTrackingContext.h" +#import "LDEventTrackingContext+Testable.h" +#import "LDUserModel+Testable.h" +#import "NSJSONSerialization+Testable.h" +#import "LDFlagConfigTracker+Testable.h" +#import "NSDate+ReferencedDate.h" +#import "NSDate+Testable.h" +#import "NSDateFormatter+JsonHeader+Testable.h" +#import "NSDate+ReferencedDate.h" +#import "NSDictionary+LaunchDarkly.h" +#import "LDEnvironmentController+Testable.h" +#import "LDUtil.h" + +extern NSString * _Nonnull const kLDFlagConfigValueKeyValue; +extern NSString * _Nonnull const kLDFlagConfigValueKeyVersion; +extern NSString * _Nonnull const kLDStreamPath; + +NSString *const mockMobileKey = @"mockMobileKey"; +NSString *const kFeaturesJsonDictionary = @"featuresJsonDictionary"; +NSString *const kBoolFlagKey = @"isABawler"; + +@interface LDEnvironmentController (LDEnvironmentControllerTest) +-(void)startPolling; +-(void)stopPolling; +-(void)configureEventSource; +-(void)syncWithServerForEvents; +-(void)syncWithServerForConfig; +-(void)processedEvents:(BOOL)success jsonEventArray:(NSArray*)jsonEventArray responseDate:(NSDate*)responseDate; +-(void)processedConfig:(BOOL)success jsonConfigDictionary:(NSDictionary *)jsonConfigDictionary; +-(void)willEnterBackground; +-(void)willEnterForeground; +- (void)backgroundFetchInitiated; +@end + +@interface LDEnvironmentControllerTest : DarklyXCTestCase +@property (nonatomic, strong) LDConfig *config; +@property (nonatomic, strong) LDUserModel *user; +@property (nonatomic, strong) NSString *encodedUserString; +@property (nonatomic, strong) NSData *encodedUserData; +@property (nonatomic, strong) NSArray *events; +@property (nonatomic, strong) LDEnvironmentController *environmentController; +@property (nonatomic) id requestManagerMock; +@property (nonatomic) id dataManagerMock; +@property (nonatomic) id pollingManagerMock; +@property (nonatomic) id eventSourceMock; +@property (nonatomic, strong) NSDictionary *notificationUserInfo; + +@end + +@implementation LDEnvironmentControllerTest + +- (void)setUp { + [super setUp]; + + self.config = [[LDConfig alloc] initWithMobileKey:mockMobileKey]; + self.config.flushInterval = @(30); + + self.user = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + NSString *userJsonString = [[self.user dictionaryValueWithPrivateAttributesAndFlagConfig:NO] jsonString]; + self.encodedUserString = [LDUtil base64UrlEncodeString:userJsonString]; + self.encodedUserData = [userJsonString dataUsingEncoding:NSUTF8StringEncoding]; + + self.events = [LDEventModel stubEventDictionariesForUser:self.user config:self.config]; + + self.requestManagerMock = [OCMockObject niceMockForClass:[LDRequestManager class]]; + [[[self.requestManagerMock stub] andReturn:self.requestManagerMock] requestManagerForMobileKey:[OCMArg any] config:[OCMArg any] delegate:[OCMArg any] callbackQueue:[OCMArg any]]; + + self.dataManagerMock = [OCMockObject niceMockForClass:[LDDataManager class]]; + + self.pollingManagerMock = [OCMockObject niceMockForClass:[LDPollingManager class]]; + [[[self.pollingManagerMock stub] andReturn:self.pollingManagerMock] sharedInstance]; + + self.eventSourceMock = [OCMockObject niceMockForClass:[LDEventSource class]]; + [[[self.eventSourceMock stub] andReturn: self.eventSourceMock] eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]]; + + self.environmentController = [LDEnvironmentController controllerWithMobileKey:mockMobileKey config:self.config user:self.user dataManager:self.dataManagerMock]; + self.notificationUserInfo = @{kLDNotificationUserInfoKeyMobileKey: mockMobileKey}; +} + +- (void)tearDown { + [[NSNotificationCenter defaultCenter] removeObserver:self.environmentController]; //There is some test pollution without this. environmentController is not dealloc'd right away when set to nil + [self.requestManagerMock stopMocking]; + [self.dataManagerMock stopMocking]; + [self.pollingManagerMock stopMocking]; + [self.eventSourceMock stopMocking]; + self.requestManagerMock = nil; + self.dataManagerMock = nil; + self.pollingManagerMock = nil; + self.eventSourceMock = nil; + [super tearDown]; +} + +#pragma mark - Lifecycle + +-(void)testInitAndConstructor { + id notificationCenterMock = [OCMockObject niceMockForClass:[NSNotificationCenter class]]; + [[[notificationCenterMock stub] andReturn:notificationCenterMock] defaultCenter]; + [[notificationCenterMock expect] addObserver:[OCMArg checkWithBlock:^BOOL(id obj) { + return [((LDEnvironmentController*)obj).mobileKey isEqual:self.config.mobileKey]; + }] selector:@selector(willEnterForeground) name:UIApplicationDidBecomeActiveNotification object:nil]; + [[notificationCenterMock expect] addObserver:[OCMArg checkWithBlock:^BOOL(id obj) { + return [((LDEnvironmentController*)obj).mobileKey isEqual:self.config.mobileKey]; + }] selector:@selector(willEnterBackground) name:UIApplicationWillResignActiveNotification object:nil]; + [[notificationCenterMock expect] addObserver:[OCMArg checkWithBlock:^BOOL(id obj) { + return [((LDEnvironmentController*)obj).mobileKey isEqual:self.config.mobileKey]; + }] selector:@selector(backgroundFetchInitiated) name:kLDBackgroundFetchInitiated object:nil]; + [[notificationCenterMock expect] addObserver:[OCMArg checkWithBlock:^BOOL(id obj) { + return [((LDEnvironmentController*)obj).mobileKey isEqual:self.config.mobileKey]; + }] selector:@selector(syncWithServerForConfig) name:kLDFlagConfigTimerFiredNotification object:nil]; + [[notificationCenterMock expect] addObserver:[OCMArg checkWithBlock:^BOOL(id obj) { + return [((LDEnvironmentController*)obj).mobileKey isEqual:self.config.mobileKey]; + }] selector:@selector(syncWithServerForEvents) name:kLDEventTimerFiredNotification object:nil]; + self.cleanup = ^{ + [notificationCenterMock stopMocking]; + }; + + self.environmentController = [LDEnvironmentController controllerWithMobileKey:mockMobileKey config:self.config user:self.user dataManager:self.dataManagerMock]; + + XCTAssertEqualObjects(self.environmentController.mobileKey, self.config.mobileKey); + XCTAssertEqualObjects(self.environmentController.config, self.config); + XCTAssertEqualObjects(self.environmentController.user, self.user); + XCTAssertEqualObjects(self.environmentController.dataManager, self.dataManagerMock); + XCTAssertEqualObjects(self.environmentController.requestManager, self.requestManagerMock); + [notificationCenterMock verify]; +} + +#pragma mark - Control + +- (void)testSetOnline_yes_streaming { + [[self.pollingManagerMock expect] startEventPollingUsingConfig:self.config isOnline:YES]; + self.eventSourceMock = nil; + self.eventSourceMock = [OCMockObject mockForClass:[LDEventSource class]]; + [[[self.eventSourceMock expect] andReturn:self.eventSourceMock] eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]]; + [[self.eventSourceMock expect] onMessage:[OCMArg any]]; + [[self.eventSourceMock expect] onError:[OCMArg any]]; + [[self.requestManagerMock reject] performFeatureFlagRequest:[OCMArg any] isOnline:[OCMArg any]]; + + self.environmentController.online = YES; + + [self.pollingManagerMock verify]; + [self.eventSourceMock verify]; + [self.requestManagerMock verify]; +} + +- (void)testSetOnline_yes_polling { + self.config.streaming = NO; + [[self.pollingManagerMock expect] startEventPollingUsingConfig:self.config isOnline:YES]; + [[self.eventSourceMock reject] eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]]; + [[self.eventSourceMock reject] onMessage:[OCMArg any]]; + [[self.eventSourceMock reject] onError:[OCMArg any]]; + [[self.requestManagerMock expect] performFeatureFlagRequest:self.user isOnline:YES]; + [[self.pollingManagerMock expect] startFlagConfigPollingUsingConfig:self.config isOnline:YES]; + + self.environmentController.online = YES; + + [self.pollingManagerMock verify]; + [self.eventSourceMock verify]; + [self.requestManagerMock verify]; +} + +- (void)testSetOnline_no_streaming { + self.environmentController.online = YES; + [[self.pollingManagerMock expect] stopEventPolling]; + [[self.eventSourceMock expect] close]; + [[self.pollingManagerMock reject] stopFlagConfigPolling]; + + self.environmentController.online = NO; + + XCTAssertNil([self.environmentController eventSource]); + [self.eventSourceMock verify]; + [self.pollingManagerMock verify]; +} + +- (void)testSetOnline_no_polling { + self.config.streaming = NO; + [[self.requestManagerMock expect] performFeatureFlagRequest:self.user isOnline:YES]; + self.environmentController.online = YES; + [[self.pollingManagerMock expect] stopEventPolling]; + [[self.eventSourceMock reject] close]; + [[self.pollingManagerMock expect] stopFlagConfigPolling]; + + self.environmentController.online = NO; + + XCTAssertNil([self.environmentController eventSource]); + [self.eventSourceMock verify]; + [self.pollingManagerMock verify]; +} + +- (void)testStartPolling_online { + [[self.pollingManagerMock expect] startEventPollingUsingConfig:self.config isOnline:YES]; + [[self.eventSourceMock expect] onMessage:[OCMArg isNotNil]]; + + self.environmentController.online = YES; //This triggers startPolling + + [self.pollingManagerMock verify]; + [self.eventSourceMock verify]; +} + +- (void)testStartPolling_offline { + self.environmentController.online = NO; + [[self.pollingManagerMock reject] startEventPollingUsingConfig:[OCMArg any] isOnline:[OCMArg any]]; + + [self.environmentController startPolling]; + + [self.pollingManagerMock verify]; + XCTAssertNil(self.environmentController.eventSource); +} + +- (void)testStopPolling_streaming { + self.environmentController.online = YES; //triggers startPolling + [[self.pollingManagerMock expect] stopEventPolling]; + [[self.eventSourceMock expect] close]; + [[self.dataManagerMock expect] recordSummaryEventWithTracker:self.user.flagConfigTracker]; + [[self.dataManagerMock expect] allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { + ((void (^)(NSArray *))obj)(self.events); + return YES; + }]]; + [[self.requestManagerMock expect] performEventRequest:self.events isOnline:YES]; + + [self.environmentController stopPolling]; + + [self.pollingManagerMock verify]; + [self.eventSourceMock verify]; + [self.dataManagerMock verify]; + [self.requestManagerMock verify]; + XCTAssertNil(self.environmentController.eventSource); +} + +- (void)testStopPolling_polling { + self.config.streaming = NO; + self.environmentController.online = YES; //triggers startPolling + [[self.pollingManagerMock expect] stopEventPolling]; + [[self.pollingManagerMock expect] stopFlagConfigPolling]; + [[self.dataManagerMock expect] recordSummaryEventWithTracker:self.user.flagConfigTracker]; + [[self.dataManagerMock expect] allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { + ((void (^)(NSArray *))obj)(self.events); + return YES; + }]]; + [[self.requestManagerMock expect] performEventRequest:self.events isOnline:YES]; + + [self.environmentController stopPolling]; + + [self.pollingManagerMock verify]; + [self.dataManagerMock verify]; + [self.requestManagerMock verify]; + XCTAssertNil(self.environmentController.eventSource); +} + +- (void)testWillEnterBackground_streaming { + self.environmentController.online = YES; //triggers startPolling + [[self.pollingManagerMock expect] suspendEventPolling]; + [[self.eventSourceMock expect] close]; + [[self.pollingManagerMock reject] suspendFlagConfigPolling]; + [[self.dataManagerMock expect] recordSummaryEventWithTracker:self.user.flagConfigTracker]; + [[self.dataManagerMock expect] allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { + ((void (^)(NSArray *))obj)(self.events); + return YES; + }]]; + [[self.requestManagerMock expect] performEventRequest:self.events isOnline:YES]; + NSDate *triggerDate = [NSDate date]; + + [self.environmentController willEnterBackground]; + + [self.pollingManagerMock verify]; + [self.eventSourceMock verify]; + [self.dataManagerMock verify]; + XCTAssertTrue([self.environmentController.backgroundTime isWithinTimeInterval:1.0 ofDate:triggerDate]); +} + +- (void)testWillEnterBackground_polling { + self.config.streaming = NO; + self.environmentController.online = YES; //triggers startPolling + [[self.pollingManagerMock expect] suspendEventPolling]; + [[self.eventSourceMock reject] close]; + [[self.pollingManagerMock expect] suspendFlagConfigPolling]; + [[self.dataManagerMock expect] recordSummaryEventWithTracker:self.user.flagConfigTracker]; + [[self.dataManagerMock expect] allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { + ((void (^)(NSArray *))obj)(self.events); + return YES; + }]]; + [[self.requestManagerMock expect] performEventRequest:self.events isOnline:YES]; + NSDate *triggerDate = [NSDate date]; + + [self.environmentController willEnterBackground]; + + [self.pollingManagerMock verify]; + [self.eventSourceMock verify]; + [self.dataManagerMock verify]; + XCTAssertTrue([self.environmentController.backgroundTime isWithinTimeInterval:1.0 ofDate:triggerDate]); +} + +- (void)testWillEnterForeground_online_streaming { + self.environmentController.online = YES; + [self.environmentController willEnterBackground]; + [[self.pollingManagerMock expect] resumeEventPollingWhenIsOnline:YES]; + [[self.eventSourceMock expect] onMessage:[OCMArg any]]; + [[self.eventSourceMock expect] onError:[OCMArg any]]; + [[self.requestManagerMock reject] performFeatureFlagRequest:[OCMArg any] isOnline:[OCMArg any]]; + + [self.environmentController willEnterForeground]; + + [self.pollingManagerMock verify]; + [self.eventSourceMock verify]; + [self.requestManagerMock verify]; +} + +- (void)testWillEnterForeground_online_polling { + self.config.streaming = NO; + self.environmentController.online = YES; + [self.environmentController willEnterBackground]; + [[self.pollingManagerMock expect] resumeEventPollingWhenIsOnline:YES]; + [[self.eventSourceMock reject] onMessage:[OCMArg any]]; + [[self.eventSourceMock reject] onError:[OCMArg any]]; + [[self.pollingManagerMock expect] resumeFlagConfigPollingWhenIsOnline:YES]; + //Just going back online won't trigger a flag request here. If the timer fires in the background, it will trigger a flag request on return to the foreground. Can't simulate that here. + + [self.environmentController willEnterForeground]; + + [self.pollingManagerMock verify]; + [self.eventSourceMock verify]; +} + +- (void)testWillEnterForeground_offline { + self.environmentController.online = NO; + [self.environmentController willEnterBackground]; + [[self.pollingManagerMock reject] resumeEventPollingWhenIsOnline:[OCMArg any]]; + [[self.eventSourceMock reject] onMessage:[OCMArg any]]; + [[self.eventSourceMock reject] onError:[OCMArg any]]; + [[self.pollingManagerMock reject] resumeFlagConfigPollingWhenIsOnline:[OCMArg any]]; + [[self.requestManagerMock reject] performFeatureFlagRequest:[OCMArg any] isOnline:[OCMArg any]]; + + [self.environmentController willEnterForeground]; + + [self.pollingManagerMock verify]; + [self.eventSourceMock verify]; + [self.requestManagerMock verify]; +} + +#pragma mark - Streaming + +- (void)testEventSourceConfigured_getMethod { + [self.eventSourceMock stopMocking]; + self.eventSourceMock = [OCMockObject niceMockForClass:[LDEventSource class]]; + __block NSURL *streamUrl; + __block NSString *streamConnectMethod; + __block NSData *streamConnectData; + [[[self.eventSourceMock expect] andReturn:self.eventSourceMock] eventSourceWithURL:[OCMArg checkWithBlock:^BOOL(id obj) { + if (![obj isKindOfClass:[NSURL class]]) { return NO; } + streamUrl = obj; + return YES; + }] httpHeaders:[OCMArg any] connectMethod:[OCMArg checkWithBlock:^BOOL(id obj) { + if (obj && ![obj isKindOfClass:[NSString class]]) { return NO; } + streamConnectMethod = obj; + return YES; + }] connectBody:[OCMArg checkWithBlock:^BOOL(id obj) { + if (obj && ![obj isKindOfClass:[NSData class]]) { return NO; } + streamConnectData = obj; + return YES; + }]]; + + self.environmentController.online = YES; + + XCTAssertTrue([[streamUrl pathComponents] containsObject:kLDStreamPath]); + XCTAssertFalse([[streamUrl pathComponents] containsObject:@"mping"]); + XCTAssertTrue([[streamUrl lastPathComponent] isEqualToString:self.encodedUserString]); + XCTAssertTrue(streamConnectMethod == nil || [streamConnectMethod isEqualToString:@"GET"]); + XCTAssertNil(streamConnectData); + [self.eventSourceMock verify]; +} + +- (void)testEventSourceConfigured_reportMethod { + self.config.useReport = YES; + [self.eventSourceMock stopMocking]; + self.eventSourceMock = [OCMockObject niceMockForClass:[LDEventSource class]]; + __block NSURL *streamUrl; + __block NSString *streamConnectMethod; + __block NSData *streamConnectData; + [[[self.eventSourceMock expect] andReturn:self.eventSourceMock] eventSourceWithURL:[OCMArg checkWithBlock:^BOOL(id obj) { + if (![obj isKindOfClass:[NSURL class]]) { return NO; } + streamUrl = obj; + return YES; + }] httpHeaders:[OCMArg any] connectMethod:[OCMArg checkWithBlock:^BOOL(id obj) { + if (obj && ![obj isKindOfClass:[NSString class]]) { return NO; } + streamConnectMethod = obj; + return YES; + }] connectBody:[OCMArg checkWithBlock:^BOOL(id obj) { + if (obj && ![obj isKindOfClass:[NSData class]]) { return NO; } + streamConnectData = obj; + return YES; + }]]; + + self.environmentController.online = YES; + + XCTAssertTrue([[streamUrl lastPathComponent] isEqualToString:kLDStreamPath]); + XCTAssertFalse([[streamUrl pathComponents] containsObject:@"mping"]); + XCTAssertTrue([streamConnectMethod isEqualToString:kHTTPMethodReport]); + XCTAssertTrue([streamConnectData isEqualToData:self.encodedUserData]); +} + +- (void)testEventSourceNotCreated_offline { + [self.environmentController startPolling]; + + XCTAssertNil(self.environmentController.eventSource); +} + +- (void)testConfigureEventSource_offline { + [self.environmentController configureEventSource]; + + XCTAssertNil(self.environmentController.eventSource); +} + +- (void)testEventSourceRemainsConstantAcrossStartPollingCalls { + int numTries = 5; + self.eventSourceMock = nil; + self.eventSourceMock = [OCMockObject mockForClass:[LDEventSource class]]; + [[[self.eventSourceMock expect] andReturn:self.eventSourceMock] eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]]; + [[self.eventSourceMock expect] onMessage:[OCMArg any]]; + [[self.eventSourceMock expect] onError:[OCMArg any]]; + self.environmentController.online = YES; + + [[self.eventSourceMock reject] eventSourceWithURL:[OCMArg any] httpHeaders:[OCMArg any] connectMethod:[OCMArg any] connectBody:[OCMArg any]]; + for (int i = 0; i < numTries; i++) { + [self.environmentController startPolling]; + } + [self.eventSourceMock verify]; +} + +- (void)testEventSourceRemovedOnStopPolling { + self.environmentController.online = YES; + [[self.eventSourceMock expect] close]; + + [self.environmentController stopPolling]; + + XCTAssertNil(self.environmentController.eventSource); + [self.eventSourceMock verify]; +} + +- (void)testEventSourceRemainsConstantAcrossWillEnterForegroundCalls { + int numTries = 5; + self.environmentController.online = YES; + [self.environmentController startPolling]; + XCTAssertNotNil(self.environmentController.eventSource); + + LDEventSource *eventSource = [self.environmentController eventSource]; + for (int i = 0; i < numTries; i++) { + [self.environmentController willEnterForeground]; + XCTAssert(eventSource == self.environmentController.eventSource); + } +} + +- (void)testSSEPingEvent { + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + self.environmentController.online = YES; + [[self.requestManagerMock expect] performFeatureFlagRequest:self.user isOnline:YES]; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + messageHandler([LDEvent stubPingEvent]); + + [self.requestManagerMock verify]; +} + +- (void)testSSEPutEventSuccess { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserUpdatedNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + __block NSDictionary *featureFlagsChangedNotificationUserInfo; + [[notificationObserver expect] notificationWithName:kLDFeatureFlagsChangedNotification object:[OCMArg any] userInfo:[OCMArg checkWithBlock:^BOOL(id obj) { + featureFlagsChangedNotificationUserInfo = obj; + return YES; + }]]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + LDFlagConfigModel *targetFlagConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"featureFlags-excludeNulls"]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + [[self.dataManagerMock expect] saveUser:[OCMArg checkWithBlock:^BOOL(id obj) { + if (![obj isKindOfClass:[LDUserModel class]]) { return NO; } + LDUserModel *savedUser = obj; + XCTAssertTrue([savedUser.flagConfig isEqualToConfig:targetFlagConfig]); + return YES; + }]]; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + LDEvent *put = [LDEvent stubEvent:kLDEventTypePut fromJsonFileNamed:@"featureFlags-excludeNulls"]; + + messageHandler(put); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:targetFlagConfig]); + XCTAssertFalse(self.user.flagConfig == originalFlagConfig); + [self.dataManagerMock verify]; + [notificationObserver verify]; + XCTAssertNotNil(featureFlagsChangedNotificationUserInfo); + if (featureFlagsChangedNotificationUserInfo != nil) { + XCTAssertEqualObjects(featureFlagsChangedNotificationUserInfo[kLDNotificationUserInfoKeyMobileKey], mockMobileKey); + NSSet *changedFlagKeys = [NSSet setWithArray:[originalFlagConfig differingFlagKeysFromConfig:targetFlagConfig]]; + XCTAssertEqualObjects([NSSet setWithArray:featureFlagsChangedNotificationUserInfo[kLDNotificationUserInfoKeyFlagKeys]], changedFlagKeys); + } +} + +- (void)testSSEPutResultedInNoChange { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + LDFlagConfigModel *targetFlagConfig = [originalFlagConfig copy]; + + [[self.dataManagerMock expect] saveUser:self.user]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + LDEvent *put = [LDEvent stubEvent:kLDEventTypePut fromJsonFileNamed:@"featureFlags"]; + + messageHandler(put); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig: targetFlagConfig]); + XCTAssertFalse(self.user.flagConfig == originalFlagConfig); + [self.dataManagerMock verify]; + [notificationObserver verify]; +} + +- (void)testSSEPutEventFailedNilData { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + LDFlagConfigModel *targetFlagConfig = [originalFlagConfig copy]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + LDEvent *put = [LDEvent stubEvent:kLDEventTypePut fromJsonFileNamed:@"featureFlags"]; + put.data = nil; + + messageHandler(put); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig: targetFlagConfig]); + XCTAssertTrue(self.user.flagConfig == originalFlagConfig); + [self.dataManagerMock verify]; + [notificationObserver verify]; +} + +- (void)testSSEPutEventFailedEmptyData { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + LDFlagConfigModel *targetFlagConfig = [originalFlagConfig copy]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + LDEvent *put = [LDEvent stubEvent:kLDEventTypePut fromJsonFileNamed:@"featureFlags"]; + put.data = @""; + + messageHandler(put); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig: targetFlagConfig]); + XCTAssertTrue(self.user.flagConfig == originalFlagConfig); + [self.dataManagerMock verify]; + [notificationObserver verify]; +} + +- (void)testSSEPutEventFailedInvalidData { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + LDFlagConfigModel *targetFlagConfig = [originalFlagConfig copy]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + LDEvent *put = [LDEvent stubEvent:kLDEventTypePut fromJsonFileNamed:@"featureFlags"]; + put.data = @"{\"someInvalidData\":}"; + + messageHandler(put); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig: targetFlagConfig]); + XCTAssertTrue(self.user.flagConfig == originalFlagConfig); + [self.dataManagerMock verify]; + [notificationObserver verify]; +} + +- (void)testSSEPatchEventSuccess { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserUpdatedNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + __block NSDictionary *featureFlagsChangedNotificationUserInfo; + [[notificationObserver expect] notificationWithName:kLDFeatureFlagsChangedNotification object:[OCMArg any] userInfo:[OCMArg checkWithBlock:^BOOL(id obj) { + featureFlagsChangedNotificationUserInfo = obj; + return YES; + }]]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + self.environmentController.online = YES; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + LDEvent *patch = [LDEvent stubEvent:kLDEventTypePatch fromJsonFileNamed:@"ldEnvironmentControllerTestPatchIsANumber"]; + LDFlagConfigModel *targetFlagConfig = [self.user.flagConfig applySSEEvent:patch]; + + [[self.dataManagerMock expect] saveUser:self.user]; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + messageHandler(patch); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:targetFlagConfig]); + XCTAssertTrue(self.user.flagConfig == originalFlagConfig); //same object with the new flagConfigValue + [self.dataManagerMock verify]; + [notificationObserver verify]; + XCTAssertNotNil(featureFlagsChangedNotificationUserInfo); + if (featureFlagsChangedNotificationUserInfo != nil) { + XCTAssertEqualObjects(featureFlagsChangedNotificationUserInfo[kLDNotificationUserInfoKeyMobileKey], mockMobileKey); + XCTAssertEqualObjects([NSSet setWithArray:featureFlagsChangedNotificationUserInfo[kLDNotificationUserInfoKeyFlagKeys]], [NSSet setWithArray:@[kLDFlagKeyIsANumber]]); + } +} + +- (void)testSSEPatchResultedInNoChange { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + //By pulling the patch from the existing flagConfig, the patch should not be applied, resulting in no change to the flagConfig + LDEvent *patch = [LDEvent stubEvent:kLDEventTypePatch flagKey:kLDFlagKeyIsANumber withDataDictionary:[self.user.flagConfig.featuresJsonDictionary[kLDFlagKeyIsANumber] dictionaryValue]]; + LDFlagConfigModel *targetFlagConfig = [self.user.flagConfig copy]; + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + messageHandler(patch); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:targetFlagConfig]); + XCTAssertTrue(self.user.flagConfig == originalFlagConfig); //same object with the new flagConfigValue + [self.dataManagerMock verify]; + [notificationObserver verify]; +} + +- (void)testSSEPatchFailedNilData { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + LDFlagConfigModel *targetFlagConfig = [self.user.flagConfig copy]; + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + LDEvent *patch = [LDEvent stubEvent:kLDEventTypePatch fromJsonFileNamed:@"ldEnvironmentControllerTestPatchIsANumber"]; + patch.data = nil; + + messageHandler(patch); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:targetFlagConfig]); + XCTAssertTrue(self.user.flagConfig == originalFlagConfig); //same object with the new flagConfigValue + [self.dataManagerMock verify]; + [notificationObserver verify]; +} + +- (void)testSSEPatchFailedEmptyData { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + LDFlagConfigModel *targetFlagConfig = [self.user.flagConfig copy]; + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + LDEvent *patch = [LDEvent stubEvent:kLDEventTypePatch fromJsonFileNamed:@"ldEnvironmentControllerTestPatchIsANumber"]; + patch.data = @""; + + messageHandler(patch); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:targetFlagConfig]); + XCTAssertTrue(self.user.flagConfig == originalFlagConfig); //same object with the new flagConfigValue + [self.dataManagerMock verify]; + [notificationObserver verify]; +} + +- (void)testSSEPatchFailedInvalidData { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + LDFlagConfigModel *targetFlagConfig = [self.user.flagConfig copy]; + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + LDEvent *patch = [LDEvent stubEvent:kLDEventTypePatch fromJsonFileNamed:@"ldEnvironmentControllerTestPatchIsANumber"]; + patch.data = @"{\"someInvalidData\":}"; + + messageHandler(patch); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:targetFlagConfig]); + XCTAssertTrue(self.user.flagConfig == originalFlagConfig); //same object with the new flagConfigValue + [self.dataManagerMock verify]; + [notificationObserver verify]; +} + +- (void)testSSEDeleteEventSuccess { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserUpdatedNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + LDEvent *delete = [LDEvent stubEvent:kLDEventTypeDelete fromJsonFileNamed:@"ldEnvironmentControllerTestDeleteIsANumber"]; + LDFlagConfigModel *targetFlagConfig = [self.user.flagConfig applySSEEvent:delete]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + [[self.dataManagerMock expect] saveUser:self.user]; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + messageHandler(delete); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:targetFlagConfig]); + XCTAssertNil([self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsANumber]); + XCTAssertTrue(self.user.flagConfig == originalFlagConfig); //same object without the flagConfigValue + [self.dataManagerMock verify]; + [notificationObserver verify]; +} + +- (void)testSSEDeleteResultedInNoChange { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + //By pulling the delete from the existing flagConfig, the delete should not be applied, resulting in no change to the flagConfig + LDEvent *delete = [LDEvent stubEvent:kLDEventTypeDelete flagKey:kLDFlagKeyIsANumber withDataDictionary:[self.user.flagConfig.featuresJsonDictionary[kLDFlagKeyIsANumber] dictionaryValue]]; + LDFlagConfigModel *targetFlagConfig = [self.user.flagConfig copy]; + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + messageHandler(delete); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:targetFlagConfig]); + XCTAssertNotNil([self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsANumber]); + XCTAssertTrue(self.user.flagConfig == originalFlagConfig); //same object + [self.dataManagerMock verify]; + [notificationObserver verify]; +} + +- (void)testSSEDeleteFailedNilData { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + LDFlagConfigModel *targetFlagConfig = [self.user.flagConfig copy]; + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + LDEvent *delete = [LDEvent stubEvent:kLDEventTypeDelete fromJsonFileNamed:@"ldEnvironmentControllerTestDeleteIsANumber"]; + delete.data = nil; + + messageHandler(delete); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:targetFlagConfig]); + XCTAssertNotNil([self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsANumber]); + XCTAssertTrue(self.user.flagConfig == originalFlagConfig); //same object + [self.dataManagerMock verify]; + [notificationObserver verify]; +} + +- (void)testSSEDeleteFailedEmptyData { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + LDFlagConfigModel *targetFlagConfig = [self.user.flagConfig copy]; + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + LDEvent *delete = [LDEvent stubEvent:kLDEventTypeDelete fromJsonFileNamed:@"ldEnvironmentControllerTestDeleteIsANumber"]; + delete.data = @""; + + messageHandler(delete); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:targetFlagConfig]); + XCTAssertNotNil([self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsANumber]); + XCTAssertTrue(self.user.flagConfig == originalFlagConfig); //same object + [self.dataManagerMock verify]; + [notificationObserver verify]; +} + +- (void)testSSEDeleteFailedInvalidData { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + LDFlagConfigModel *targetFlagConfig = [self.user.flagConfig copy]; + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + LDEvent *delete = [LDEvent stubEvent:kLDEventTypeDelete fromJsonFileNamed:@"ldEnvironmentControllerTestDeleteIsANumber"]; + delete.data = @"{\"someInvalidData\":}"; + + messageHandler(delete); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:targetFlagConfig]); + XCTAssertNotNil([self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsANumber]); + XCTAssertTrue(self.user.flagConfig == originalFlagConfig); //same object + [self.dataManagerMock verify]; + [notificationObserver verify]; +} + +- (void)testSSEUnrecognizedEvent { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserNoChangeNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDUserUpdatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + //it's not obvious, but by not setting expect on the mock observer, the observer will fail when verify is called IF it has received the notification + + __block LDEventSourceEventHandler messageHandler; + [[self.eventSourceMock stub] onMessage:[OCMArg checkWithBlock:^BOOL(id obj) { + messageHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + LDFlagConfigModel *targetFlagConfig = [originalFlagConfig copy]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(messageHandler); + if (!messageHandler) { return; } + + LDEvent *put = [LDEvent stubEvent:@"someUnrecognizedEvent" fromJsonFileNamed:@"featureFlags"]; + + messageHandler(put); + + XCTAssertTrue([self.user.flagConfig isEqualToConfig: targetFlagConfig]); + XCTAssertTrue(self.user.flagConfig == originalFlagConfig); + [self.dataManagerMock verify]; + [notificationObserver verify]; +} + +- (void)testClientUnauthorizedPosted { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDClientUnauthorizedNotification object:nil]; + [[notificationObserver expect] notificationWithName: kLDClientUnauthorizedNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDServerConnectionUnavailableNotification object:nil]; + [[notificationObserver expect] notificationWithName: kLDServerConnectionUnavailableNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + + __block LDEventSourceEventHandler errorHandler; + [[self.eventSourceMock stub] onError:[OCMArg checkWithBlock:^BOOL(id obj) { + errorHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(errorHandler); + if (!errorHandler) { return; } + errorHandler([LDEvent stubUnauthorizedEvent]); + + [notificationObserver verify]; +} + +- (void)testClientUnauthorizedNotPosted { + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDClientUnauthorizedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDServerConnectionUnavailableNotification object:nil]; + [[notificationObserver expect] notificationWithName: kLDServerConnectionUnavailableNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + + __block LDEventSourceEventHandler errorHandler; + [[self.eventSourceMock stub] onError:[OCMArg checkWithBlock:^BOOL(id obj) { + errorHandler = (LDEventSourceEventHandler)obj; + return YES; + }]]; + + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; + + self.environmentController.online = YES; + + XCTAssertNotNil(errorHandler); + if (!errorHandler) { return; } + errorHandler([LDEvent stubErrorEvent]); + + [notificationObserver verify]; +} + +#pragma mark - Polling + +- (void)testFlagConfigTimerFiredNotification { + self.environmentController.online = YES; + [[self.requestManagerMock expect] performFeatureFlagRequest:self.user isOnline:self.environmentController.isOnline]; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDFlagConfigTimerFiredNotification object:nil]; + + [self.requestManagerMock verify]; +} + +- (void)testSyncWithServerForConfig { + self.environmentController.online = YES; + [[self.requestManagerMock expect] performFeatureFlagRequest:self.user isOnline:self.environmentController.isOnline]; + + [self.environmentController syncWithServerForConfig]; + + [self.requestManagerMock verify]; +} + +- (void)testSyncWithServerForConfig_UserDoesNotExist { + self.environmentController.user = nil; + self.environmentController.online = YES; + [[self.requestManagerMock reject] performFeatureFlagRequest:[OCMArg any] isOnline:[OCMArg any]]; + + [self.environmentController syncWithServerForConfig]; + + [self.requestManagerMock verify]; +} + +- (void)testSyncWithServerForConfig_offline { + self.environmentController.online = NO; + [[self.requestManagerMock reject] performFeatureFlagRequest:[OCMArg any] isOnline:[OCMArg any]]; + + [self.environmentController syncWithServerForConfig]; + + [self.requestManagerMock verify]; +} + +#pragma mark - Flag Config Processing Notification + +- (void)testProcessedConfig_success_flagConfigChanged { + id userUpdatedNotificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:userUpdatedNotificationObserver name:kLDUserUpdatedNotification object:nil]; + [[userUpdatedNotificationObserver expect] notificationWithName:kLDUserUpdatedNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + + id userNoChangeNotificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:userNoChangeNotificationObserver name:kLDUserNoChangeNotification object:nil]; + + id featureFlagsChangedNotificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:featureFlagsChangedNotificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + __block NSDictionary *featureFlagsChangedNotificationUserInfo; + [[featureFlagsChangedNotificationObserver expect] notificationWithName:kLDFeatureFlagsChangedNotification object:[OCMArg any] userInfo:[OCMArg checkWithBlock:^BOOL(id obj) { + featureFlagsChangedNotificationUserInfo = obj; + return YES; + }]]; + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:userUpdatedNotificationObserver]; + [[NSNotificationCenter defaultCenter] removeObserver:userNoChangeNotificationObserver]; + [[NSNotificationCenter defaultCenter] removeObserver:featureFlagsChangedNotificationObserver]; + }; + + [[self.dataManagerMock expect] saveUser:self.user]; + + LDFlagConfigModel *startingFlagConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"ldEnvironmentControllerTestConfigA"]; + self.user.flagConfig = startingFlagConfig; + LDFlagConfigModel *endingFlagConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"ldEnvironmentControllerTestConfigB"]; + + [self.environmentController processedConfig:YES jsonConfigDictionary:[endingFlagConfig dictionaryValue]]; + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:endingFlagConfig]); + [self.dataManagerMock verify]; + [userUpdatedNotificationObserver verify]; + [userNoChangeNotificationObserver verify]; + [featureFlagsChangedNotificationObserver verify]; + XCTAssertNotNil(featureFlagsChangedNotificationUserInfo); + if (featureFlagsChangedNotificationUserInfo != nil) { + XCTAssertEqualObjects(featureFlagsChangedNotificationUserInfo[kLDNotificationUserInfoKeyMobileKey], mockMobileKey); + NSSet *changedFlagKeys = [NSSet setWithArray:[startingFlagConfig differingFlagKeysFromConfig:endingFlagConfig]]; + XCTAssertEqualObjects([NSSet setWithArray:featureFlagsChangedNotificationUserInfo[kLDNotificationUserInfoKeyFlagKeys]], changedFlagKeys); + } +} + +- (void)testProcessedConfig_success_flagConfigUnchanged_trackingContextChanged { + id userUpdatedNotificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:userUpdatedNotificationObserver name:kLDUserUpdatedNotification object:nil]; + + id userNoChangeNotificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:userNoChangeNotificationObserver name:kLDUserNoChangeNotification object:nil]; + [[userNoChangeNotificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + + id featureFlagsChangedNotificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:featureFlagsChangedNotificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:userUpdatedNotificationObserver]; + [[NSNotificationCenter defaultCenter] removeObserver:userNoChangeNotificationObserver]; + [[NSNotificationCenter defaultCenter] removeObserver:featureFlagsChangedNotificationObserver]; + }; + + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; //the user stub creates feature flags from the "featureFlags" fixture + + //Changing the eventTrackingContext should have no effect on the flagConfig comparison. Changing it here verifies that requirement. + LDEventTrackingContext *updatedEventTrackingContext = [LDEventTrackingContext contextWithTrackEvents:YES debugEventsUntilDate:[NSDate dateWithTimeIntervalSinceNow:30.0]]; + LDFlagConfigModel *updatedFlagConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"featureFlags" eventTrackingContext:updatedEventTrackingContext]; + + [self.environmentController processedConfig:YES jsonConfigDictionary:[updatedFlagConfig dictionaryValue]]; + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:originalFlagConfig]); + [self.dataManagerMock verify]; + [userUpdatedNotificationObserver verify]; + [userNoChangeNotificationObserver verify]; + [featureFlagsChangedNotificationObserver verify]; +} + +- (void)testProcessedConfig_success_withoutFlagConfig { + id userUpdatedNotificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:userUpdatedNotificationObserver name:kLDUserUpdatedNotification object:nil]; + + id userNoChangeNotificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:userNoChangeNotificationObserver name:kLDUserNoChangeNotification object:nil]; + [[userNoChangeNotificationObserver expect] notificationWithName:kLDUserNoChangeNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + + id featureFlagsChangedNotificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:featureFlagsChangedNotificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + id connectionUnavailableNotificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:connectionUnavailableNotificationObserver name:kLDServerConnectionUnavailableNotification object:nil]; + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:userUpdatedNotificationObserver]; + [[NSNotificationCenter defaultCenter] removeObserver:userNoChangeNotificationObserver]; + [[NSNotificationCenter defaultCenter] removeObserver:featureFlagsChangedNotificationObserver]; + [[NSNotificationCenter defaultCenter] removeObserver:connectionUnavailableNotificationObserver]; + }; + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + + [self.environmentController processedConfig:YES jsonConfigDictionary:nil]; + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:originalFlagConfig]); + [self.dataManagerMock verify]; + [userUpdatedNotificationObserver verify]; + [userNoChangeNotificationObserver verify]; + [featureFlagsChangedNotificationObserver verify]; + [connectionUnavailableNotificationObserver verify]; +} + +- (void)testProcessedConfig_failure { + id userUpdatedNotificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:userUpdatedNotificationObserver name:kLDUserUpdatedNotification object:nil]; + + id userNoChangeNotificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:userNoChangeNotificationObserver name:kLDUserNoChangeNotification object:nil]; + + id featureFlagsChangedNotificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:featureFlagsChangedNotificationObserver name:kLDFeatureFlagsChangedNotification object:nil]; + + id connectionUnavailableNotificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:connectionUnavailableNotificationObserver name:kLDServerConnectionUnavailableNotification object:nil]; + [[connectionUnavailableNotificationObserver expect] notificationWithName:kLDServerConnectionUnavailableNotification object:[OCMArg any] userInfo:self.notificationUserInfo]; + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:userUpdatedNotificationObserver]; + [[NSNotificationCenter defaultCenter] removeObserver:userNoChangeNotificationObserver]; + [[NSNotificationCenter defaultCenter] removeObserver:featureFlagsChangedNotificationObserver]; + [[NSNotificationCenter defaultCenter] removeObserver:connectionUnavailableNotificationObserver]; + }; + + LDFlagConfigModel *originalFlagConfig = self.user.flagConfig; + + [[self.dataManagerMock reject] saveUser:[OCMArg any]]; + + [self.environmentController processedConfig:NO jsonConfigDictionary:nil]; + + XCTAssertTrue([self.user.flagConfig isEqualToConfig:originalFlagConfig]); + [self.dataManagerMock verify]; + [userUpdatedNotificationObserver verify]; + [userNoChangeNotificationObserver verify]; + [featureFlagsChangedNotificationObserver verify]; + [connectionUnavailableNotificationObserver verify]; +} + +#pragma mark - Events + +- (void)testEventTimerFiredNotification { + [[self.dataManagerMock stub] allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { + void (^completion)(NSArray *) = obj; + completion(self.events); + return YES; + }]]; + [[self.dataManagerMock expect] recordSummaryEventWithTracker:[OCMArg any]]; + self.environmentController.online = YES; + [[self.requestManagerMock expect] performEventRequest:self.events isOnline:self.environmentController.isOnline]; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDEventTimerFiredNotification object:nil]; + + [self.requestManagerMock verify]; + [self.dataManagerMock verify]; +} + +- (void)testSyncWithServerForEvents_eventsExist { + [[self.dataManagerMock stub] allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { + void (^completion)(NSArray *) = obj; + completion(self.events); + return YES; + }]]; + self.environmentController.online = YES; + [[self.requestManagerMock expect] performEventRequest:self.events isOnline:YES]; + [[self.dataManagerMock expect] recordSummaryEventWithTracker:[OCMArg any]]; + LDMillisecond startDateMillis = [[NSDate date] millisSince1970]; + + [self.environmentController syncWithServerForEvents]; + + [self.requestManagerMock verify]; + [self.dataManagerMock verify]; + XCTAssertNotNil(self.user.flagConfigTracker); + XCTAssertFalse(self.user.flagConfigTracker.hasTrackedEvents); + XCTAssertTrue(Approximately(self.user.flagConfigTracker.startDateMillis, startDateMillis, 10)); +} + +- (void)testSyncWithServerForEvents_eventsEmpty { + self.environmentController.online = YES; + [[self.dataManagerMock expect] recordSummaryEventWithTracker:[OCMArg any]]; + [[self.dataManagerMock stub] allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { + void (^completion)(NSArray *) = obj; + completion(@[]); + return YES; + }]]; + [[self.requestManagerMock reject] performEventRequest:[OCMArg any] isOnline:[OCMArg any]]; + + [self.environmentController syncWithServerForEvents]; + + [self.requestManagerMock verify]; + [self.dataManagerMock verify]; +} + +- (void)testSyncWithServerForEvents_eventsNil { + self.environmentController.online = YES; + [[self.dataManagerMock expect] recordSummaryEventWithTracker:[OCMArg any]]; + [[self.dataManagerMock stub] allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { + void (^completion)(NSArray *) = obj; + completion(nil); + return YES; + }]]; + [[self.requestManagerMock reject] performEventRequest:[OCMArg any] isOnline:[OCMArg any]]; + + [self.environmentController syncWithServerForEvents]; + + [self.requestManagerMock verify]; + [self.dataManagerMock verify]; +} + +- (void)testSyncWithServerForEvents_offline { + [[self.dataManagerMock stub] allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { + void (^completion)(NSArray *) = obj; + completion(self.events); + return YES; + }]]; + [[self.requestManagerMock reject] performEventRequest:[OCMArg any] isOnline:[OCMArg any]]; + [[self.dataManagerMock reject] allEventDictionaries:[OCMArg any]]; + + [self.environmentController syncWithServerForEvents]; + + [self.requestManagerMock verify]; + [self.dataManagerMock verify]; +} + +- (void)testProcessedEvents_success_withProcessedEvents { + LDFlagConfigValue *flagConfigValue = [LDFlagConfigValue flagConfigValueFromJsonFileNamed:@"boolConfigIsABool-true" + flagKey:kLDFlagKeyIsABool + eventTrackingContext:[LDEventTrackingContext stub]]; + LDEventModel *event = [LDEventModel featureEventWithFlagKey:kFeatureEventKeyStub + reportedFlagValue:flagConfigValue.value + flagConfigValue:flagConfigValue + defaultFlagValue:@(NO) + user:self.user + inlineUser:self.config.inlineUserInEvents]; + NSArray *events = @[[event dictionaryValueUsingConfig:self.config]]; + NSDate *headerDate = [NSDateFormatter eventDateHeaderStub]; + [[self.dataManagerMock expect] deleteProcessedEvents:events]; + [[self.dataManagerMock expect] setLastEventResponseDate:headerDate]; + + [self.environmentController processedEvents:YES jsonEventArray:events responseDate:headerDate]; + + [self.dataManagerMock verify]; +} + +- (void)testProcessedEvents_success_emptyProcessedEvents { + NSDate *headerDate = [NSDateFormatter eventDateHeaderStub]; + [[self.dataManagerMock expect] deleteProcessedEvents:@[]]; + [[self.dataManagerMock expect] setLastEventResponseDate:headerDate]; + + [self.environmentController processedEvents:YES jsonEventArray:@[] responseDate:headerDate]; + + [self.dataManagerMock verify]; +} + +- (void)testProcessedEvents_success_nilProcessedEvents { + NSDate *headerDate = [NSDateFormatter eventDateHeaderStub]; + [[self.dataManagerMock expect] deleteProcessedEvents:nil]; + [[self.dataManagerMock expect] setLastEventResponseDate:headerDate]; + + [self.environmentController processedEvents:YES jsonEventArray:nil responseDate:headerDate]; + + [self.dataManagerMock verify]; +} + +- (void)testProcessedEvents_failure { + [[self.dataManagerMock reject] deleteProcessedEvents:[OCMArg any]]; + + [self.environmentController processedEvents:NO jsonEventArray:nil responseDate:nil]; + + [self.dataManagerMock verify]; +} + +- (void)testFlushEventsWhenOnline { + self.environmentController.online = YES; + [[self.requestManagerMock expect] performEventRequest:[OCMArg any] isOnline:self.environmentController.isOnline]; + [[self.dataManagerMock stub] allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { + void (^completion)(NSArray *) = obj; + completion(self.events); + return YES; + }]]; + [[self.dataManagerMock expect] recordSummaryEventWithTracker:[OCMArg any]]; + + [self.environmentController flushEvents]; + + [self.requestManagerMock verify]; +} + +- (void)testFlushEventsWhenOffline { + [[self.dataManagerMock stub] allEventDictionaries:[OCMArg checkWithBlock:^BOOL(id obj) { + void (^completion)(NSArray *) = obj; + completion(self.events); + return YES; + }]]; + [[self.requestManagerMock reject] performEventRequest:[OCMArg any] isOnline:[OCMArg any]]; + [[self.dataManagerMock reject] allEventDictionaries:[OCMArg any]]; + + [self.environmentController flushEvents]; + + [self.requestManagerMock verify]; + [self.dataManagerMock verify]; +} + +@end diff --git a/DarklyTests/LDEnvironmentTest.m b/DarklyTests/LDEnvironmentTest.m new file mode 100644 index 00000000..a928343e --- /dev/null +++ b/DarklyTests/LDEnvironmentTest.m @@ -0,0 +1,1052 @@ +// +// LDEnvironmentTest.m +// DarklyTests +// +// Created by Mark Pokorny on 10/3/18. +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "DarklyXCTestCase.h" +#import + +#import "LDEnvironment.h" +#import "LDEnvironmentController.h" +#import "LDDataManager.h" +#import "LDFlagConfigModel.h" + +#import "ClientDelegateMock.h" +#import "LDUserModel+Testable.h" +#import "LDFlagConfigValue+Testable.h" +#import "NSThread+MainExecutable.h" +#import "LDConfig+Testable.h" + +#pragma mark - ClientDelegateProtocolMock + +@interface ClientDelegateProtocolWithoutMethodsMock : NSObject + +@end + +@implementation ClientDelegateProtocolWithoutMethodsMock + +@end + +#pragma mark - LDEnvironment(LDEnvironmentTest) + +@interface LDEnvironment(LDEnvironmentTest) +@property (nonatomic, assign) BOOL environmentStarted; +@property (nonatomic, assign) BOOL willGoOnlineAfterDelay; +@property (nonatomic, strong) LDEnvironmentController *environmentController; +@property (nonatomic, strong) LDDataManager *dataManager; +-(void)notifyDelegateOfUpdatesForFlagKeys:(NSArray*)updatedFlagKeys; +@end + +#pragma mark - LDEnvironmentTest + +@interface LDEnvironmentTest: DarklyXCTestCase +@property (nonatomic, strong) ClientDelegateMock *clientDelegateMock; +@property (nonatomic, copy) NSString *mobileKey; +@property (nonatomic, strong) LDConfig *config; +@property (nonatomic, strong) LDUserModel *user; +@property (nonatomic, strong) id dataManagerMock; +@property (nonatomic, strong) id environmentControllerMock; +@property (nonatomic, strong) LDEnvironment *environment; +@end + +@implementation LDEnvironmentTest +static NSString *const secondaryEnvironmentMobileKey = @"LDEnvironmentTest.secondaryEnvironment.mobileKey"; +static NSString *const featureFlagKey = @"LDEnvironmentTest.featureFlagKey"; +static NSString *const customEventName = @"LDEnvironmentTest.Event.custom.eventName"; +static NSString *const dummyFeatureFlagKey = @"LDEnvironmentTest.FeatureFlags.Keys.dummy"; +static NSString *const stringFeatureFlagValue = @"test"; +static NSString *const defaultStringFeatureFlagValue = @"LDEnvironmentTest.FeatureFlags.Values.default"; + +-(void)setUp { + [super setUp]; + self.clientDelegateMock = [ClientDelegateMock clientDelegateMock]; + self.mobileKey = @"LDEnvironmentTest.primaryEnvironment.mobileKey"; + self.config = [[LDConfig alloc] initWithMobileKey:self.mobileKey]; + self.user = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + self.dataManagerMock = [OCMockObject niceMockForClass:[LDDataManager class]]; + [[[self.dataManagerMock stub] andReturn:self.dataManagerMock] dataManagerWithMobileKey:self.mobileKey config:self.config]; + self.environmentControllerMock = [OCMockObject niceMockForClass:[LDEnvironmentController class]]; + [[[self.environmentControllerMock stub] andReturn:self.environmentControllerMock] controllerWithMobileKey:self.mobileKey config:self.config user:[OCMArg checkWithBlock:^BOOL(id obj) { + //Because the environment makes a copy of the user, this makes sure to serve the environmentControllerMock whenever the user passed into controllerWithMobileKey matches self.user + return [obj isEqual:self.user ignoringAttributes:@[kUserAttributeUpdatedAt]]; + }] dataManager:self.dataManagerMock]; + self.environment = [LDEnvironment environmentForMobileKey:self.mobileKey config:self.config user:self.user]; + [[[self.dataManagerMock stub] andReturn:self.user.flagConfig] retrieveFlagConfigForUser:self.environment.user]; + self.environment.delegate = self.clientDelegateMock; +} + +-(void)tearDown { + self.clientDelegateMock = nil; + self.dataManagerMock = nil; + self.environmentControllerMock = nil; + [[NSNotificationCenter defaultCenter] removeObserver:self.environment]; + [super tearDown]; +} + +#pragma mark - Lifecycle + +-(void)testInitAndConstructor { + XCTAssertNotNil(self.environment); + XCTAssertEqualObjects(self.environment.mobileKey, self.config.mobileKey); + XCTAssertEqualObjects(self.environment.config, self.config); + XCTAssertTrue([self.environment.user isEqual:self.user ignoringAttributes:@[kUserAttributeUpdatedAt]]); + XCTAssertEqual(self.environment.isStarted, NO); + XCTAssertEqual(self.environment.isOnline, NO); + XCTAssertEqualObjects(self.environment.delegate, self.clientDelegateMock); + XCTAssertEqualObjects(self.environment.environmentController, self.environmentControllerMock); + XCTAssertEqualObjects(self.environment.dataManager, self.dataManagerMock); +} + +-(void)testIsPrimary_mobileKeyMatchesConfigMobileKey { + XCTAssertTrue(self.environment.isPrimary); +} + +-(void)testIsPrimary_mobileKeyDiffersFromConfigMobileKey { + self.environment = [LDEnvironment environmentForMobileKey:[NSUUID UUID].UUIDString config:self.config user:self.user]; + + XCTAssertFalse(self.environment.isPrimary); +} + +-(void)testEnvironmentName_primaryEnvironment { + XCTAssertEqualObjects(self.environment.environmentName, kLDPrimaryEnvironmentName); +} + +-(void)testEnvironmentName_secondaryEnvironment { + NSDictionary *secondaryMobileKeysMock = [LDConfig secondaryMobileKeysStub]; + self.config.secondaryMobileKeys = secondaryMobileKeysMock; + + for (NSString *keyName in secondaryMobileKeysMock.allKeys) { + NSString *mobileKey = secondaryMobileKeysMock[keyName]; + self.environment = [LDEnvironment environmentForMobileKey:mobileKey config:self.config user:self.user]; + + XCTAssertEqualObjects(self.environment.environmentName, keyName); + } +} + +#pragma mark - Controls + +-(void)testStart { + self.dataManagerMock = [OCMockObject niceMockForClass:[LDDataManager class]]; + [[[self.dataManagerMock stub] andReturn:self.dataManagerMock] dataManagerWithMobileKey:self.mobileKey config:self.config]; + self.environment = [LDEnvironment environmentForMobileKey:self.mobileKey config:self.config user:self.user]; + [[self.dataManagerMock expect] convertToEnvironmentBasedCacheForUser:self.environment.user config:self.config]; + [[[self.dataManagerMock expect] andReturn:self.user.flagConfig] retrieveFlagConfigForUser:self.environment.user]; + [[self.dataManagerMock expect] saveUser:self.environment.user]; + [[self.dataManagerMock expect] recordIdentifyEventWithUser:self.environment.user]; + + [self.environment start]; + + XCTAssertTrue(self.environment.isStarted); + [self.dataManagerMock verify]; +} + +-(void)testStart_secondaryEnvironment { + NSString *mobileKey = [NSUUID UUID].UUIDString; + self.dataManagerMock = [OCMockObject niceMockForClass:[LDDataManager class]]; + [[[self.dataManagerMock stub] andReturn:self.dataManagerMock] dataManagerWithMobileKey:mobileKey config:self.config]; + self.environment = [LDEnvironment environmentForMobileKey:mobileKey config:self.config user:self.user]; + [[self.dataManagerMock reject] convertToEnvironmentBasedCacheForUser:[OCMArg any] config:[OCMArg any]]; + [[self.dataManagerMock expect] retrieveFlagConfigForUser:self.environment.user]; + [[self.dataManagerMock expect] saveUser:self.environment.user]; + [[self.dataManagerMock expect] recordIdentifyEventWithUser:self.environment.user]; + + [self.environment start]; + + XCTAssertTrue(self.environment.isStarted); + [self.dataManagerMock verify]; +} + +-(void)testStop { + [self.environment start]; + self.environment.online = YES; + [[self.environmentControllerMock expect] setOnline:NO]; + + [self.environment stop]; + + XCTAssertFalse(self.environment.isStarted); + XCTAssertFalse(self.environment.isOnline); + [self.environmentControllerMock verify]; +} + +-(void)testOnline { + [self.environment start]; + [[self.environmentControllerMock expect] setOnline:YES]; + + self.environment.online = YES; + + XCTAssertTrue(self.environment.isOnline); + + [[self.environmentControllerMock expect] setOnline:NO]; + + self.environment.online = NO; + + XCTAssertFalse(self.environment.isOnline); + [self.environmentControllerMock verify]; +} + +-(void)testOnline_alreadyOnline { + [self.environment start]; + self.environment.online = YES; + [[[self.environmentControllerMock stub] andReturnValue:@YES] isOnline]; + [[self.environmentControllerMock reject] setOnline:YES]; + + self.environment.online = YES; + + XCTAssertTrue(self.environment.isOnline); + [self.environmentControllerMock verify]; +} + +-(void)testOnline_alreadyOnline_controllerOffline { + [self.environment start]; + self.environment.online = YES; + [[[self.environmentControllerMock stub] andReturnValue:@NO] isOnline]; + [[self.environmentControllerMock expect] setOnline:YES]; + + self.environment.online = YES; + + XCTAssertTrue(self.environment.isOnline); + [self.environmentControllerMock verify]; +} + +-(void)testOnline_alreadyOffline { + [self.environment start]; + [[self.environmentControllerMock reject] setOnline:NO]; + + self.environment.online = NO; + + XCTAssertFalse(self.environment.isOnline); + [self.environmentControllerMock verify]; +} + +-(void)testOnline_alreadyOffline_controllerOnline { + [self.environment start]; + [[[self.environmentControllerMock stub] andReturnValue:@YES] isOnline]; + [[self.environmentControllerMock expect] setOnline:NO]; + + self.environment.online = NO; + + XCTAssertFalse(self.environment.isOnline); + [self.environmentControllerMock verify]; +} + +-(void)testOnline_notStarted { + [[self.environmentControllerMock reject] setOnline:[OCMArg any]]; + + self.environment.online = YES; + + XCTAssertFalse(self.environment.isOnline); + [self.environmentControllerMock verify]; +} + +-(void)testFlush { + [self.environment start]; + self.environment.online = YES; + [[self.environmentControllerMock expect] flushEvents]; + + BOOL result = [self.environment flush]; + + XCTAssertTrue(result); + [self.environmentControllerMock verify]; +} + +-(void)testFlush_offline { + [self.environment start]; + [[self.environmentControllerMock reject] flushEvents]; + + BOOL result = [self.environment flush]; + + XCTAssertFalse(result); + [self.environmentControllerMock verify]; +} + +-(void)testFlush_notStarted { + [[self.environmentControllerMock reject] flushEvents]; + + BOOL result = [self.environment flush]; + + XCTAssertFalse(result); + [self.environmentControllerMock verify]; +} + +#pragma mark - Feature Flags + +//Bool Variation ------ +-(void)testBoolVariation { + [self.environment start]; + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsABool]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:kLDFlagKeyIsABool + reportedFlagValue:@(NO) + flagConfigValue:flagConfigValue + defaultFlagValue:@(YES) + user:self.environment.user]; + + BOOL reportedValue = [self.environment boolVariation:kLDFlagKeyIsABool fallback:YES]; + + XCTAssertFalse(reportedValue); + [self.dataManagerMock verify]; +} + +-(void)testBoolVariation_flagMissing { + [self.environment start]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:dummyFeatureFlagKey + reportedFlagValue:@(YES) + flagConfigValue:nil + defaultFlagValue:@(YES) + user:self.environment.user]; + + BOOL reportedValue = [self.environment boolVariation:dummyFeatureFlagKey fallback:YES]; + + XCTAssertTrue(reportedValue); + [self.dataManagerMock verify]; +} + +-(void)testBoolVariation_nullValue { + [self.environment start]; + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsANull]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:kLDFlagKeyIsANull + reportedFlagValue:@(YES) + flagConfigValue:flagConfigValue + defaultFlagValue:@(YES) + user:self.environment.user]; + + BOOL reportedValue = [self.environment boolVariation:kLDFlagKeyIsANull fallback:YES]; + + XCTAssertTrue(reportedValue); + [self.dataManagerMock verify]; +} + +-(void)testBoolVariation_notStarted { + [[self.dataManagerMock reject] recordFlagEvaluationEventsWithFlagKey:[OCMArg any] + reportedFlagValue:[OCMArg any] + flagConfigValue:[OCMArg any] + defaultFlagValue:[OCMArg any] + user:[OCMArg any]]; + + BOOL reportedValue = [self.environment boolVariation:dummyFeatureFlagKey fallback:YES]; + + XCTAssertTrue(reportedValue); + [self.dataManagerMock verify]; +} + +//Number Variation ------ +-(void)testNumberVariation { + [self.environment start]; + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsANumber]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:kLDFlagKeyIsANumber + reportedFlagValue:@(0) + flagConfigValue:flagConfigValue + defaultFlagValue:@(7) + user:self.environment.user]; + + NSNumber *reportedValue = [self.environment numberVariation:kLDFlagKeyIsANumber fallback:@(7)]; + + XCTAssertEqualObjects(reportedValue, @(0)); + [self.dataManagerMock verify]; +} + +-(void)testNumberVariation_flagMissing { + [self.environment start]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:dummyFeatureFlagKey + reportedFlagValue:@(7) + flagConfigValue:nil + defaultFlagValue:@(7) + user:self.environment.user]; + + NSNumber *reportedValue = [self.environment numberVariation:dummyFeatureFlagKey fallback:@(7)]; + + XCTAssertEqualObjects(reportedValue, @(7)); + [self.dataManagerMock verify]; +} + +-(void)testNumberVariation_nullValue { + [self.environment start]; + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsANull]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:kLDFlagKeyIsANull + reportedFlagValue:@(7) + flagConfigValue:flagConfigValue + defaultFlagValue:@(7) + user:self.environment.user]; + + NSNumber *reportedValue = [self.environment numberVariation:kLDFlagKeyIsANull fallback:@(7)]; + + XCTAssertEqualObjects(reportedValue, @(7)); + [self.dataManagerMock verify]; +} + +-(void)testNumberVariation_notStarted { + [[self.dataManagerMock reject] recordFlagEvaluationEventsWithFlagKey:[OCMArg any] + reportedFlagValue:[OCMArg any] + flagConfigValue:[OCMArg any] + defaultFlagValue:[OCMArg any] + user:[OCMArg any]]; + + NSNumber *reportedValue = [self.environment numberVariation:kLDFlagKeyIsANull fallback:@(7)]; + + XCTAssertEqualObjects(reportedValue, @(7)); + [self.dataManagerMock verify]; +} + +//Double Variation ------ +-(void)testDoubleVariation { + [self.environment start]; + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsADouble]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:kLDFlagKeyIsADouble + reportedFlagValue:flagConfigValue.value + flagConfigValue:flagConfigValue + defaultFlagValue:@(2.71828) + user:self.environment.user]; + + double reportedValue = [self.environment doubleVariation:kLDFlagKeyIsADouble fallback:2.71828]; + + XCTAssertEqual(reportedValue, [flagConfigValue.value doubleValue]); + [self.dataManagerMock verify]; +} + +-(void)testDoubleVariation_flagMissing { + [self.environment start]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:dummyFeatureFlagKey + reportedFlagValue:@(2.71828) + flagConfigValue:nil + defaultFlagValue:@(2.71828) + user:self.environment.user]; + + double reportedValue = [self.environment doubleVariation:dummyFeatureFlagKey fallback:2.71828]; + + XCTAssertEqual(reportedValue, 2.71828); + [self.dataManagerMock verify]; +} + +-(void)testDoubleVariation_nullValue { + [self.environment start]; + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsANull]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:kLDFlagKeyIsANull + reportedFlagValue:@(2.71828) + flagConfigValue:flagConfigValue + defaultFlagValue:@(2.71828) + user:self.environment.user]; + + double reportedValue = [self.environment doubleVariation:kLDFlagKeyIsANull fallback:2.71828]; + + XCTAssertEqual(reportedValue, 2.71828); + [self.dataManagerMock verify]; +} + +-(void)testDoubleVariation_notStarted { + [[self.dataManagerMock reject] recordFlagEvaluationEventsWithFlagKey:[OCMArg any] + reportedFlagValue:[OCMArg any] + flagConfigValue:[OCMArg any] + defaultFlagValue:[OCMArg any] + user:[OCMArg any]]; + + double reportedValue = [self.environment doubleVariation:kLDFlagKeyIsANull fallback:2.71828]; + + XCTAssertEqual(reportedValue, 2.71828); + [self.dataManagerMock verify]; +} + +//String Variation ------ +-(void)testStringVariation { + [self.environment start]; + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsAString]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:kLDFlagKeyIsAString + reportedFlagValue:stringFeatureFlagValue + flagConfigValue:flagConfigValue + defaultFlagValue:defaultStringFeatureFlagValue + user:self.environment.user]; + + NSString *reportedValue = [self.environment stringVariation:kLDFlagKeyIsAString fallback:defaultStringFeatureFlagValue]; + + XCTAssertEqualObjects(reportedValue, stringFeatureFlagValue); + [self.dataManagerMock verify]; +} + +-(void)testStringVariation_flagMissing { + [self.environment start]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:dummyFeatureFlagKey + reportedFlagValue:defaultStringFeatureFlagValue + flagConfigValue:nil + defaultFlagValue:defaultStringFeatureFlagValue + user:self.environment.user]; + + NSString *reportedValue = [self.environment stringVariation:dummyFeatureFlagKey fallback:defaultStringFeatureFlagValue]; + + XCTAssertEqualObjects(reportedValue, defaultStringFeatureFlagValue); + [self.dataManagerMock verify]; +} + +-(void)testStringVariation_nullValue { + [self.environment start]; + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsANull]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:kLDFlagKeyIsANull + reportedFlagValue:defaultStringFeatureFlagValue + flagConfigValue:flagConfigValue + defaultFlagValue:defaultStringFeatureFlagValue + user:self.environment.user]; + + NSString *reportedValue = [self.environment stringVariation:kLDFlagKeyIsANull fallback:defaultStringFeatureFlagValue]; + + XCTAssertEqualObjects(reportedValue, defaultStringFeatureFlagValue); + [self.dataManagerMock verify]; +} + +-(void)testStringVariation_notStarted { + [[self.dataManagerMock reject] recordFlagEvaluationEventsWithFlagKey:[OCMArg any] + reportedFlagValue:[OCMArg any] + flagConfigValue:[OCMArg any] + defaultFlagValue:[OCMArg any] + user:[OCMArg any]]; + + NSString *reportedValue = [self.environment stringVariation:kLDFlagKeyIsANull fallback:defaultStringFeatureFlagValue]; + + XCTAssertEqualObjects(reportedValue, defaultStringFeatureFlagValue); + [self.dataManagerMock verify]; +} + +//Array Variation ------ +-(void)testArrayVariation { + [self.environment start]; + NSArray *defaultArrayFeatureFlagValue = @[@(1), @(2)]; + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsAnArray]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:kLDFlagKeyIsAnArray + reportedFlagValue:flagConfigValue.value + flagConfigValue:flagConfigValue + defaultFlagValue:defaultArrayFeatureFlagValue + user:self.environment.user]; + + NSArray *reportedValue = [self.environment arrayVariation:kLDFlagKeyIsAnArray fallback:defaultArrayFeatureFlagValue]; + + XCTAssertEqualObjects(reportedValue, flagConfigValue.value); + [self.dataManagerMock verify]; +} + +-(void)testArrayVariation_flagMissing { + [self.environment start]; + NSArray *defaultArrayFeatureFlagValue = @[@(1), @(2)]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:dummyFeatureFlagKey + reportedFlagValue:defaultArrayFeatureFlagValue + flagConfigValue:nil + defaultFlagValue:defaultArrayFeatureFlagValue + user:self.environment.user]; + + NSArray *reportedValue = [self.environment arrayVariation:dummyFeatureFlagKey fallback:defaultArrayFeatureFlagValue]; + + XCTAssertEqualObjects(reportedValue, defaultArrayFeatureFlagValue); + [self.dataManagerMock verify]; +} + +-(void)testArrayVariation_nullValue { + [self.environment start]; + NSArray *defaultArrayFeatureFlagValue = @[@(1), @(2)]; + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsANull]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:kLDFlagKeyIsANull + reportedFlagValue:defaultArrayFeatureFlagValue + flagConfigValue:flagConfigValue + defaultFlagValue:defaultArrayFeatureFlagValue + user:self.environment.user]; + + NSArray *reportedValue = [self.environment arrayVariation:kLDFlagKeyIsANull fallback:defaultArrayFeatureFlagValue]; + + XCTAssertEqualObjects(reportedValue, defaultArrayFeatureFlagValue); + [self.dataManagerMock verify]; +} + +-(void)testArrayVariation_notStarted { + NSArray *defaultArrayFeatureFlagValue = @[@(1), @(2)]; + [[self.dataManagerMock reject] recordFlagEvaluationEventsWithFlagKey:[OCMArg any] + reportedFlagValue:[OCMArg any] + flagConfigValue:[OCMArg any] + defaultFlagValue:[OCMArg any] + user:[OCMArg any]]; + + NSArray *reportedValue = [self.environment arrayVariation:kLDFlagKeyIsANull fallback:defaultArrayFeatureFlagValue]; + + XCTAssertEqualObjects(reportedValue, defaultArrayFeatureFlagValue); + [self.dataManagerMock verify]; +} + +//Dictionary Variation ------ +-(void)testDictionaryVariation { + [self.environment start]; + NSDictionary *defaultDictionaryFeatureFlagValue = @{@"key1": @"value1", @"key2": @[@1, @2]}; + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsADictionary]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:kLDFlagKeyIsADictionary + reportedFlagValue:flagConfigValue.value + flagConfigValue:flagConfigValue + defaultFlagValue:defaultDictionaryFeatureFlagValue + user:self.environment.user]; + + NSDictionary *reportedValue = [self.environment dictionaryVariation:kLDFlagKeyIsADictionary fallback:defaultDictionaryFeatureFlagValue]; + + XCTAssertEqualObjects(reportedValue, flagConfigValue.value); + [self.dataManagerMock verify]; +} + +-(void)testDictionaryVariation_flagMissing { + [self.environment start]; + NSDictionary *defaultDictionaryFeatureFlagValue = @{@"key1": @"value1", @"key2": @[@1, @2]}; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:dummyFeatureFlagKey + reportedFlagValue:defaultDictionaryFeatureFlagValue + flagConfigValue:nil + defaultFlagValue:defaultDictionaryFeatureFlagValue + user:self.environment.user]; + + NSDictionary *reportedValue = [self.environment dictionaryVariation:dummyFeatureFlagKey fallback:defaultDictionaryFeatureFlagValue]; + + XCTAssertEqualObjects(reportedValue, defaultDictionaryFeatureFlagValue); + [self.dataManagerMock verify]; +} + +-(void)testDictionaryVariation_nullValue { + [self.environment start]; + NSDictionary *defaultDictionaryFeatureFlagValue = @{@"key1": @"value1", @"key2": @[@1, @2]}; + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:kLDFlagKeyIsANull]; + [[self.dataManagerMock expect] recordFlagEvaluationEventsWithFlagKey:kLDFlagKeyIsANull + reportedFlagValue:defaultDictionaryFeatureFlagValue + flagConfigValue:flagConfigValue + defaultFlagValue:defaultDictionaryFeatureFlagValue + user:self.environment.user]; + + NSDictionary *reportedValue = [self.environment dictionaryVariation:kLDFlagKeyIsANull fallback:defaultDictionaryFeatureFlagValue]; + + XCTAssertEqualObjects(reportedValue, defaultDictionaryFeatureFlagValue); + [self.dataManagerMock verify]; +} + +-(void)testDictionaryVariation_notStarted { + NSDictionary *defaultDictionaryFeatureFlagValue = @{@"key1": @"value1", @"key2": @[@1, @2]}; + [[self.dataManagerMock reject] recordFlagEvaluationEventsWithFlagKey:[OCMArg any] + reportedFlagValue:[OCMArg any] + flagConfigValue:[OCMArg any] + defaultFlagValue:[OCMArg any] + user:[OCMArg any]]; + + NSDictionary *reportedValue = [self.environment dictionaryVariation:kLDFlagKeyIsANull fallback:defaultDictionaryFeatureFlagValue]; + + XCTAssertEqualObjects(reportedValue, defaultDictionaryFeatureFlagValue); + [self.dataManagerMock verify]; +} + +-(void)testAllFlags { + [self.environment start]; + + NSDictionary *allFlags = self.environment.allFlags; + + for (NSString *flagKey in self.user.flagConfig.featuresJsonDictionary.allKeys) { + LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:flagKey]; + if (flagConfigValue.value == nil || [flagConfigValue.value isKindOfClass:[NSNull class]]) { + XCTAssertNil(allFlags[flagKey]); + } else { + XCTAssertEqualObjects(allFlags[flagKey], flagConfigValue.value); + } + } +} + +-(void)testAllFlags_notStarted { + NSDictionary *allFlags = self.environment.allFlags; + + XCTAssertNil(allFlags); +} + +#pragma mark - Event Tracking + +-(void)testTrack { + [self.environment start]; + NSDictionary *customEventData = @{@"The Answer":@(42)}; + [[self.dataManagerMock expect] recordCustomEventWithKey:customEventName customData:customEventData user:self.environment.user]; + + BOOL result = [self.environment track:customEventName data:customEventData]; + + XCTAssertTrue(result); + [self.dataManagerMock verify]; +} + +-(void)testTrack_notStarted { + NSDictionary *customEventData = @{@"The Answer":@(42)}; + [[self.dataManagerMock reject] recordCustomEventWithKey:[OCMArg any] customData:[OCMArg any] user:[OCMArg any]]; + + BOOL result = [self.environment track:customEventName data:customEventData]; + + XCTAssertFalse(result); + [self.dataManagerMock verify]; +} + +#pragma mark - User + +-(void)testUpdateUser { + [self.environment start]; + self.environment.online = YES; + LDUserModel *newUser = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + id newUserMock = [OCMockObject niceMockForClass:[LDUserModel class]]; + [[[newUserMock expect] andReturn:newUser] copy]; + [[self.dataManagerMock expect] recordSummaryEventWithTracker:self.environment.user.flagConfigTracker]; + [[self.environmentControllerMock expect] setOnline:NO]; + [[self.dataManagerMock expect] convertToEnvironmentBasedCacheForUser:newUser config:self.config]; + [[self.dataManagerMock expect] retrieveFlagConfigForUser:newUser]; + [[self.dataManagerMock expect] recordIdentifyEventWithUser:newUser]; + [[self.dataManagerMock expect] saveUser:newUser]; + [[[self.environmentControllerMock expect] andReturn:self.environmentControllerMock] controllerWithMobileKey:self.mobileKey config:self.config user:newUser dataManager:self.dataManagerMock]; + [[self.environmentControllerMock expect] setOnline:YES]; + + [self.environment updateUser:newUserMock]; + + XCTAssertEqualObjects(self.environment.user, newUser); + XCTAssertTrue(self.environment.isOnline); + [newUserMock verify]; + [self.environmentControllerMock verify]; + [self.dataManagerMock verify]; +} + +-(void)testUpdateUser_secondaryEnvironment { + //Replace the original mocks and environment with mocks and an environment configured with a different key than in config.mobileKey + NSString *mobileKey = [NSUUID UUID].UUIDString; + self.dataManagerMock = [OCMockObject niceMockForClass:[LDDataManager class]]; + [[[self.dataManagerMock stub] andReturn:self.dataManagerMock] dataManagerWithMobileKey:mobileKey config:self.config]; + LDUserModel *newUser = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + id newUserMock = [OCMockObject niceMockForClass:[LDUserModel class]]; + [[[newUserMock expect] andReturn:newUser] copy]; + id originalEnvironmentControllerMock = self.environmentControllerMock; //Keep the original, which should be called to set offline prior to setting the user + self.environmentControllerMock = [OCMockObject niceMockForClass:[LDEnvironmentController class]]; + [[[self.environmentControllerMock expect] andReturn:self.environmentControllerMock] controllerWithMobileKey:mobileKey config:self.config user:[OCMArg checkWithBlock:^BOOL(id obj) { + //Because the environment makes a copy of the user, this makes sure to serve the environmentControllerMock whenever the user passed into controllerWithMobileKey matches self.user + if (![obj isKindOfClass:[LDUserModel class]]) { return NO; } + LDUserModel *user = obj; + return [user isEqual:newUser ignoringAttributes:@[kUserAttributeUpdatedAt]]; + }] dataManager:self.dataManagerMock]; + self.environment = [LDEnvironment environmentForMobileKey:mobileKey config:self.config user:self.user]; + + [self.environment start]; + self.environment.online = YES; + [[self.dataManagerMock expect] recordSummaryEventWithTracker:self.environment.user.flagConfigTracker]; + [[originalEnvironmentControllerMock expect] setOnline:NO]; + [[self.dataManagerMock reject] convertToEnvironmentBasedCacheForUser:[OCMArg any] config:[OCMArg any]]; + [[self.dataManagerMock expect] retrieveFlagConfigForUser:newUser]; + [[self.dataManagerMock expect] recordIdentifyEventWithUser:newUser]; + [[self.dataManagerMock expect] saveUser:newUser]; + [[self.environmentControllerMock expect] setOnline:YES]; + + [self.environment updateUser:newUserMock]; + + XCTAssertEqualObjects(self.environment.user, newUser); + XCTAssertTrue(self.environment.isOnline); + [newUserMock verify]; + [self.environmentControllerMock verify]; + [self.dataManagerMock verify]; +} + +-(void)testUpdateUser_offline { + [self.environment start]; + LDUserModel *newUser = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + id newUserMock = [OCMockObject niceMockForClass:[LDUserModel class]]; + [[[newUserMock expect] andReturn:newUser] copy]; + [[self.dataManagerMock expect] recordSummaryEventWithTracker:self.environment.user.flagConfigTracker]; + [[self.environmentControllerMock reject] setOnline:NO]; + [[self.dataManagerMock expect] convertToEnvironmentBasedCacheForUser:newUser config:self.config]; + [[self.dataManagerMock expect] retrieveFlagConfigForUser:newUser]; + [[self.dataManagerMock expect] recordIdentifyEventWithUser:newUser]; + [[self.dataManagerMock expect] saveUser:newUser]; + [[[self.environmentControllerMock expect] andReturn:self.environmentControllerMock] controllerWithMobileKey:self.mobileKey config:self.config user:newUser dataManager:self.dataManagerMock]; + [[self.environmentControllerMock reject] setOnline:YES]; + + [self.environment updateUser:newUserMock]; + + XCTAssertEqualObjects(self.environment.user, newUser); + XCTAssertFalse(self.environment.isOnline); + [newUserMock verify]; + [self.environmentControllerMock verify]; + [self.dataManagerMock verify]; +} + +-(void)testUpdateUser_missingNewUser { + LDUserModel *originalEnvironmentUser = self.environment.user; + [self.environment start]; + self.environment.online = YES; + [[self.dataManagerMock reject] recordSummaryEventWithTracker:[OCMArg any]]; + [[self.environmentControllerMock reject] setOnline:[OCMArg any]]; + [[self.dataManagerMock reject] convertToEnvironmentBasedCacheForUser:[OCMArg any] config:[OCMArg any]]; + [[self.dataManagerMock reject] retrieveFlagConfigForUser:[OCMArg any]]; + [[self.dataManagerMock reject] recordIdentifyEventWithUser:[OCMArg any]]; + [[self.environmentControllerMock reject] controllerWithMobileKey:[OCMArg any] config:[OCMArg any] user:[OCMArg any] dataManager:[OCMArg any]]; + + [self.environment updateUser:nil]; + + XCTAssertEqualObjects(self.environment.user, originalEnvironmentUser); + XCTAssertTrue(self.environment.isOnline); + [self.environmentControllerMock verify]; + [self.dataManagerMock verify]; +} + +-(void)testUpdateUser_notStarted { + LDUserModel *originalEnvironmentUser = self.environment.user; + LDUserModel *newUser = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + [[self.dataManagerMock reject] recordSummaryEventWithTracker:[OCMArg any]]; + [[self.environmentControllerMock reject] setOnline:[OCMArg any]]; + [[self.dataManagerMock reject] convertToEnvironmentBasedCacheForUser:[OCMArg any] config:[OCMArg any]]; + [[self.dataManagerMock reject] retrieveFlagConfigForUser:[OCMArg any]]; + [[self.dataManagerMock reject] recordIdentifyEventWithUser:[OCMArg any]]; + [[self.environmentControllerMock reject] controllerWithMobileKey:[OCMArg any] config:[OCMArg any] user:[OCMArg any] dataManager:[OCMArg any]]; + + [self.environment updateUser:newUser]; + + XCTAssertEqualObjects(self.environment.user, originalEnvironmentUser); + XCTAssertFalse(self.environment.isOnline); + [self.environmentControllerMock verify]; + [self.dataManagerMock verify]; +} + +#pragma mark - Notification Handling + +-(void)testHandleUserUpdated { + [self.environment start]; + self.environment.online = YES; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserUpdatedNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey}]; + + XCTAssertEqual(self.clientDelegateMock.userDidUpdateCallCount, 1); +} + +-(void)testHandleUserUpdated_delegateDoesNotRespond { + id clientDelegateMock = [OCMockObject niceMockForClass:[ClientDelegateProtocolWithoutMethodsMock class]]; + self.environment.delegate = clientDelegateMock; + [self.environment start]; + self.environment.online = YES; + [[clientDelegateMock reject] userDidUpdate]; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserUpdatedNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey}]; + + [clientDelegateMock verify]; +} + +-(void)testHandleUserUpdated_otherEnvironment { + [self.environment start]; + self.environment.online = YES; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserUpdatedNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:secondaryEnvironmentMobileKey}]; + + XCTAssertEqual(self.clientDelegateMock.userDidUpdateCallCount, 0); +} + +- (void)testHandleUserUnchanged { + [self.environment start]; + self.environment.online = YES; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserNoChangeNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey}]; + + XCTAssertEqual(self.clientDelegateMock.userUnchangedCallCount, 1); +} + +- (void)testHandleUserUnchanged_otherEnvironment { + [self.environment start]; + self.environment.online = YES; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserNoChangeNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:secondaryEnvironmentMobileKey}]; + + XCTAssertEqual(self.clientDelegateMock.userUnchangedCallCount, 0); +} + +- (void)testHandleUserUnchanged_delegateDoesNotRespond { + id clientDelegateMock = [OCMockObject niceMockForClass:[ClientDelegateProtocolWithoutMethodsMock class]]; + self.environment.delegate = clientDelegateMock; + [self.environment start]; + self.environment.online = YES; + [[clientDelegateMock reject] userUnchanged]; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDUserNoChangeNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey}]; + + [clientDelegateMock verify]; +} + +- (void)testHandleFeatureFlagsChanged { + [self.environment start]; + self.environment.online = YES; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDFeatureFlagsChangedNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey, + kLDNotificationUserInfoKeyFlagKeys:@[featureFlagKey]}]; + + XCTAssertEqual(self.clientDelegateMock.featureFlagDidUpdateCallCount, 1); +} + +- (void)testHandleFeatureFlagsChanged_otherEnvironment { + [self.environment start]; + self.environment.online = YES; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDFeatureFlagsChangedNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:secondaryEnvironmentMobileKey, + kLDNotificationUserInfoKeyFlagKeys:@[featureFlagKey]}]; + + XCTAssertEqual(self.clientDelegateMock.featureFlagDidUpdateCallCount, 0); +} + +- (void)testHandleFeatureFlagsChanged_missingFlagKey { + [self.environment start]; + self.environment.online = YES; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDFeatureFlagsChangedNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey}]; + + XCTAssertEqual(self.clientDelegateMock.featureFlagDidUpdateCallCount, 0); +} + +- (void)testHandleFeatureFlagsChanged_delegateDoesNotRespond { + id clientDelegateMock = [OCMockObject niceMockForClass:[ClientDelegateProtocolWithoutMethodsMock class]]; + self.environment.delegate = clientDelegateMock; + [self.environment start]; + self.environment.online = YES; + [[clientDelegateMock reject] featureFlagDidUpdate:[OCMArg any]]; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDFeatureFlagsChangedNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey, + kLDNotificationUserInfoKeyFlagKeys:@[featureFlagKey]}]; + [clientDelegateMock verify]; +} + +-(void)testNotifyDelegateOfUpdates { + [self.environment start]; + self.environment.online = YES; + NSMutableArray *notifiedFlagKeys = [NSMutableArray arrayWithCapacity:[LDFlagConfigValue flagKeys].count]; + XCTestExpectation *delegateNotifiedExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"%@.%@.delegateNotifiedExpectation", + NSStringFromClass([self class]), NSStringFromSelector(_cmd)]]; + + self.clientDelegateMock.featureFlagDidUpdateCallback = ^(NSString *flagKey) { + [notifiedFlagKeys addObject:flagKey]; + if ([[NSSet setWithArray:notifiedFlagKeys] isEqualToSet:[NSSet setWithArray:[LDFlagConfigValue flagKeys]]]) { + [delegateNotifiedExpectation fulfill]; + } + }; + + [self.environment notifyDelegateOfUpdatesForFlagKeys:[LDFlagConfigValue flagKeys]]; + [self waitForExpectations:@[delegateNotifiedExpectation] timeout:1.0]; +} + +-(void)testNotifyDelegateOfUpdates_notStarted { + self.environment.online = YES; + id threadMock = [OCMockObject niceMockForClass:[NSThread class]]; + [[threadMock reject] performOnMainThread:[OCMArg any] waitUntilDone:[OCMArg any]]; + + [self.environment notifyDelegateOfUpdatesForFlagKeys:[LDFlagConfigValue flagKeys]]; + + XCTAssertEqual(self.clientDelegateMock.featureFlagDidUpdateCallCount, 0); + [threadMock verify]; +} + +-(void)testNotifyDelegateOfUpdates_offline { + [self.environment start]; + id threadMock = [OCMockObject niceMockForClass:[NSThread class]]; + [[threadMock reject] performOnMainThread:[OCMArg any] waitUntilDone:[OCMArg any]]; + + [self.environment notifyDelegateOfUpdatesForFlagKeys:[LDFlagConfigValue flagKeys]]; + + XCTAssertEqual(self.clientDelegateMock.featureFlagDidUpdateCallCount, 0); + [threadMock verify]; +} + +-(void)testNotifyDelegateOfUpdates_missingFlagKeys { + [self.environment start]; + self.environment.online = YES; + id threadMock = [OCMockObject niceMockForClass:[NSThread class]]; + [[threadMock reject] performOnMainThread:[OCMArg any] waitUntilDone:[OCMArg any]]; + + [self.environment notifyDelegateOfUpdatesForFlagKeys:nil]; + + XCTAssertEqual(self.clientDelegateMock.featureFlagDidUpdateCallCount, 0); + [threadMock verify]; +} + +-(void)testNotifyDelegateOfUpdates_emptyFlagKeys { + [self.environment start]; + self.environment.online = YES; + id threadMock = [OCMockObject niceMockForClass:[NSThread class]]; + [[threadMock reject] performOnMainThread:[OCMArg any] waitUntilDone:[OCMArg any]]; + + [self.environment notifyDelegateOfUpdatesForFlagKeys:@[]]; + + XCTAssertEqual(self.clientDelegateMock.featureFlagDidUpdateCallCount, 0); + [threadMock verify]; +} + +-(void)testNotifyDelegateOfUpdates_noDelegate { + self.environment.delegate = nil; + [self.environment start]; + self.environment.online = YES; + id threadMock = [OCMockObject niceMockForClass:[NSThread class]]; + [[threadMock reject] performOnMainThread:[OCMArg any] waitUntilDone:[OCMArg any]]; + + [self.environment notifyDelegateOfUpdatesForFlagKeys:[LDFlagConfigValue flagKeys]]; + + XCTAssertEqual(self.clientDelegateMock.featureFlagDidUpdateCallCount, 0); + [threadMock verify]; +} + +-(void)testNotifyDelegateOfUpdates_delegateMethodNotImplemented { + self.environment.delegate = (id)[[NSObject alloc] init]; + [self.environment start]; + self.environment.online = YES; + id threadMock = [OCMockObject niceMockForClass:[NSThread class]]; + [[threadMock reject] performOnMainThread:[OCMArg any] waitUntilDone:[OCMArg any]]; + + [self.environment notifyDelegateOfUpdatesForFlagKeys:[LDFlagConfigValue flagKeys]]; + + XCTAssertEqual(self.clientDelegateMock.featureFlagDidUpdateCallCount, 0); + [threadMock verify]; +} + +- (void)testHandleServerUnavailable { + [self.environment start]; + self.environment.online = YES; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDServerConnectionUnavailableNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey}]; + + XCTAssertEqual(self.clientDelegateMock.serverConnectionUnavailableCallCount, 1); +} + +- (void)testHandleServerUnavailable_otherEnvironment { + [self.environment start]; + self.environment.online = YES; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDServerConnectionUnavailableNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:secondaryEnvironmentMobileKey}]; + + XCTAssertEqual(self.clientDelegateMock.serverConnectionUnavailableCallCount, 0); +} + +- (void)testHandleServerUnavailable_delegateDoesNotRespond { + id clientDelegateMock = [OCMockObject niceMockForClass:[ClientDelegateProtocolWithoutMethodsMock class]]; + self.environment.delegate = clientDelegateMock; + [self.environment start]; + self.environment.online = YES; + [[clientDelegateMock reject] serverConnectionUnavailable]; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDServerConnectionUnavailableNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey}]; + + [clientDelegateMock verify]; +} + +- (void)testHandleClientUnauthorized { + [self.environment start]; + self.environment.online = YES; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDClientUnauthorizedNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey}]; + + XCTAssertEqual(self.environment.isOnline, NO); +} + +- (void)testHandleClientUnauthorized_otherEnvironment { + [self.environment start]; + self.environment.online = YES; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDClientUnauthorizedNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:secondaryEnvironmentMobileKey}]; + + XCTAssertEqual(self.environment.isOnline, YES); +} + +- (void)testHandleClientUnauthorized_delegateDoesNotRespond { + id clientDelegateMock = [OCMockObject niceMockForClass:[ClientDelegateProtocolWithoutMethodsMock class]]; + self.environment.delegate = clientDelegateMock; + [self.environment start]; + self.environment.online = YES; + [[clientDelegateMock reject] serverConnectionUnavailable]; + + [[NSNotificationCenter defaultCenter] postNotificationName:kLDClientUnauthorizedNotification object:nil userInfo:@{kLDNotificationUserInfoKeyMobileKey:self.mobileKey}]; + + XCTAssertEqual(self.environment.isOnline, NO); +} + +@end diff --git a/DarklyTests/LDPollingManagerTest.m b/DarklyTests/LDPollingManagerTest.m index 76cfc815..fdd4f991 100644 --- a/DarklyTests/LDPollingManagerTest.m +++ b/DarklyTests/LDPollingManagerTest.m @@ -4,261 +4,461 @@ #import "DarklyXCTestCase.h" #import "LDPollingManager.h" -#import "LDClientManager.h" #import "LDConfig.h" -#import "LDClient.h" #import "OCMock.h" +#import "NSNumber+LaunchDarkly.h" extern NSString *const kTestMobileKey; +NSString *const kAlternateMobileKey = @"alternateMobileKey"; + +@interface LDPollingManager (Testable) +-(uint64_t)flagConfigPollingIntervalNanos; +-(uint64_t)eventPollingIntervalNanos; +-(void)flagConfigPoll; +-(void)eventPoll; +@end @interface LDPollingManagerTest : DarklyXCTestCase -@property (nonatomic, strong) id mockLDClient; -@property (nonatomic, strong) id mockLDClientManager; @end @implementation LDPollingManagerTest - - (void)setUp { [super setUp]; - - id mockLDClient = OCMClassMock([LDClient class]); - OCMStub(ClassMethod([mockLDClient sharedInstance])).andReturn(mockLDClient); - self.mockLDClient = mockLDClient; - - id mockLDClientManager = OCMClassMock([LDClientManager class]); - OCMStub(ClassMethod([mockLDClientManager sharedInstance])).andReturn(mockLDClientManager); - self.mockLDClientManager = mockLDClientManager; - } +} - (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. - [self.mockLDClient stopMocking]; - [self.mockLDClientManager stopMocking]; + [[LDPollingManager sharedInstance] stopFlagConfigPolling]; + [[LDPollingManager sharedInstance] stopEventPolling]; [super tearDown]; } +- (void)testFlagConfigPollingStates { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; + config.streaming = NO; + + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingState, POLL_RUNNING); + + [[LDPollingManager sharedInstance] suspendFlagConfigPolling]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingState, POLL_SUSPENDED); + + [[LDPollingManager sharedInstance] stopFlagConfigPolling]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingState, POLL_STOPPED); +} + +- (void)testFlagConfigPollingState_suspendPolling_notRunning { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; + config.streaming = NO; + + [[LDPollingManager sharedInstance] suspendFlagConfigPolling]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingState, POLL_STOPPED); +} + +- (void)testFlagConfigPollingState_resumePolling { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; + config.streaming = NO; + + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingState, POLL_RUNNING); + + [[LDPollingManager sharedInstance] suspendFlagConfigPolling]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingState, POLL_SUSPENDED); + + [[LDPollingManager sharedInstance] resumeFlagConfigPollingWhenIsOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingState, POLL_RUNNING); +} + +- (void)testFlagConfigPollingState_stopPolling_pollRunning { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; + config.streaming = NO; + + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingState, POLL_RUNNING); + + [[LDPollingManager sharedInstance] stopFlagConfigPolling]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingState, POLL_STOPPED); +} + - (void)testEventPollingStates { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - OCMStub([self.mockLDClient ldConfig]).andReturn(config); - - OCMStub([self.mockLDClientManager isOnline]).andReturn(YES); - - // create the expectation with a nice descriptive message - LDPollingManager *dnu = [LDPollingManager sharedInstance]; - dnu.eventPollingIntervalMillis = 5000; // for the purposes of the unit tests set it to 5 secs. - [dnu startEventPolling]; - - NSInteger expectedValue = POLL_RUNNING; - NSInteger actualValue = [dnu eventPollingState]; - - XCTAssertTrue(actualValue == expectedValue); - - [dnu pauseEventPolling]; - - expectedValue = POLL_PAUSED; - actualValue = [dnu eventPollingState]; - - XCTAssertTrue(actualValue == expectedValue); - - [dnu stopEventPolling]; - - expectedValue = POLL_STOPPED; - actualValue = [dnu eventPollingState]; - - XCTAssertTrue(actualValue == expectedValue); -} - -- (void)testPollingInterval { + + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingState, POLL_RUNNING); + + [[LDPollingManager sharedInstance] suspendEventPolling]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingState, POLL_SUSPENDED); + + [[LDPollingManager sharedInstance] stopEventPolling]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingState, POLL_STOPPED); +} + +- (void)testEventPollingState_suspendPolling_notRunning { + [[LDPollingManager sharedInstance] suspendEventPolling]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingState, POLL_STOPPED); +} + +- (void)testEventPollingState_resumePolling { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; + + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingState, POLL_RUNNING); + + [[LDPollingManager sharedInstance] suspendEventPolling]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingState, POLL_SUSPENDED); + + [[LDPollingManager sharedInstance] resumeEventPollingWhenIsOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingState, POLL_RUNNING); +} + +- (void)testEventPollingState_stopPolling_pollRunning { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; + + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingState, POLL_RUNNING); + + [[LDPollingManager sharedInstance] stopEventPolling]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingState, POLL_STOPPED); +} + +-(void)testPollingInterval_defaultConfig { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; + + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingIntervalNanos, [@(kDefaultPollingInterval) nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + XCTAssertEqual([[LDPollingManager sharedInstance] flagConfigPollingState], POLL_RUNNING); + [[LDPollingManager sharedInstance] stopFlagConfigPolling]; + + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingIntervalNanos, [@(kDefaultFlushInterval) nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + XCTAssertEqual([[LDPollingManager sharedInstance] eventPollingState], POLL_RUNNING); + [[LDPollingManager sharedInstance] stopEventPolling]; +} + +-(void)testPollingInterval_belowMinima { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - OCMStub([self.mockLDClient ldConfig]).andReturn(config); - OCMStub([self.mockLDClientManager isOnline]).andReturn(YES); - LDPollingManager *pollingManager = [LDPollingManager sharedInstance]; + config.pollingInterval = @(kMinimumPollingInterval - 1); //config prevents this, and instead sets the min polling interval + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingIntervalNanos, [@(kMinimumPollingInterval) nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + [[LDPollingManager sharedInstance] stopFlagConfigPolling]; + + config.flushInterval = @(kMinimumFlushInterval - 1); + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingIntervalNanos, [@(kMinimumFlushInterval) nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + [[LDPollingManager sharedInstance] stopEventPolling]; +} - [pollingManager startConfigPolling]; - XCTAssertEqual(pollingManager.configPollingIntervalMillis, kDefaultPollingInterval*kMillisInSecs); - XCTAssertEqual([pollingManager configPollingState], POLL_RUNNING); - [pollingManager stopConfigPolling]; +-(void)testPollingInterval_atMinima { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - [pollingManager startEventPolling]; - XCTAssertEqual(pollingManager.eventPollingIntervalMillis, kDefaultFlushInterval*kMillisInSecs); - XCTAssertEqual([pollingManager eventPollingState], POLL_RUNNING); - [pollingManager stopEventPolling]; + config.pollingInterval = @(kMinimumPollingInterval); + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingIntervalNanos, [config.pollingInterval nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + [[LDPollingManager sharedInstance] stopFlagConfigPolling]; + + config.flushInterval = @(kMinimumFlushInterval); + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingIntervalNanos, [config.flushInterval nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + [[LDPollingManager sharedInstance] stopEventPolling]; +} - config.pollingInterval = [NSNumber numberWithInt:kMinimumPollingInterval - 1]; - [pollingManager startConfigPolling]; - XCTAssertEqual(pollingManager.configPollingIntervalMillis, kMinimumPollingInterval*kMillisInSecs); - [pollingManager stopConfigPolling]; +- (void)testPollingInterval_justAboveMinima { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; config.pollingInterval = [NSNumber numberWithInt:kMinimumPollingInterval + 1]; - [pollingManager startConfigPolling]; - XCTAssertEqual(pollingManager.configPollingIntervalMillis, [config.pollingInterval intValue]*kMillisInSecs); - [pollingManager stopConfigPolling]; + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingIntervalNanos, [config.pollingInterval nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + [[LDPollingManager sharedInstance] stopFlagConfigPolling]; config.flushInterval = [NSNumber numberWithInt:50]; - [pollingManager startEventPolling]; - XCTAssertEqual(pollingManager.eventPollingIntervalMillis, [config.flushInterval intValue]*kMillisInSecs); - [pollingManager stopEventPolling]; + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingIntervalNanos, [config.flushInterval nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + [[LDPollingManager sharedInstance] stopEventPolling]; +} + +-(void)testPollingInterval_noConfig { + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:nil isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingIntervalNanos, [@(kDefaultPollingInterval) nanoSecondValue]); + XCTAssertNil([LDPollingManager sharedInstance].config); + XCTAssertEqual([[LDPollingManager sharedInstance] flagConfigPollingState], POLL_RUNNING); + [[LDPollingManager sharedInstance] stopFlagConfigPolling]; + + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:nil isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingIntervalNanos, [@(kDefaultFlushInterval) nanoSecondValue]); + XCTAssertEqual([[LDPollingManager sharedInstance] eventPollingState], POLL_RUNNING); + [[LDPollingManager sharedInstance] stopEventPolling]; +} + +-(void)testPollingInterval_pollingModeDefaultEventPollingInterval { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; + config.streaming = NO; + + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingIntervalNanos, [@(kDefaultPollingInterval) nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + XCTAssertEqual([[LDPollingManager sharedInstance] flagConfigPollingState], POLL_RUNNING); + [[LDPollingManager sharedInstance] stopFlagConfigPolling]; + + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingIntervalNanos, [@(kDefaultPollingInterval) nanoSecondValue]); //Event polling interval matches flagConfig polling interval + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + XCTAssertEqual([[LDPollingManager sharedInstance] eventPollingState], POLL_RUNNING); + [[LDPollingManager sharedInstance] stopEventPolling]; } -- (void)testConfigPollingOnline { +- (void)testFlagConfigPolling { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - OCMStub([self.mockLDClient ldConfig]).andReturn(config); - OCMStub([self.mockLDClientManager isOnline]).andReturn(YES); + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; + XCTAssertTrue([LDPollingManager sharedInstance].flagConfigPollingState == POLL_RUNNING); - LDPollingManager *pollingManager = [LDPollingManager sharedInstance]; - [pollingManager stopConfigPolling]; //Do not assume anything about polling manager state - XCTAssertTrue(pollingManager.configPollingState == POLL_STOPPED); + [[LDPollingManager sharedInstance] stopFlagConfigPolling]; + XCTAssertTrue([LDPollingManager sharedInstance].flagConfigPollingState == POLL_STOPPED); +} - [pollingManager startConfigPolling]; - XCTAssertTrue(pollingManager.configPollingState == POLL_RUNNING); +-(void)testStartFlagConfigPolling_offline { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - [pollingManager stopConfigPolling]; - XCTAssertTrue(pollingManager.configPollingState == POLL_STOPPED); + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:NO]; + XCTAssertTrue([LDPollingManager sharedInstance].flagConfigPollingState == POLL_STOPPED); } -- (void)testConfigPollingOffline { +- (void)testResumeFlagConfigPolling { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - OCMStub([self.mockLDClient ldConfig]).andReturn(config); - OCMStub([self.mockLDClientManager isOnline]).andReturn(NO); + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; + XCTAssertTrue([LDPollingManager sharedInstance].flagConfigPollingState == POLL_RUNNING); - LDPollingManager *pollingManager = [LDPollingManager sharedInstance]; - [pollingManager stopConfigPolling]; //Do not assume anything about polling manager state - XCTAssertTrue(pollingManager.configPollingState == POLL_STOPPED); + [[LDPollingManager sharedInstance] suspendFlagConfigPolling]; + XCTAssertTrue([LDPollingManager sharedInstance].flagConfigPollingState == POLL_SUSPENDED); - [pollingManager startConfigPolling]; - XCTAssertTrue(pollingManager.configPollingState == POLL_STOPPED); + [[LDPollingManager sharedInstance] resumeFlagConfigPollingWhenIsOnline:YES]; + XCTAssertTrue([LDPollingManager sharedInstance].flagConfigPollingState == POLL_RUNNING); + + [[LDPollingManager sharedInstance] stopFlagConfigPolling]; + XCTAssertTrue([LDPollingManager sharedInstance].flagConfigPollingState == POLL_STOPPED); +} + +- (void)testResumeFlagConfigPolling_pollStopped { + [[LDPollingManager sharedInstance] resumeFlagConfigPollingWhenIsOnline:YES]; + + XCTAssertTrue([LDPollingManager sharedInstance].flagConfigPollingState == POLL_STOPPED); } -- (void)testResumeConfigPollingOnline { +- (void)testResumeFlagConfigPolling_pollRunning { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - OCMStub([self.mockLDClient ldConfig]).andReturn(config); - OCMStub([self.mockLDClientManager isOnline]).andReturn(YES); + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; + XCTAssertTrue([LDPollingManager sharedInstance].flagConfigPollingState == POLL_RUNNING); - LDPollingManager *pollingManager = [LDPollingManager sharedInstance]; - [pollingManager stopConfigPolling]; //Do not assume anything about polling manager state - XCTAssertTrue(pollingManager.configPollingState == POLL_STOPPED); + [[LDPollingManager sharedInstance] resumeFlagConfigPollingWhenIsOnline:YES]; + XCTAssertTrue([LDPollingManager sharedInstance].flagConfigPollingState == POLL_RUNNING); +} - [pollingManager startConfigPolling]; - XCTAssertTrue(pollingManager.configPollingState == POLL_RUNNING); +- (void)testResumeFlagConfigPolling_offline { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - [pollingManager suspendConfigPolling]; - XCTAssertTrue(pollingManager.configPollingState == POLL_SUSPENDED); + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; + XCTAssertTrue([LDPollingManager sharedInstance].flagConfigPollingState == POLL_RUNNING); - [pollingManager resumeConfigPolling]; - XCTAssertTrue(pollingManager.configPollingState == POLL_RUNNING); + [[LDPollingManager sharedInstance] suspendFlagConfigPolling]; + XCTAssertTrue([LDPollingManager sharedInstance].flagConfigPollingState == POLL_SUSPENDED); - [pollingManager stopConfigPolling]; - XCTAssertTrue(pollingManager.configPollingState == POLL_STOPPED); + [[LDPollingManager sharedInstance] resumeFlagConfigPollingWhenIsOnline:NO]; + XCTAssertTrue([LDPollingManager sharedInstance].flagConfigPollingState == POLL_SUSPENDED); } -- (void)testResumeConfigPollingOffline { +- (void)testEventPolling { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - OCMStub([self.mockLDClient ldConfig]).andReturn(config); - [[[self.mockLDClientManager expect] andReturnValue:OCMOCK_VALUE(YES)] isOnline]; + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + XCTAssertTrue([LDPollingManager sharedInstance].eventPollingState == POLL_RUNNING); - LDPollingManager *pollingManager = [LDPollingManager sharedInstance]; - [pollingManager stopConfigPolling]; //Do not assume anything about polling manager state - XCTAssertTrue(pollingManager.configPollingState == POLL_STOPPED); + [[LDPollingManager sharedInstance] stopEventPolling]; + XCTAssertTrue([LDPollingManager sharedInstance].eventPollingState == POLL_STOPPED); +} + +-(void)testStartEventPolling_offline { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - [pollingManager startConfigPolling]; - XCTAssertTrue(pollingManager.configPollingState == POLL_RUNNING); + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:NO]; + XCTAssertTrue([LDPollingManager sharedInstance].eventPollingState == POLL_STOPPED); +} + +- (void)testResumeEventPolling { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - [pollingManager suspendConfigPolling]; - XCTAssertTrue(pollingManager.configPollingState == POLL_SUSPENDED); + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + XCTAssertTrue([LDPollingManager sharedInstance].eventPollingState == POLL_RUNNING); - [[[self.mockLDClientManager expect] andReturnValue:OCMOCK_VALUE(NO)] isOnline]; - XCTAssertTrue([LDClientManager sharedInstance].isOnline == NO); + [[LDPollingManager sharedInstance] suspendEventPolling]; + XCTAssertTrue([LDPollingManager sharedInstance].eventPollingState == POLL_SUSPENDED); - [pollingManager resumeConfigPolling]; - XCTAssertTrue(pollingManager.configPollingState == POLL_SUSPENDED); + [[LDPollingManager sharedInstance] resumeEventPollingWhenIsOnline:YES]; + XCTAssertTrue([LDPollingManager sharedInstance].eventPollingState == POLL_RUNNING); - [pollingManager stopConfigPolling]; - XCTAssertTrue(pollingManager.configPollingState == POLL_STOPPED); + [[LDPollingManager sharedInstance] stopEventPolling]; + XCTAssertTrue([LDPollingManager sharedInstance].eventPollingState == POLL_STOPPED); } -- (void)testEventPollingOnline { +- (void)testResumeEventPolling_offline { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - OCMStub([self.mockLDClient ldConfig]).andReturn(config); - OCMStub([self.mockLDClientManager isOnline]).andReturn(YES); + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + XCTAssertTrue([LDPollingManager sharedInstance].eventPollingState == POLL_RUNNING); - LDPollingManager *pollingManager = [LDPollingManager sharedInstance]; - [pollingManager stopEventPolling]; //Do not assume anything about polling manager state - XCTAssertTrue(pollingManager.eventPollingState == POLL_STOPPED); + [[LDPollingManager sharedInstance] suspendEventPolling]; + XCTAssertTrue([LDPollingManager sharedInstance].eventPollingState == POLL_SUSPENDED); - [pollingManager startEventPolling]; - XCTAssertTrue(pollingManager.eventPollingState == POLL_RUNNING); + [[LDPollingManager sharedInstance] resumeEventPollingWhenIsOnline:NO]; + XCTAssertTrue([LDPollingManager sharedInstance].eventPollingState == POLL_SUSPENDED); +} + +- (void)testResumeEventPolling_pollStopped { + [[LDPollingManager sharedInstance] resumeEventPollingWhenIsOnline:YES]; - [pollingManager stopEventPolling]; - XCTAssertTrue(pollingManager.eventPollingState == POLL_STOPPED); + XCTAssertTrue([LDPollingManager sharedInstance].eventPollingState == POLL_STOPPED); } -- (void)testEventPollingOffline { +- (void)testResumeEventPolling_pollRunning { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - OCMStub([self.mockLDClient ldConfig]).andReturn(config); - OCMStub([self.mockLDClientManager isOnline]).andReturn(NO); + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + XCTAssertTrue([LDPollingManager sharedInstance].eventPollingState == POLL_RUNNING); + + [[LDPollingManager sharedInstance] resumeEventPollingWhenIsOnline:YES]; + XCTAssertTrue([LDPollingManager sharedInstance].eventPollingState == POLL_RUNNING); +} - LDPollingManager *pollingManager = [LDPollingManager sharedInstance]; - [pollingManager stopEventPolling]; //Do not assume anything about polling manager state - XCTAssertTrue(pollingManager.eventPollingState == POLL_STOPPED); - [pollingManager startEventPolling]; - XCTAssertTrue(pollingManager.eventPollingState == POLL_STOPPED); +- (void)testFlagConfigPoll { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; + config.streaming = NO; + id flagConfigTimerFiredObserver = OCMObserverMock(); + [[NSNotificationCenter defaultCenter] addMockObserver:flagConfigTimerFiredObserver name:kLDFlagConfigTimerFiredNotification object:nil]; + [[flagConfigTimerFiredObserver expect] notificationWithName: kLDFlagConfigTimerFiredNotification object:nil]; + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:flagConfigTimerFiredObserver]; + }; + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingState, POLL_RUNNING); + + [[LDPollingManager sharedInstance] flagConfigPoll]; + + [flagConfigTimerFiredObserver verify]; } -- (void)testResumeEventPollingOnline { +- (void)testFlagConfigPoll_pollNotRunning { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - OCMStub([self.mockLDClient ldConfig]).andReturn(config); + config.streaming = NO; + id flagConfigTimerFiredObserver = OCMObserverMock(); + [[NSNotificationCenter defaultCenter] addMockObserver:flagConfigTimerFiredObserver name:kLDFlagConfigTimerFiredNotification object:nil]; + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:flagConfigTimerFiredObserver]; + }; - OCMStub([self.mockLDClientManager isOnline]).andReturn(YES); + [[LDPollingManager sharedInstance] flagConfigPoll]; - LDPollingManager *pollingManager = [LDPollingManager sharedInstance]; - [pollingManager stopEventPolling]; //Do not assume anything about polling manager state - XCTAssertTrue(pollingManager.eventPollingState == POLL_STOPPED); + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingState, POLL_STOPPED); + [flagConfigTimerFiredObserver verify]; +} - [pollingManager startEventPolling]; - XCTAssertTrue(pollingManager.eventPollingState == POLL_RUNNING); +- (void)testEventPoll { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; + id eventTimerFiredObserver = OCMObserverMock(); + [[NSNotificationCenter defaultCenter] addMockObserver:eventTimerFiredObserver name:kLDEventTimerFiredNotification object:nil]; + [[eventTimerFiredObserver expect] notificationWithName: kLDEventTimerFiredNotification object:[OCMArg any]]; + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:eventTimerFiredObserver]; + }; + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingState, POLL_RUNNING); + + [[LDPollingManager sharedInstance] eventPoll]; + + [eventTimerFiredObserver verify]; +} - [pollingManager suspendEventPolling]; - XCTAssertTrue(pollingManager.eventPollingState == POLL_SUSPENDED); +- (void)testEventPoll_pollNotRunning { + id eventTimerFiredObserver = OCMObserverMock(); + [[NSNotificationCenter defaultCenter] addMockObserver:eventTimerFiredObserver name:kLDEventTimerFiredNotification object:nil]; + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:eventTimerFiredObserver]; + }; - [pollingManager resumeEventPolling]; - XCTAssertTrue(pollingManager.eventPollingState == POLL_RUNNING); + [[LDPollingManager sharedInstance] eventPoll]; - [pollingManager stopEventPolling]; - XCTAssertTrue(pollingManager.eventPollingState == POLL_STOPPED); + [eventTimerFiredObserver verify]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingState, POLL_STOPPED); } -- (void)testResumeEventPollingOffline { +- (void)testStartFlagConfigPollingUsingConfig { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - OCMStub([self.mockLDClient ldConfig]).andReturn(config); - [[[self.mockLDClientManager expect] andReturnValue:OCMOCK_VALUE(YES)] isOnline]; + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; + + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingIntervalNanos, [@(kDefaultPollingInterval) nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + XCTAssertEqual([[LDPollingManager sharedInstance] flagConfigPollingState], POLL_RUNNING); +} + +- (void)testStartFlagConfigPollingUsingConfig_pollRunning { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - LDPollingManager *pollingManager = [LDPollingManager sharedInstance]; - [pollingManager stopEventPolling]; //Do not assume anything about polling manager state - XCTAssertTrue(pollingManager.eventPollingState == POLL_STOPPED); + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:config isOnline:YES]; - [pollingManager startEventPolling]; - XCTAssertTrue(pollingManager.eventPollingState == POLL_RUNNING); + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingIntervalNanos, [@(kDefaultPollingInterval) nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + XCTAssertEqual([[LDPollingManager sharedInstance] flagConfigPollingState], POLL_RUNNING); + + //Once the poll is running, it shouldn't start again until it's been stopped + LDConfig *altConfig = [[LDConfig alloc] initWithMobileKey:kAlternateMobileKey]; + [[LDPollingManager sharedInstance] startFlagConfigPollingUsingConfig:altConfig isOnline:YES]; + + XCTAssertEqual([LDPollingManager sharedInstance].flagConfigPollingIntervalNanos, [@(kDefaultPollingInterval) nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + XCTAssertEqual([[LDPollingManager sharedInstance] flagConfigPollingState], POLL_RUNNING); + + [[LDPollingManager sharedInstance] stopFlagConfigPolling]; + XCTAssertEqual([[LDPollingManager sharedInstance] flagConfigPollingState], POLL_STOPPED); +} + +- (void)testStartEventPollingUsingConfig { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; + + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingIntervalNanos, [@(kDefaultFlushInterval) nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingState, POLL_RUNNING); +} + +- (void)testStartEventPollingUsingConfig_pollRunning { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - [pollingManager suspendEventPolling]; - XCTAssertTrue(pollingManager.eventPollingState == POLL_SUSPENDED); + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:config isOnline:YES]; + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingIntervalNanos, [@(kDefaultFlushInterval) nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingState, POLL_RUNNING); - [[[self.mockLDClientManager expect] andReturnValue:OCMOCK_VALUE(NO)] isOnline]; - XCTAssertTrue([LDClientManager sharedInstance].isOnline == NO); + //Once the poll is running, it shouldn't start again until it's been stopped + LDConfig *altConfig = [[LDConfig alloc] initWithMobileKey:kAlternateMobileKey]; + [[LDPollingManager sharedInstance] startEventPollingUsingConfig:altConfig isOnline:YES]; - [pollingManager resumeEventPolling]; - XCTAssertTrue(pollingManager.eventPollingState == POLL_SUSPENDED); + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingIntervalNanos, [@(kDefaultFlushInterval) nanoSecondValue]); + XCTAssertEqualObjects([LDPollingManager sharedInstance].config, config); + XCTAssertEqual([LDPollingManager sharedInstance].eventPollingState, POLL_RUNNING); - [pollingManager stopEventPolling]; - XCTAssertTrue(pollingManager.eventPollingState == POLL_STOPPED); + [[LDPollingManager sharedInstance] stopEventPolling]; + XCTAssertEqual([[LDPollingManager sharedInstance] eventPollingState], POLL_STOPPED); } @end diff --git a/DarklyTests/LDRequestManagerTest.m b/DarklyTests/LDRequestManagerTest.m index 4d692a37..bab46ca4 100644 --- a/DarklyTests/LDRequestManagerTest.m +++ b/DarklyTests/LDRequestManagerTest.m @@ -7,33 +7,54 @@ #import "DarklyXCTestCase.h" #import "LDRequestManager.h" -#import "LDDataManager.h" -#import "LDClientManager.h" -#import "LDUserBuilder.h" +#import "LDRequestManager+Testable.h" +#import "LDRequestManagerDelegateMock.h" #import "LDConfig.h" #import "LDConfig+Testable.h" -#import "LDClient.h" #import "NSDateFormatter+JsonHeader.h" #import "NSDateFormatter+JsonHeader+Testable.h" #import "NSHTTPURLResponse+LaunchDarkly+Testable.h" +#import "LDUtil.h" +#import "NSURLSession+LaunchDarkly.h" extern NSString * const kEventHeaderLaunchDarklyEventSchema; extern NSString * const kEventSchema; +extern NSString * const kFlagResponseHeaderEtag; +extern NSString * const kFlagRequestHeaderIfNoneMatch; static NSString *const httpMethodGet = @"GET"; static NSString *const testMobileKey = @"testMobileKey"; static NSString *const emptyJson = @"{ }"; static NSString *const flagRequestHost = @"app.launchdarkly.com"; static NSString *const eventRequestHost = @"mobile.launchdarkly.com"; +NSString * const etagStub = @"com.launchdarkly.test.requestManager.etag"; static const int httpStatusCodeOk = 200; static const int httpStatusCodeUnauthorized = 401; static const int httpStatusCodeInternalServerError = 500; +@interface NSURLRequest (LDRequestManagerTest) +@property (copy, nonatomic, readonly) NSString *ifNoneMatchHeader; +-(BOOL)hasHost:(NSString*)host method:(NSString*)method ifNoneMatchHeader:(NSString*)ifNoneMatchHeader cachePolicy:(NSURLRequestCachePolicy)cachePolicy; +@end + +@implementation NSURLRequest (LDRequestManagerTest) +-(NSString*)ifNoneMatchHeader { + return self.allHTTPHeaderFields[kFlagRequestHeaderIfNoneMatch]; +} + +-(BOOL)hasHost:(NSString*)host method:(NSString*)method ifNoneMatchHeader:(NSString*)ifNoneMatchHeader cachePolicy:(NSURLRequestCachePolicy)cachePolicy { + return [self.URL.host isEqualToString:host] + && [self.HTTPMethod isEqualToString:method] + && ((ifNoneMatchHeader == nil && self.ifNoneMatchHeader == nil) || (ifNoneMatchHeader != nil && [self.ifNoneMatchHeader isEqualToString:ifNoneMatchHeader])) + && self.cachePolicy == cachePolicy; +} +@end + @interface LDRequestManagerTest : DarklyXCTestCase -@property (nonatomic, strong) id clientManagerMock; -@property (nonatomic, strong) id ldClientMock; +@property (nonatomic, strong) LDRequestManager *requestManager; +@property (nonatomic, strong) LDConfig *config; +@property (nonatomic, strong) LDUserModel *user; @property (nonatomic, strong) id requestManagerDelegateMock; - @end @implementation LDRequestManagerTest @@ -42,47 +63,149 @@ - (void)setUp { [super setUp]; [OHHTTPStubs removeAllStubs]; - self.ldClientMock = [self mockClientWithUser:[self mockUser] config:[self testConfig]]; - - id clientManagerMock = OCMClassMock([LDClientManager class]); - OCMStub(ClassMethod([clientManagerMock sharedInstance])).andReturn(clientManagerMock); - OCMStub([clientManagerMock isOnline]).andReturn(YES); - self.clientManagerMock = clientManagerMock; - + self.config = [self testConfig]; + self.user = [self mockUser]; + [NSURLSession setSharedLDSessionForConfig:self.config]; + self.requestManager = [LDRequestManager requestManagerForMobileKey:self.config.mobileKey config:self.config delegate:nil callbackQueue:nil]; } - (void)tearDown { - [LDRequestManager sharedInstance].delegate = nil; [self.requestManagerDelegateMock stopMocking]; self.requestManagerDelegateMock = nil; - [self.ldClientMock stopMocking]; - self.ldClientMock = nil; - [self.clientManagerMock stopMocking]; - self.clientManagerMock = nil; + self.requestManager.delegate = nil; [OHHTTPStubs onStubActivation:nil]; [OHHTTPStubs removeAllStubs]; [super tearDown]; } +- (void)testInitAndConstructRequestManager { + id requestManagerDelegateMock = [OCMockObject mockForProtocol:@protocol(RequestManagerDelegate)]; + LDConfig *config = [self testConfig]; + dispatch_queue_t completionQueue = dispatch_queue_create("com.launchdarkly.LDRequestManagerTest.completionQueue", DISPATCH_QUEUE_SERIAL); + + self.requestManager = [LDRequestManager requestManagerForMobileKey:config.mobileKey config:config delegate:requestManagerDelegateMock callbackQueue:completionQueue]; + + XCTAssertNotNil(self.requestManager); + XCTAssertEqualObjects(self.requestManager.mobileKey, config.mobileKey); + XCTAssertEqualObjects(self.requestManager.config, config); + XCTAssertEqualObjects(self.requestManager.delegate, requestManagerDelegateMock); + XCTAssertEqualObjects(self.requestManager.callbackQueue, completionQueue); +} + - (void)testPerformFeatureFlagRequest_GetRequest_Success { XCTestExpectation *getRequestMade = [self expectationWithDescription:@"feature flag GET request made"]; id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; [[requestManagerDelegateMock expect] processedConfig:YES jsonConfigDictionary:[OCMArg isKindOfClass:[NSDictionary class]]]; - [LDRequestManager sharedInstance].delegate = requestManagerDelegateMock; + self.requestManager.delegate = requestManagerDelegateMock; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { - return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:httpMethodGet]; + return [request hasHost:flagRequestHost method:httpMethodGet ifNoneMatchHeader:nil cachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { [getRequestMade fulfill]; return [OHHTTPStubsResponse responseWithData: [self successJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; }]; - [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; - [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) { + [self waitForExpectationsWithTimeout:1 handler:^(NSError * _Nullable error) { + [requestManagerDelegateMock verifyWithDelay:1]; + XCTAssertEqualObjects(self.requestManager.featureFlagEtag, etagStub); + }]; +} + +- (void)testPerformFeatureFlagRequest_GetRequest_Success_etagExists { + XCTestExpectation *getRequestMade = [self expectationWithDescription:@"feature flag GET request made"]; + self.requestManager.featureFlagEtag = etagStub; + + id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; + [[requestManagerDelegateMock expect] processedConfig:YES jsonConfigDictionary:[OCMArg isKindOfClass:[NSDictionary class]]]; + self.requestManager.delegate = requestManagerDelegateMock; + + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request hasHost:flagRequestHost method:httpMethodGet ifNoneMatchHeader:etagStub cachePolicy:NSURLRequestUseProtocolCachePolicy]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + [getRequestMade fulfill]; + return [OHHTTPStubsResponse responseWithData: [self successJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; + }]; + + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; + + [self waitForExpectations:@[getRequestMade] timeout:1.0]; + [requestManagerDelegateMock verifyWithDelay:1]; + XCTAssertEqualObjects(self.requestManager.featureFlagEtag, etagStub); +} + +- (void)testPerformFeatureFlagRequest_GetRequest_Success_CallbackOnSpecifiedQueue { + XCTestExpectation *delegateCallbackExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"%@.%@.delegateCallbackExpectation", + NSStringFromClass([self class]), NSStringFromSelector(_cmd)]]; + dispatch_queue_t callbackQueue = dispatch_queue_create([[NSString stringWithFormat:@"LDRequestManagerTest.%@", NSStringFromSelector(_cmd)] UTF8String], DISPATCH_QUEUE_SERIAL); + self.requestManager.callbackQueue = callbackQueue; + LDRequestManagerDelegateMock *requestManagerDelegateMock = [[LDRequestManagerDelegateMock alloc] init]; + requestManagerDelegateMock.processedConfigCallback = ^{ + [delegateCallbackExpectation fulfill]; + XCTAssertFalse([NSThread isMainThread]); + }; + self.requestManager.delegate = requestManagerDelegateMock; + + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request hasHost:flagRequestHost method:httpMethodGet ifNoneMatchHeader:nil cachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithData: [self successJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; + }]; + + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; + [self waitForExpectationsWithTimeout:1 handler:^(NSError * _Nullable error) { + XCTAssertEqual(requestManagerDelegateMock.processedConfigCallCount, 1); + XCTAssertEqualObjects(self.requestManager.featureFlagEtag, etagStub); + }]; +} + +- (void)testPerformFeatureFlagRequest_GetRequest_notModified { + XCTestExpectation *getRequestMade = [self expectationWithDescription:@"feature flag GET request made"]; + + id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; + [[requestManagerDelegateMock expect] processedConfig:YES jsonConfigDictionary:nil]; + self.requestManager.delegate = requestManagerDelegateMock; + self.requestManager.featureFlagEtag = etagStub; + + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request hasHost:flagRequestHost method:httpMethodGet ifNoneMatchHeader:etagStub cachePolicy:NSURLRequestUseProtocolCachePolicy]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + [getRequestMade fulfill]; + return [OHHTTPStubsResponse responseWithData:[NSData data] statusCode:(int)kHTTPStatusCodeNotModified headers:[self headerForStatusCode:(int)kHTTPStatusCodeNotModified]]; + }]; + + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; + + [self waitForExpectationsWithTimeout:1 handler:^(NSError * _Nullable error) { [requestManagerDelegateMock verifyWithDelay:1]; + XCTAssertEqualObjects(self.requestManager.featureFlagEtag, etagStub); + }]; +} + +- (void)testPerformFeatureFlagRequest_GetRequest_notModified_missingEtag { + XCTestExpectation *getRequestMade = [self expectationWithDescription:@"feature flag GET request made"]; + + id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; + [[requestManagerDelegateMock expect] processedConfig:YES jsonConfigDictionary:nil]; + self.requestManager.delegate = requestManagerDelegateMock; + self.requestManager.featureFlagEtag = etagStub; + + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request hasHost:flagRequestHost method:httpMethodGet ifNoneMatchHeader:etagStub cachePolicy:NSURLRequestUseProtocolCachePolicy]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + [getRequestMade fulfill]; + return [OHHTTPStubsResponse responseWithData:[NSData data] + statusCode:(int)kHTTPStatusCodeNotModified + headers:[self headerForStatusCode:(int)kHTTPStatusCodeNotModified includeEtag:NO]]; }]; + + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; + + [self waitForExpectations:@[getRequestMade] timeout:1.0]; + [requestManagerDelegateMock verifyWithDelay:1]; + XCTAssertEqualObjects(self.requestManager.featureFlagEtag, etagStub); } - (void)testPerformFeatureFlagRequest_GetRequest_Success_invalidData { @@ -90,24 +213,25 @@ - (void)testPerformFeatureFlagRequest_GetRequest_Success_invalidData { id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; [[requestManagerDelegateMock expect] processedConfig:NO jsonConfigDictionary:[OCMArg any]]; - [LDRequestManager sharedInstance].delegate = requestManagerDelegateMock; + self.requestManager.delegate = requestManagerDelegateMock; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { - return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:httpMethodGet]; + return [request hasHost:flagRequestHost method:httpMethodGet ifNoneMatchHeader:nil cachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { [getRequestMade fulfill]; - return [OHHTTPStubsResponse responseWithData: [self invalidJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; + return [OHHTTPStubsResponse responseWithData:[self invalidJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; }]; - [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; - [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) { + [self waitForExpectationsWithTimeout:1 handler:^(NSError * _Nullable error) { [requestManagerDelegateMock verifyWithDelay:1]; + XCTAssertNil(self.requestManager.featureFlagEtag); }]; } - (void)testPerformFeatureFlagRequest_GetRequest_DoesNotMakeFallbackRequest { - NSMutableArray *selectedStatusCodes = [NSMutableArray arrayWithArray:[LDClient sharedInstance].ldConfig.flagRetryStatusCodes]; + NSMutableArray *selectedStatusCodes = [NSMutableArray arrayWithArray:self.config.flagRetryStatusCodes]; [selectedStatusCodes addObjectsFromArray:[self selectedNoFallbackStatusCodes]]; for (NSNumber *statusCode in selectedStatusCodes) { @@ -118,10 +242,10 @@ - (void)testPerformFeatureFlagRequest_GetRequest_DoesNotMakeFallbackRequest { __block id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; [[requestManagerDelegateMock expect] processedConfig:NO jsonConfigDictionary:[OCMArg isNil]]; - [LDRequestManager sharedInstance].delegate = requestManagerDelegateMock; + self.requestManager.delegate = requestManagerDelegateMock; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { - return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:httpMethodGet]; + return [request hasHost:flagRequestHost method:httpMethodGet ifNoneMatchHeader:nil cachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { [getRequestMade fulfill]; return [OHHTTPStubsResponse responseWithData: [NSData data] statusCode:[statusCode intValue] headers:[self headerForStatusCode:[statusCode intValue]]]; @@ -133,12 +257,13 @@ - (void)testPerformFeatureFlagRequest_GetRequest_DoesNotMakeFallbackRequest { } }]; - [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; - [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) { + [self waitForExpectationsWithTimeout:1 handler:^(NSError * _Nullable error) { [requestManagerDelegateMock verifyWithDelay:1]; //By checking the delegate, we are sure a fallback GET isn't called - - [LDRequestManager sharedInstance].delegate = nil; + XCTAssertNil(self.requestManager.featureFlagEtag); + + self.requestManager.delegate = nil; requestManagerDelegateMock = nil; getRequestMade = nil; @@ -150,23 +275,20 @@ - (void)testPerformFeatureFlagRequest_GetRequest_DoesNotMakeFallbackRequest { } - (void)testPerformFeatureFlagRequest_ReportRequest_Success { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; - config.useReport = YES; - - self.ldClientMock = [self mockClientWithUser:[self mockUser] config:config]; + self.config.useReport = YES; - __block id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; + __block id requestManagerDelegateMock = [OCMockObject mockForProtocol:@protocol(RequestManagerDelegate)]; [[requestManagerDelegateMock expect] processedConfig:YES jsonConfigDictionary:[OCMArg isKindOfClass:[NSDictionary class]]]; - [LDRequestManager sharedInstance].delegate = requestManagerDelegateMock; + self.requestManager.delegate = requestManagerDelegateMock; __weak XCTestExpectation *reportRequestMade = [self expectationWithDescription:@"feature flag REPORT request made"]; __weak XCTestExpectation *responseArrived = [self expectationWithDescription:@"feature flag response arrived"]; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { - return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:kHTTPMethodReport]; + return [request hasHost:flagRequestHost method:kHTTPMethodReport ifNoneMatchHeader:nil cachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { [reportRequestMade fulfill]; - return [OHHTTPStubsResponse responseWithData: [self successJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; + return [OHHTTPStubsResponse responseWithData:[self successJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; }]; [OHHTTPStubs onStubActivation:^(NSURLRequest * _Nonnull request, id _Nonnull stub) { @@ -174,46 +296,119 @@ - (void)testPerformFeatureFlagRequest_ReportRequest_Success { [responseArrived fulfill]; }]; - [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; - - [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) { - [requestManagerDelegateMock verifyWithDelay:1]; + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; + + [self waitForExpectations:@[reportRequestMade, responseArrived] timeout:1]; + [requestManagerDelegateMock verifyWithDelay:1]; + XCTAssertEqualObjects(self.requestManager.featureFlagEtag, etagStub); +} + +- (void)testPerformFeatureFlagRequest_ReportRequest_Success_etagExists { + self.config.useReport = YES; + self.requestManager.featureFlagEtag = etagStub; + + __block id requestManagerDelegateMock = [OCMockObject mockForProtocol:@protocol(RequestManagerDelegate)]; + [[requestManagerDelegateMock expect] processedConfig:YES jsonConfigDictionary:[OCMArg isKindOfClass:[NSDictionary class]]]; + self.requestManager.delegate = requestManagerDelegateMock; + + __weak XCTestExpectation *reportRequestMade = [self expectationWithDescription:@"feature flag REPORT request made"]; + __weak XCTestExpectation *responseArrived = [self expectationWithDescription:@"feature flag response arrived"]; + + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request hasHost:flagRequestHost method:kHTTPMethodReport ifNoneMatchHeader:etagStub cachePolicy:NSURLRequestUseProtocolCachePolicy]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + [reportRequestMade fulfill]; + return [OHHTTPStubsResponse responseWithData:[self successJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; + }]; + + [OHHTTPStubs onStubActivation:^(NSURLRequest * _Nonnull request, id _Nonnull stub) { + XCTAssertTrue([request.HTTPMethod isEqualToString:kHTTPMethodReport]); + [responseArrived fulfill]; }]; + + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; + + [self waitForExpectations:@[reportRequestMade, responseArrived] timeout:1]; + [requestManagerDelegateMock verifyWithDelay:1]; + XCTAssertEqualObjects(self.requestManager.featureFlagEtag, etagStub); } - (void)testPerformFeatureFlagRequest_ReportRequest_Success_invalidData { __weak XCTestExpectation *reportRequestMade = [self expectationWithDescription:@"feature flag REPORT request made"]; - LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; - config.useReport = YES; - - self.ldClientMock = [self mockClientWithUser:[self mockUser] config:config]; + self.config.useReport = YES; - id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; + id requestManagerDelegateMock = [OCMockObject mockForProtocol:@protocol(RequestManagerDelegate)]; [[requestManagerDelegateMock expect] processedConfig:NO jsonConfigDictionary:[OCMArg any]]; - [LDRequestManager sharedInstance].delegate = requestManagerDelegateMock; + self.requestManager.delegate = requestManagerDelegateMock; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { - return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:kHTTPMethodReport]; + return [request hasHost:flagRequestHost method:kHTTPMethodReport ifNoneMatchHeader:nil cachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { [reportRequestMade fulfill]; return [OHHTTPStubsResponse responseWithData: [self invalidJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; }]; - [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; - [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) { - [requestManagerDelegateMock verifyWithDelay:1]; + [self waitForExpectations:@[reportRequestMade] timeout:1]; + [requestManagerDelegateMock verifyWithDelay:1]; + XCTAssertNil(self.requestManager.featureFlagEtag); +} + +- (void)testPerformFeatureFlagRequest_reportRequest_notModified { + XCTestExpectation *reportRequestMade = [self expectationWithDescription:@"feature flag REPORT request made"]; + self.config.useReport = YES; + + id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; + [[requestManagerDelegateMock expect] processedConfig:YES jsonConfigDictionary:nil]; + self.requestManager.delegate = requestManagerDelegateMock; + self.requestManager.featureFlagEtag = etagStub; + + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request hasHost:flagRequestHost method:kHTTPMethodReport ifNoneMatchHeader:etagStub cachePolicy:NSURLRequestUseProtocolCachePolicy]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + [reportRequestMade fulfill]; + return [OHHTTPStubsResponse responseWithData:[NSData data] statusCode:(int)kHTTPStatusCodeNotModified headers:[self headerForStatusCode:(int)kHTTPStatusCodeNotModified]]; + }]; + + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; + + [self waitForExpectations:@[reportRequestMade] timeout:1.0]; + [requestManagerDelegateMock verifyWithDelay:1]; + XCTAssertEqualObjects(self.requestManager.featureFlagEtag, etagStub); +} + +- (void)testPerformFeatureFlagRequest_reportRequest_notModified_missingEtag { + XCTestExpectation *getRequestMade = [self expectationWithDescription:@"feature flag GET request made"]; + self.config.useReport = YES; + + id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; + [[requestManagerDelegateMock expect] processedConfig:YES jsonConfigDictionary:nil]; + self.requestManager.delegate = requestManagerDelegateMock; + self.requestManager.featureFlagEtag = etagStub; + + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request hasHost:flagRequestHost method:kHTTPMethodReport ifNoneMatchHeader:etagStub cachePolicy:NSURLRequestUseProtocolCachePolicy]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + [getRequestMade fulfill]; + return [OHHTTPStubsResponse responseWithData:[NSData data] + statusCode:(int)kHTTPStatusCodeNotModified + headers:[self headerForStatusCode:(int)kHTTPStatusCodeNotModified includeEtag:NO]]; }]; + + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; + + [self waitForExpectations:@[getRequestMade] timeout:1.0]; + [requestManagerDelegateMock verifyWithDelay:1]; + XCTAssertEqualObjects(self.requestManager.featureFlagEtag, etagStub); } - (void)testPerformFeatureFlagRequest_ReportRequest_MakesFallbackGetRequest { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; - config.useReport = YES; - - self.ldClientMock = [self mockClientWithUser:[self mockUser] config:config]; + self.config.useReport = YES; - NSArray *fallbackStatusCodes = [config flagRetryStatusCodes]; + //Because the flagRetryStatusCodes are empty, this test doesn't really run. It's here in case LD ever wants to turn this feature on. + NSArray *fallbackStatusCodes = [self.config flagRetryStatusCodes]; XCTAssertNotNil(fallbackStatusCodes); for (NSNumber *fallbackStatusCode in fallbackStatusCodes) { @@ -225,22 +420,22 @@ - (void)testPerformFeatureFlagRequest_ReportRequest_MakesFallbackGetRequest { __block XCTestExpectation *errorResponseArrived = [self expectationWithDescription:@"feature flag error response arrived"]; __block XCTestExpectation *flagResponseArrived = [self expectationWithDescription:@"feature flag response arrived"]; - __block id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; - [[requestManagerDelegateMock expect] processedConfig:NO jsonConfigDictionary:[OCMArg isNil]]; - [LDRequestManager sharedInstance].delegate = requestManagerDelegateMock; + self.requestManagerDelegateMock = [OCMockObject mockForProtocol:@protocol(RequestManagerDelegate)]; + [[self.requestManagerDelegateMock expect] processedConfig:YES jsonConfigDictionary:[OCMArg isKindOfClass:[NSDictionary class]]]; + self.requestManager.delegate = self.requestManagerDelegateMock; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { - return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:kHTTPMethodReport]; + return [request hasHost:flagRequestHost method:kHTTPMethodReport ifNoneMatchHeader:nil cachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { [reportRequestMade fulfill]; return [OHHTTPStubsResponse responseWithData: [NSData data] statusCode:[fallbackStatusCode intValue] headers:[self headerForStatusCode:[fallbackStatusCode intValue]]]; }].name = reportStubName; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { - return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:httpMethodGet]; + return [request hasHost:flagRequestHost method:httpMethodGet ifNoneMatchHeader:nil cachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { [getRequestMade fulfill]; - return [OHHTTPStubsResponse responseWithData: [self emptyJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; + return [OHHTTPStubsResponse responseWithData:[self successJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; }].name = getStubName; [OHHTTPStubs onStubActivation:^(NSURLRequest * _Nonnull request, id _Nonnull stub) { @@ -252,13 +447,14 @@ - (void)testPerformFeatureFlagRequest_ReportRequest_MakesFallbackGetRequest { } }]; - [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; - [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) { - [requestManagerDelegateMock verifyWithDelay:1]; - - [LDRequestManager sharedInstance].delegate = nil; - requestManagerDelegateMock = nil; + [self waitForExpectationsWithTimeout:1 handler:^(NSError * _Nullable error) { + [self.requestManagerDelegateMock verifyWithDelay:1]; + XCTAssertEqualObjects(self.requestManager.featureFlagEtag, etagStub); + + self.requestManager.delegate = nil; + self.requestManagerDelegateMock = nil; reportRequestMade = nil; getRequestMade = nil; @@ -274,30 +470,27 @@ - (void)testPerformFeatureFlagRequest_ReportRequest_DoesNotMakeFallbackGetReques NSArray *noFallbackStatusCodes = [self selectedNoFallbackStatusCodes]; for (NSNumber *fallbackStatusCode in noFallbackStatusCodes) { - LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; - config.useReport = YES; + self.config.useReport = YES; - self.ldClientMock = [self mockClientWithUser:[self mockUser] config:config]; - NSString *reportStubName = @"report stub"; NSString *getStubName = @"get stub"; __block XCTestExpectation *reportRequestMade = [self expectationWithDescription:@"feature flag REPORT request made"]; __block XCTestExpectation *errorResponseArrived = [self expectationWithDescription:@"feature flag error response arrived"]; - __block id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; + __block id requestManagerDelegateMock = [OCMockObject mockForProtocol:@protocol(RequestManagerDelegate)]; [[requestManagerDelegateMock expect] processedConfig:NO jsonConfigDictionary:[OCMArg isNil]]; - [LDRequestManager sharedInstance].delegate = requestManagerDelegateMock; + self.requestManager.delegate = requestManagerDelegateMock; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { - return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:kHTTPMethodReport]; + return [request hasHost:flagRequestHost method:kHTTPMethodReport ifNoneMatchHeader:nil cachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { [reportRequestMade fulfill]; return [OHHTTPStubsResponse responseWithData: [NSData data] statusCode:[fallbackStatusCode intValue] headers:[self headerForStatusCode:[fallbackStatusCode intValue]]]; }].name = reportStubName; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { - return [kBaseUrl containsString:request.URL.host] && [request.HTTPMethod isEqualToString:httpMethodGet]; + return [request hasHost:flagRequestHost method:httpMethodGet ifNoneMatchHeader:nil cachePolicy:NSURLRequestReloadIgnoringLocalCacheData]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { XCTFail(@"Request Manager made GET flag request in response to a non-fallback status code"); return [OHHTTPStubsResponse responseWithData: [self emptyJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; @@ -309,12 +502,13 @@ - (void)testPerformFeatureFlagRequest_ReportRequest_DoesNotMakeFallbackGetReques } }]; - [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; - [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) { + [self waitForExpectationsWithTimeout:1 handler:^(NSError * _Nullable error) { [requestManagerDelegateMock verifyWithDelay:1]; + XCTAssertNil(self.requestManager.featureFlagEtag); - [LDRequestManager sharedInstance].delegate = nil; + self.requestManager.delegate = nil; requestManagerDelegateMock = nil; reportRequestMade = nil; @@ -325,38 +519,57 @@ - (void)testPerformFeatureFlagRequest_ReportRequest_DoesNotMakeFallbackGetReques } } -- (void)testPerformFeatureFlagRequestWithoutUser { - self.ldClientMock = [self mockClientWithUser:nil config:[self testConfig]]; - +- (void)testPerformFeatureFlagRequest_offline { + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.host isEqualToString:flagRequestHost]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + XCTFail(@"Request Manager made feature flag request while offline"); + return [OHHTTPStubsResponse responseWithData: [NSData data] statusCode:200 headers:[self headerForStatusCode:200]]; + }]; + + [self.requestManager performFeatureFlagRequest:self.user isOnline:NO]; +} + +- (void)testPerformFeatureFlagRequest_WithoutMobileKey { + NSString *nilMobileKey; + self.requestManager = [LDRequestManager requestManagerForMobileKey:nilMobileKey config:self.config delegate:nil callbackQueue:nil]; [self mockFlagResponse]; - [OHHTTPStubs onStubActivation:^(NSURLRequest * _Nonnull request, id _Nonnull stub) { if ([kBaseUrl containsString:request.URL.host]) { XCTFail(@"performFeatureFlagRequest should not make a flag request without a user"); } }]; - - [[LDRequestManager sharedInstance] performFeatureFlagRequest:nil]; -} -- (void)testPerformFeatureFlagRequestOffline { - self.clientManagerMock = OCMClassMock([LDClientManager class]); - OCMStub(ClassMethod([self.clientManagerMock sharedInstance])).andReturn(self.clientManagerMock); - OCMStub([self.clientManagerMock isOnline]).andReturn(NO); + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; +} +- (void)testPerformFeatureFlagRequestWithoutUser { [self mockFlagResponse]; - + [OHHTTPStubs onStubActivation:^(NSURLRequest * _Nonnull request, id _Nonnull stub) { if ([kBaseUrl containsString:request.URL.host]) { - XCTFail(@"performFeatureFlagRequest should not make a flag request while offline"); + XCTFail(@"performFeatureFlagRequest should not make a flag request without a user"); } }]; - [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; + LDUserModel *nilUser; + [self.requestManager performFeatureFlagRequest:nilUser isOnline:YES]; } - (void)testFlagRequestPostsClientUnauthorizedNotificationOnUnauthorizedResponse { - XCTestExpectation *clientUnauthorizedExpection = [self expectationForNotification:kLDClientUnauthorizedNotification object:nil handler:nil]; + XCTestExpectation *notificationExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"%@.%@.notificationExpectation", + NSStringFromClass([self class]), NSStringFromSelector(_cmd)]]; + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDClientUnauthorizedNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDClientUnauthorizedNotification object:nil userInfo:[OCMArg checkWithBlock:^BOOL(id obj) { + if (![obj isKindOfClass:[NSDictionary class]]) { return NO; } + NSDictionary *userInfo = obj; + [notificationExpectation fulfill]; + return [userInfo[kLDNotificationUserInfoKeyMobileKey] isEqualToString:testMobileKey]; + }]]; + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; [OHHTTPStubs removeAllStubs]; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { @@ -366,29 +579,36 @@ - (void)testFlagRequestPostsClientUnauthorizedNotificationOnUnauthorizedResponse }]; XCTAssertTrue([OHHTTPStubs allStubs].count == 1); - [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; + [self waitForExpectations:@[notificationExpectation] timeout:1.0]; - [self waitForExpectations:@[clientUnauthorizedExpection] timeout:10.0]; + [notificationObserver verify]; } - (void)testFlagRequestDoesNotPostClientUnauthorizedNotificationOnErrorResponse { id clientUnauthorizedObserver = OCMObserverMock(); [[NSNotificationCenter defaultCenter] addMockObserver:clientUnauthorizedObserver name:kLDClientUnauthorizedNotification object:nil]; //it's not obvious, but by not setting expect on the mock observer, the observer will fail when verify is called IF it has received the notification + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:clientUnauthorizedObserver]; + }; + XCTestExpectation *responseExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"%@.%@.responseExpectation", + NSStringFromClass([self class]), NSStringFromSelector(_cmd)]]; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { return [kBaseUrl containsString:request.URL.host]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + [responseExpectation fulfill]; return [OHHTTPStubsResponse responseWithData:[NSData data] statusCode:httpStatusCodeInternalServerError headers:[self headerForStatusCode:httpStatusCodeInternalServerError]]; }]; - [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; + [self.requestManager performFeatureFlagRequest:self.user isOnline:YES]; + [self waitForExpectations:@[responseExpectation] timeout:1.0]; [clientUnauthorizedObserver verify]; - [[NSNotificationCenter defaultCenter] removeObserver:clientUnauthorizedObserver]; } -- (void)testPerformEventRequest_Online { +- (void)testPerformEventRequest { XCTestExpectation* responseArrivedExpectation = [self expectationWithDescription:@"response of async request has arrived"]; NSData *data = [[NSData alloc] initWithBase64EncodedString:@"" options: 0] ; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { @@ -397,7 +617,7 @@ - (void)testPerformEventRequest_Online { return [OHHTTPStubsResponse responseWithData:data statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; }]; self.requestManagerDelegateMock = OCMProtocolMock(@protocol(RequestManagerDelegate)); - [LDRequestManager sharedInstance].delegate = self.requestManagerDelegateMock; + self.requestManager.delegate = self.requestManagerDelegateMock; NSDate *targetHeaderDate = [NSDateFormatter eventDateHeaderStub]; [[self.requestManagerDelegateMock expect] processedEvents:YES jsonEventArray:[OCMArg isKindOfClass:[NSArray class]] responseDate:[OCMArg checkWithBlock:^BOOL(id obj) { XCTAssertEqualObjects(obj, targetHeaderDate); @@ -405,36 +625,53 @@ - (void)testPerformEventRequest_Online { return YES; }]]; - [[LDRequestManager sharedInstance] performEventRequest:[self stubEvents]]; + [self.requestManager performEventRequest:[self stubEvents] isOnline:YES]; [self waitForExpectations:@[responseArrivedExpectation] timeout:1.0]; [self.requestManagerDelegateMock verify]; } -- (void)testPerformEventRequest_Offline { - [self.clientManagerMock stopMocking]; - id clientManagerMock = OCMClassMock([LDClientManager class]); - OCMStub(ClassMethod([clientManagerMock sharedInstance])).andReturn(clientManagerMock); - OCMStub([clientManagerMock isOnline]).andReturn(NO); - self.clientManagerMock = clientManagerMock; - - NSData *data = [[NSData alloc] initWithBase64EncodedString:@"" options: 0]; +- (void)testPerformEventRequest_CallbackOnSpecifiedQueue { + XCTestExpectation* responseArrivedExpectation = [self expectationWithDescription:@"response of async request has arrived"]; + NSData *data = [[NSData alloc] initWithBase64EncodedString:@"" options: 0] ; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { - return [kEventsUrl containsString:request.URL.host]; + return [request.URL.host isEqualToString:@"mobile.launchdarkly.com"] && [[request valueForHTTPHeaderField:kEventHeaderLaunchDarklyEventSchema] isEqualToString:kEventSchema]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { - return [OHHTTPStubsResponse responseWithData: data statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; - }]; - [OHHTTPStubs onStubActivation:^(NSURLRequest * _Nonnull request, id _Nonnull stub) { - if ([kEventsUrl containsString:request.URL.host]) { - XCTFail(@"performEventRequest should not make a flag request while offline"); - } + return [OHHTTPStubsResponse responseWithData:data statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; }]; + LDRequestManagerDelegateMock *requestManagerDelegateMock = [[LDRequestManagerDelegateMock alloc] init]; + self.requestManager.delegate = requestManagerDelegateMock; + dispatch_queue_t callbackQueue = dispatch_queue_create([[NSString stringWithFormat:@"LDRequestManagerTest.%@", NSStringFromSelector(_cmd)] UTF8String], DISPATCH_QUEUE_SERIAL); + self.requestManager.callbackQueue = callbackQueue; + NSDate *targetHeaderDate = [NSDateFormatter eventDateHeaderStub]; + __weak LDRequestManagerDelegateMock *weakDelegateMock = requestManagerDelegateMock; + requestManagerDelegateMock.processedEventsCallback = ^{ + __strong LDRequestManagerDelegateMock *strongDelegateMock = weakDelegateMock; + XCTAssertEqualObjects(strongDelegateMock.processedEventsResponseDate, targetHeaderDate); + [responseArrivedExpectation fulfill]; + XCTAssertFalse([NSThread isMainThread]); + }; + + [self.requestManager performEventRequest:[self stubEvents] isOnline:YES]; - [[LDRequestManager sharedInstance] performEventRequest:[self stubEvents]]; + [self waitForExpectations:@[responseArrivedExpectation] timeout:1.0]; + XCTAssertEqual(requestManagerDelegateMock.processedEventsCallCount, 1); } - (void)testEventRequestPostsClientUnauthorizedNotificationOnUnauthorizedResponse { - XCTestExpectation *clientUnauthorizedExpection = [self expectationForNotification:kLDClientUnauthorizedNotification object:nil handler:nil]; + XCTestExpectation *notificationExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"%@.%@.notificationExpectation", + NSStringFromClass([self class]), NSStringFromSelector(_cmd)]]; + id notificationObserver = [OCMockObject observerMock]; + [[NSNotificationCenter defaultCenter] addMockObserver:notificationObserver name:kLDClientUnauthorizedNotification object:nil]; + [[notificationObserver expect] notificationWithName:kLDClientUnauthorizedNotification object:nil userInfo:[OCMArg checkWithBlock:^BOOL(id obj) { + if (![obj isKindOfClass:[NSDictionary class]]) { return NO; } + NSDictionary *userInfo = obj; + [notificationExpectation fulfill]; + return [userInfo[kLDNotificationUserInfoKeyMobileKey] isEqualToString:testMobileKey]; + }]]; + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:notificationObserver]; + }; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { return [kEventsUrl containsString:request.URL.host]; @@ -442,46 +679,48 @@ - (void)testEventRequestPostsClientUnauthorizedNotificationOnUnauthorizedRespons return [OHHTTPStubsResponse responseWithData:[NSData data] statusCode:httpStatusCodeUnauthorized headers:[self headerForStatusCode:httpStatusCodeUnauthorized]]; }]; - [[LDRequestManager sharedInstance] performEventRequest:[self stubEvents]]; + [self.requestManager performEventRequest:[self stubEventsWithTag:NSStringFromSelector(_cmd)] isOnline:YES]; - [self waitForExpectations:@[clientUnauthorizedExpection] timeout:10.0]; + [self waitForExpectations:@[notificationExpectation] timeout:1.0]; + [notificationObserver verify]; } - (void)testEventRequestDoesNotPostClientUnauthorizedNotificationOnErrorResponse { - id clientUnauthorizedObserver = OCMObserverMock(); + id clientUnauthorizedObserver = [OCMockObject observerMock]; [[NSNotificationCenter defaultCenter] addMockObserver:clientUnauthorizedObserver name:kLDClientUnauthorizedNotification object:nil]; //it's not obvious, but by not setting expect on the mock observer, the observer will fail when verify is called IF it has received the notification + self.cleanup = ^{ + [[NSNotificationCenter defaultCenter] removeObserver:clientUnauthorizedObserver]; + }; + XCTestExpectation *responseExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"%@.%@.responseExpectation", + NSStringFromClass([self class]), NSStringFromSelector(_cmd)]]; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { return [kEventsUrl containsString:request.URL.host]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + [responseExpectation fulfill]; return [OHHTTPStubsResponse responseWithData:[NSData data] statusCode:httpStatusCodeInternalServerError headers:[self headerForStatusCode:httpStatusCodeInternalServerError]]; }]; - [[LDRequestManager sharedInstance] performEventRequest:[self stubEvents]]; + [self.requestManager performEventRequest:[self stubEventsWithTag:NSStringFromSelector(_cmd)] isOnline:YES]; + [self waitForExpectations:@[responseExpectation] timeout:1.0]; [clientUnauthorizedObserver verify]; - [[NSNotificationCenter defaultCenter] removeObserver:clientUnauthorizedObserver]; } -#pragma mark - Helpers +- (void)testPerformEventRequest_offline { + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.host isEqualToString:eventRequestHost]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + XCTFail(@"Request Manager made event request while offline"); + return [OHHTTPStubsResponse responseWithData: [NSData data] statusCode:200 headers:[self headerForStatusCode:200]]; + }]; -- (id)mockClientWithUser:(LDUserModel*)user config:(LDConfig*)config { - id mockClient = OCMClassMock([LDClient class]); - OCMStub(ClassMethod([mockClient sharedInstance])).andReturn(mockClient); - OCMStub([mockClient ldUser]).andReturn(user); - XCTAssertEqual([LDClient sharedInstance].ldUser, user); - - if (!config) { - config = [self testConfig]; - } - - OCMStub([mockClient ldConfig]).andReturn(config); - XCTAssertEqual([LDClient sharedInstance].ldConfig, config); - - return mockClient; + [self.requestManager performEventRequest:[self stubEvents] isOnline:NO]; } +#pragma mark - Helpers + - (void)mockFlagResponse { [OHHTTPStubs removeAllStubs]; @@ -509,8 +748,16 @@ - (LDConfig*)testConfig { } - (NSDictionary*)headerForStatusCode:(int)statusCode { - if (statusCode == httpStatusCodeOk) { - return @{@"Content-Type":@"application/json", kHeaderKeyDate:kDateHeaderValueDate}; + return [self headerForStatusCode:statusCode includeEtag:YES]; +} + +- (NSDictionary*)headerForStatusCode:(int)statusCode includeEtag:(BOOL)includeEtag { + if (statusCode == httpStatusCodeOk || statusCode == kHTTPStatusCodeNotModified) { + NSMutableDictionary *headers = [NSMutableDictionary dictionaryWithDictionary:@{@"Content-Type":@"application/json", kHeaderKeyDate:kDateHeaderValueDate}]; + if (includeEtag) { + headers[kFlagResponseHeaderEtag] = etagStub; + } + return [headers copy]; } return @{@"Content-Type":@"text", kHeaderKeyDate:kDateHeaderValueDate}; } @@ -528,11 +775,18 @@ - (NSData*)invalidJsonData { } - (NSArray*)selectedNoFallbackStatusCodes { - return @[@304, @307, @401, @404, @412, @500]; + return @[@307, @401, @404, @412, @500]; } - (NSArray*)stubEvents { + return [self stubEventsWithTag:nil]; +} + +- (NSArray*)stubEventsWithTag:(NSString*)tag { NSString *jsonEventString = @"[{\"kind\": \"feature\", \"user\": {\"key\" : \"jeff@test.com\", \"custom\" : {\"groups\" : [\"microsoft\", \"google\"]}}, \"creationDate\": 1438468068, \"key\": \"isConnected\", \"value\": true, \"default\": false}]"; + if (tag.length > 0) { + jsonEventString = [NSString stringWithFormat:@"[{\"kind\": \"feature\", \"user\": {\"key\" : \"jeff@test.com\", \"custom\" : {\"groups\" : [\"microsoft\", \"google\"]}}, \"creationDate\": 1438468068, \"key\": \"isConnected\", \"value\": true, \"default\": false, \"tag\": \"%@\"}]", tag]; + } NSData* eventData = [jsonEventString dataUsingEncoding:NSUTF8StringEncoding]; return [NSJSONSerialization JSONObjectWithData:eventData options:kNilOptions error:nil]; } diff --git a/DarklyTests/LDURLCacheTest.m b/DarklyTests/LDURLCacheTest.m new file mode 100644 index 00000000..2e12ee15 --- /dev/null +++ b/DarklyTests/LDURLCacheTest.m @@ -0,0 +1,287 @@ +// +// LDURLCacheTest.m +// DarklyTests +// +// Created by Mark Pokorny on 11/16/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "DarklyXCTestCase.h" +#import "LDURLCache.h" +#import "OCMock.h" +#import "LDUserModel+Testable.h" +#import "LDRequestManager.h" + +static NSString *const testMobileKey = @"testMobileKey"; + +@interface NSURLRequest (LDURLCacheTest) + +@end + +@implementation NSURLRequest (LDURLCacheTest) +-(BOOL)hasPropertiesMatchingRequest:(NSURLRequest*)otherRequest { + return [self.URL.scheme isEqualToString:otherRequest.URL.scheme] + && [self.URL.host isEqualToString:otherRequest.URL.host] + && [self.URL.path isEqualToString:otherRequest.URL.path] + && [self.HTTPMethod isEqualToString:otherRequest.HTTPMethod] + && ((self.HTTPBody != nil && [self.HTTPBody isEqualToData:otherRequest.HTTPBody]) || (self.HTTPBody == nil && otherRequest.HTTPBody == nil)) + && self.timeoutInterval == otherRequest.timeoutInterval + && self.cachePolicy == otherRequest.cachePolicy + && [self.allHTTPHeaderFields isEqualToDictionary:otherRequest.allHTTPHeaderFields]; +} +@end + +@interface LDRequestManager (LDURLCacheTest) +-(NSURLRequest*)flagRequestUsingReportMethodForUser:(LDUserModel*)user; +-(NSURLRequest*)flagRequestUsingGetMethodForUser:(LDUserModel*)user; +@end + +@interface LDURLCache (LDURLCacheTest) +@property (nonatomic, strong) NSURLCache *baseUrlCache; +@end + +@implementation LDURLCache (LDURLCacheTest) +@dynamic baseUrlCache; +@end + +@interface LDURLCacheTest : DarklyXCTestCase +@property (strong, nonatomic) id nsUrlCacheMock; +@property (nonatomic, strong) LDConfig *config; +@property (nonatomic, strong) LDUserModel *user; +@property (nonatomic, strong) LDRequestManager *requestManager; +@property (nonatomic, strong) NSCachedURLResponse *cachedResponseStub; +@property (strong, nonatomic) LDURLCache *urlCache; +@end + +@implementation LDURLCacheTest + +-(void)setUp { + [super setUp]; + + self.config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; + self.config.streaming = NO; + self.config.useReport = YES; + self.user = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + self.requestManager = [LDRequestManager requestManagerForMobileKey:testMobileKey config:self.config delegate:nil callbackQueue:nil]; + self.cachedResponseStub = [[NSCachedURLResponse alloc] init]; + + self.nsUrlCacheMock = [OCMockObject niceMockForClass:[NSURLCache class]]; + (void)[[[[self.nsUrlCacheMock stub] andReturn:self.nsUrlCacheMock] ignoringNonObjectArgs] initWithMemoryCapacity:0 diskCapacity:0 diskPath:[OCMArg any]]; + + self.urlCache = (LDURLCache*)[LDURLCache urlCacheForConfig:self.config usingCache:self.nsUrlCacheMock]; +} + +-(void)tearDown { + [super tearDown]; +} + +-(void)testUrlCacheForConfigUsingCache_streaming_get { + self.config.streaming = YES; + self.config.useReport = NO; + + id urlCache = [LDURLCache urlCacheForConfig:self.config usingCache:self.nsUrlCacheMock]; + + XCTAssertEqualObjects(urlCache, self.nsUrlCacheMock); +} + +-(void)testUrlCacheForConfigUsingCache_streaming_report { + self.config.streaming = YES; + self.config.useReport = YES; + + id urlCache = [LDURLCache urlCacheForConfig:self.config usingCache:self.nsUrlCacheMock]; + + XCTAssertEqualObjects(urlCache, self.nsUrlCacheMock); +} + +-(void)testUrlCacheForConfigUsingCache_polling_get { + self.config.streaming = NO; + self.config.useReport = NO; + + id urlCache = [LDURLCache urlCacheForConfig:self.config usingCache:self.nsUrlCacheMock]; + + XCTAssertEqualObjects(urlCache, self.nsUrlCacheMock); +} + +-(void)testUrlCacheForConfigUsingCache_polling_report { + self.config.streaming = NO; + self.config.useReport = YES; + + id urlCache = [LDURLCache urlCacheForConfig:self.config usingCache:self.nsUrlCacheMock]; + + XCTAssertNotEqualObjects(urlCache, self.nsUrlCacheMock); + XCTAssertEqual([urlCache class], [LDURLCache class]); +} + +-(void)testUrlCacheForConfigUsingCache_missingBaseCache { + self.config.streaming = NO; + self.config.useReport = YES; + NSURLCache *missingCache; + + id urlCache = [LDURLCache urlCacheForConfig:self.config usingCache:missingCache]; + + XCTAssertNil(urlCache); +} + +-(void)testShouldUseLDURLCacheForConfig_streaming_get { + self.config.streaming = YES; + self.config.useReport = NO; + + XCTAssertFalse([LDURLCache shouldUseLDURLCacheForConfig:self.config]); +} + +-(void)testShouldUseLDURLCacheForConfig_streaming_report { + self.config.streaming = YES; + self.config.useReport = YES; + + XCTAssertFalse([LDURLCache shouldUseLDURLCacheForConfig:self.config]); +} + +-(void)testShouldUseLDURLCacheForConfig_polling_get { + self.config.streaming = NO; + self.config.useReport = NO; + + XCTAssertFalse([LDURLCache shouldUseLDURLCacheForConfig:self.config]); +} + +-(void)testShouldUseLDURLCacheForConfig_polling_report { + self.config.streaming = NO; + self.config.useReport = YES; + + XCTAssertTrue([LDURLCache shouldUseLDURLCacheForConfig:self.config]); +} + +-(void)testStoreCachedResponseForDataTask { + NSURLRequest *reportRequest = [self.requestManager flagRequestUsingReportMethodForUser:self.user]; + NSURLSessionDataTask *reportDataTask = [[NSURLSession sharedSession] dataTaskWithRequest:reportRequest + completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + //Empty because its required, but this should never be executed + }]; + NSURLRequest *getRequest = [self.requestManager flagRequestUsingGetMethodForUser:self.user]; + [[self.nsUrlCacheMock expect] storeCachedResponse:self.cachedResponseStub forRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + if(![obj isKindOfClass:[NSURLRequest class]]) { + return NO; + } + NSURLRequest *request = obj; + return [request hasPropertiesMatchingRequest:getRequest]; + }]]; + + [self.urlCache storeCachedResponse:self.cachedResponseStub forDataTask:reportDataTask]; + + [self.nsUrlCacheMock verify]; +} + +-(void)testStoreCachedResponseForDataTask_getDataTask { + NSURLRequest *getRequest = [self.requestManager flagRequestUsingGetMethodForUser:self.user]; + NSURLSessionDataTask *getDataTask = [[NSURLSession sharedSession] dataTaskWithRequest:getRequest + completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + //Empty because its required, but this should never be executed + }]; + [[self.nsUrlCacheMock expect] storeCachedResponse:self.cachedResponseStub forDataTask:getDataTask]; + + [self.urlCache storeCachedResponse:self.cachedResponseStub forDataTask:getDataTask]; + + [self.nsUrlCacheMock verify]; +} + +-(void)testStoreCachedResponseForRequest { + NSURLRequest *reportRequest = [self.requestManager flagRequestUsingReportMethodForUser:self.user]; + NSURLRequest *getRequest = [self.requestManager flagRequestUsingGetMethodForUser:self.user]; + [[self.nsUrlCacheMock expect] storeCachedResponse:self.cachedResponseStub forRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + if(![obj isKindOfClass:[NSURLRequest class]]) { + return NO; + } + NSURLRequest *request = obj; + return [request hasPropertiesMatchingRequest:getRequest]; + }]]; + + [self.urlCache storeCachedResponse:self.cachedResponseStub forRequest:reportRequest]; + + [self.nsUrlCacheMock verify]; +} + +-(void)testStoreCachedResponseForRequest_getRequest { + NSURLRequest *getRequest = [self.requestManager flagRequestUsingGetMethodForUser:self.user]; + [[self.nsUrlCacheMock expect] storeCachedResponse:self.cachedResponseStub forRequest:getRequest]; //pass the original request through, it wasn't a REPORT + + [self.urlCache storeCachedResponse:self.cachedResponseStub forRequest:getRequest]; + + [self.nsUrlCacheMock verify]; +} + +-(void)testCachedResponseForRequest { + NSURLRequest *reportRequest = [self.requestManager flagRequestUsingReportMethodForUser:self.user]; + NSURLRequest *getRequest = [self.requestManager flagRequestUsingGetMethodForUser:self.user]; + [[[self.nsUrlCacheMock expect] andReturn:self.cachedResponseStub] cachedResponseForRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + if(![obj isKindOfClass:[NSURLRequest class]]) { + return NO; + } + NSURLRequest *request = obj; + return [request hasPropertiesMatchingRequest:getRequest]; + }]]; + + NSCachedURLResponse *cachedResponse = [self.urlCache cachedResponseForRequest:reportRequest]; + + XCTAssertEqualObjects(cachedResponse, self.cachedResponseStub); + [self.nsUrlCacheMock verify]; +} + +-(void)testCachedResponseForRequest_getRequest { + NSURLRequest *getRequest = [self.requestManager flagRequestUsingGetMethodForUser:self.user]; + [[[self.nsUrlCacheMock expect] andReturn:self.cachedResponseStub] cachedResponseForRequest:getRequest]; + + NSCachedURLResponse *cachedResponse = [self.urlCache cachedResponseForRequest:getRequest]; + + XCTAssertEqualObjects(cachedResponse, self.cachedResponseStub); + [self.nsUrlCacheMock verify]; +} + +-(void)testGetCachedResponseForDataTask { + NSURLRequest *reportRequest = [self.requestManager flagRequestUsingReportMethodForUser:self.user]; + NSURLSessionDataTask *reportDataTask = [[NSURLSession sharedSession] dataTaskWithRequest:reportRequest + completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + //Empty because its required, but this should never be executed + }]; + NSURLRequest *getRequest = [self.requestManager flagRequestUsingGetMethodForUser:self.user]; + [[[self.nsUrlCacheMock expect] andReturn:self.cachedResponseStub] cachedResponseForRequest:[OCMArg checkWithBlock:^BOOL(id obj) { + if(![obj isKindOfClass:[NSURLRequest class]]) { + return NO; + } + NSURLRequest *request = obj; + return [request hasPropertiesMatchingRequest:getRequest]; + }]]; + XCTestExpectation *responseExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"%@.%@.responseExpectation", + NSStringFromClass([self class]), NSStringFromSelector(_cmd)]]; + __block NSCachedURLResponse *reportedResponse; + + [self.urlCache getCachedResponseForDataTask:reportDataTask completionHandler:^(NSCachedURLResponse * _Nonnull cachedResponse) { + reportedResponse = cachedResponse; + [responseExpectation fulfill]; + }]; + + [self waitForExpectations:@[responseExpectation] timeout:1.0]; + XCTAssertEqualObjects(reportedResponse, self.cachedResponseStub); + [self.nsUrlCacheMock verify]; +} + +-(void)testGetCachedResponseForDataTask_getDataTask { + NSURLRequest *getRequest = [self.requestManager flagRequestUsingGetMethodForUser:self.user]; + NSURLSessionDataTask *getDataTask = [[NSURLSession sharedSession] dataTaskWithRequest:getRequest + completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + //Empty because its required, but this should never be executed + }]; + [[self.nsUrlCacheMock expect] getCachedResponseForDataTask:getDataTask completionHandler:[OCMArg invokeBlockWithArgs:self.cachedResponseStub, nil]]; + XCTestExpectation *responseExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"%@.%@.responseExpectation", + NSStringFromClass([self class]), NSStringFromSelector(_cmd)]]; + __block NSCachedURLResponse *reportedResponse; + + [self.urlCache getCachedResponseForDataTask:getDataTask completionHandler:^(NSCachedURLResponse * _Nonnull cachedResponse) { + reportedResponse = cachedResponse; + [responseExpectation fulfill]; + }]; + + [self waitForExpectations:@[responseExpectation] timeout:1.0]; + XCTAssertEqualObjects(reportedResponse, self.cachedResponseStub); + [self.nsUrlCacheMock verify]; +} + +@end diff --git a/DarklyTests/LDUserBuilderTest.m b/DarklyTests/LDUserBuilderTest.m index 3d83b7b2..75db6eb9 100644 --- a/DarklyTests/LDUserBuilderTest.m +++ b/DarklyTests/LDUserBuilderTest.m @@ -2,12 +2,14 @@ // Copyright © 2015 Catamorphic Co. All rights reserved. // -#import +#import "DarklyXCTestCase.h" #import "LDUserBuilder.h" #import "LDUserModel.h" -#import "LDDataManager.h" -#import "DarklyXCTestCase.h" -#import "OCMock.h" +#import "LDUserModel+Testable.h" + +@interface LDUserBuilder (LDUserBuilderTest) ++(NSString*)uniqueKey; +@end @interface LDUserBuilderTest : DarklyXCTestCase @@ -15,83 +17,61 @@ @interface LDUserBuilderTest : DarklyXCTestCase @implementation LDUserBuilderTest -- (void)setUp { - [super setUp]; - // Put setup code here. This method is called before the invocation of each test method in the class. -} +-(void)testBuild { + LDUserModel *originalUser = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + LDUserBuilder *userBuilder = [LDUserBuilder currentBuilder:originalUser]; -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. - [super tearDown]; -} + LDUserModel *user = [userBuilder build]; -- (void)testUserDefaultValues { - LDUserBuilder *builder = [[LDUserBuilder alloc] init]; - LDUserModel *user = [builder build]; - XCTAssertNotNil([user key]); - XCTAssertTrue([user anonymous]); - XCTAssertNotNil([user device]); - XCTAssertNotNil([user os]); - XCTAssertNil([user privateAttributes]); + NSArray *ignoredAttributes = @[kUserAttributeConfig, kUserAttributeUpdatedAt]; + XCTAssertTrue([user isEqual:originalUser ignoringAttributes:ignoredAttributes]); } -- (void)testUserWithInputValues { - NSString *testKey = @"testKey"; - NSString *testIp = @"testIp"; - NSString *testCountry = @"testCountry"; - NSString *testName = @"testName"; - NSString *testFirstName = @"testFirstName"; - NSString *testLastName = @"testLastName"; - NSString *testEmail = @"testEmail"; - NSString *testAvatar = @"testAvatar"; - NSString *testCustomKey = @"testCustomKey"; - NSString *testCustomValue = @"testCustomValue"; - Boolean testAnonymous = NO; - LDUserBuilder *builder = [[LDUserBuilder alloc] init]; - builder.key = testKey; - builder.ip = testIp; - builder.country = testCountry; - builder.name = testName; - builder.firstName = testFirstName; - builder.lastName = testLastName; - builder.email = testEmail; - builder.avatar = testAvatar; - builder.customDictionary[testCustomKey] = testCustomValue; - builder.isAnonymous = testAnonymous; +-(void)testBuild_emptyProperties { + LDUserModel *targetUser = [[LDUserModel alloc] init]; + targetUser.key = [LDUserBuilder uniqueKey]; + targetUser.anonymous = YES; + LDUserBuilder *userBuilder = [[LDUserBuilder alloc] init]; - LDUserModel *user = [builder build]; - XCTAssertEqualObjects([user key], testKey); - XCTAssertEqualObjects([user ip], testIp); - XCTAssertEqualObjects([user country], testCountry); - XCTAssertEqualObjects([user name], testName); - XCTAssertEqualObjects([user firstName], testFirstName); - XCTAssertEqualObjects([user lastName], testLastName); - XCTAssertEqualObjects([user email], testEmail); - XCTAssertEqualObjects([user avatar], testAvatar); - XCTAssertEqualObjects([[user custom] objectForKey:testCustomKey], testCustomValue); - XCTAssertFalse([user anonymous]); - XCTAssertNotNil([user device]); - XCTAssertNotNil([user os]); + LDUserModel *user = [userBuilder build]; + + XCTAssertTrue([user isEqual:targetUser ignoringAttributes:@[kUserAttributeUpdatedAt]]); } -- (void)testUserSetAnonymous { - Boolean testAnonymous = YES; +- (void)testBuild_setAnonymous { LDUserBuilder *builder = [[LDUserBuilder alloc] init]; - builder.isAnonymous = testAnonymous; + builder.key = [[NSUUID UUID] UUIDString]; + builder.isAnonymous = YES; + LDUserModel *user = [builder build]; + + XCTAssertEqualObjects(user.key, builder.key); XCTAssertTrue(user.anonymous); } -- (void)testSetPrivateAttributes { +- (void)testBuild_privateAttributes { LDUserBuilder *builder = [[LDUserBuilder alloc] init]; for (NSString *attribute in [LDUserModel allUserAttributes]) { builder.privateAttributes = @[attribute]; + LDUserModel *user = [builder build]; + XCTAssertEqualObjects(user.privateAttributes, @[attribute]); } builder.privateAttributes = [LDUserModel allUserAttributes]; + + LDUserModel *user = [builder build]; + + XCTAssertEqualObjects(user.privateAttributes, [LDUserModel allUserAttributes]); +} + +- (void)testBuild_allPrivateAttributes { + LDUserBuilder *builder = [[LDUserBuilder alloc] init]; + builder.privateAttributes = [LDUserModel allUserAttributes]; + LDUserModel *user = [builder build]; + XCTAssertEqualObjects(user.privateAttributes, [LDUserModel allUserAttributes]); } diff --git a/DarklyTests/Mocks/ClientDelegateMock.h b/DarklyTests/Mocks/ClientDelegateMock.h new file mode 100644 index 00000000..a6f48fd1 --- /dev/null +++ b/DarklyTests/Mocks/ClientDelegateMock.h @@ -0,0 +1,27 @@ +// +// ClientDelegateMock.h +// DarklyTests +// +// Created by Mark Pokorny on 10/25/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import +#import "LDClient.h" + +typedef void(^MockLDEnvironmentDelegateCallbackBlock)(void); +typedef void(^MockLDEnvironmentDelegateFeatureFlagDidUpdateCallbackBlock)(NSString* flagKey); + +@interface ClientDelegateMock: NSObject + +@property (nonatomic, assign) NSInteger userDidUpdateCallCount; +@property (nonatomic, assign) NSInteger userUnchangedCallCount; +@property (nonatomic, assign) NSInteger featureFlagDidUpdateCallCount; +@property (nonatomic, assign) NSInteger serverConnectionUnavailableCallCount; +@property (nonatomic, strong) MockLDEnvironmentDelegateCallbackBlock userDidUpdateCallback; +@property (nonatomic, strong) MockLDEnvironmentDelegateCallbackBlock userUnchangedCallback; +@property (nonatomic, strong) MockLDEnvironmentDelegateFeatureFlagDidUpdateCallbackBlock featureFlagDidUpdateCallback; +@property (nonatomic, strong) MockLDEnvironmentDelegateCallbackBlock serverUnavailableCallback; + ++(instancetype)clientDelegateMock; +@end diff --git a/DarklyTests/Mocks/ClientDelegateMock.m b/DarklyTests/Mocks/ClientDelegateMock.m new file mode 100644 index 00000000..338e6a85 --- /dev/null +++ b/DarklyTests/Mocks/ClientDelegateMock.m @@ -0,0 +1,48 @@ +// +// ClientDelegateMock.m +// DarklyTests +// +// Created by Mark Pokorny on 10/25/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "ClientDelegateMock.h" + +@implementation ClientDelegateMock + ++(instancetype)clientDelegateMock { + return [[ClientDelegateMock alloc] init]; +} + +-(instancetype)init { + self = [super init]; + + return self; +} + +-(void)userDidUpdate { + self.userDidUpdateCallCount = [self processCallbackWithCount:self.userDidUpdateCallCount block:self.userDidUpdateCallback]; +} + +-(void)userUnchanged { + self.userUnchangedCallCount = [self processCallbackWithCount:self.userUnchangedCallCount block:self.userUnchangedCallback]; +} + +-(void)featureFlagDidUpdate:(NSString *)key { + self.featureFlagDidUpdateCallCount += 1; + if (self.featureFlagDidUpdateCallback == nil) { return; } + self.featureFlagDidUpdateCallback(key); +} + +-(void)serverConnectionUnavailable { + self.serverConnectionUnavailableCallCount = [self processCallbackWithCount:self.serverConnectionUnavailableCallCount block:self.serverUnavailableCallback]; +} + +-(NSInteger)processCallbackWithCount:(NSInteger)callbackCount block:(MockLDEnvironmentDelegateCallbackBlock)callbackBlock { + callbackCount += 1; + if (!callbackBlock) { return callbackCount; } + callbackBlock(); + return callbackCount; +} + +@end diff --git a/DarklyTests/Mocks/LDConfig+Testable.h b/DarklyTests/Mocks/LDConfig+Testable.h new file mode 100644 index 00000000..9e7d89b3 --- /dev/null +++ b/DarklyTests/Mocks/LDConfig+Testable.h @@ -0,0 +1,21 @@ +// +// LDConfig+Testable.h +// DarklyTests +// +// Created by Mark Pokorny on 10/31/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDConfig.h" + +extern NSString * const LDConfigTestEnvironmentNameMock; +extern NSString * const LDConfigTestSecondaryMobileKeyMock; + +NS_ASSUME_NONNULL_BEGIN + +@interface LDConfig (Testable) +@property (nonatomic, strong, nonnull) NSArray *flagRetryStatusCodes; ++(NSDictionary*)secondaryMobileKeysStub; +@end + +NS_ASSUME_NONNULL_END diff --git a/DarklyTests/Mocks/LDConfig+Testable.m b/DarklyTests/Mocks/LDConfig+Testable.m new file mode 100644 index 00000000..fa2d3623 --- /dev/null +++ b/DarklyTests/Mocks/LDConfig+Testable.m @@ -0,0 +1,33 @@ +// +// LDConfig+Testable.m +// DarklyTests +// +// Created by Mark Pokorny on 10/31/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDConfig+Testable.h" + +NSString * const LDConfigTestEnvironmentNameMock = @"Name"; +NSString * const LDConfigTestSecondaryMobileKeyMock = @"Key"; + +@interface LDConfig (Testable_Private) +@property (nonatomic, strong, readonly, class) NSArray *environmentSuffixes; +@end + +@implementation LDConfig (Testable) +@dynamic flagRetryStatusCodes; + ++(NSArray*)environmentSuffixes { + return @[@"A", @"B", @"C", @"D", @"E"]; +} + ++(NSDictionary*)secondaryMobileKeysStub { + NSMutableDictionary *secondaryMobileKeys = [NSMutableDictionary dictionaryWithCapacity:self.environmentSuffixes.count]; + for (NSString *suffix in self.environmentSuffixes) { + secondaryMobileKeys[[NSString stringWithFormat:@"%@.%@", LDConfigTestEnvironmentNameMock, suffix]] = + [NSString stringWithFormat:@"%@.%@", LDConfigTestSecondaryMobileKeyMock, suffix]; + } + return [secondaryMobileKeys copy]; +} +@end diff --git a/DarklyTests/Mocks/LDEnvironmentMock.h b/DarklyTests/Mocks/LDEnvironmentMock.h new file mode 100644 index 00000000..0a2fc643 --- /dev/null +++ b/DarklyTests/Mocks/LDEnvironmentMock.h @@ -0,0 +1,36 @@ +// +// LDEnvironmentMock.h +// DarklyTests +// +// Created by Mark Pokorny on 10/31/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDEnvironment.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface LDEnvironmentMock : LDEnvironment +@property (nonatomic, copy) NSString *environmentMockCalledValueMobileKey; +@property (nonatomic, strong) LDConfig *environmentMockCalledValueConfig; +@property (nonatomic, strong) LDUserModel *environmentMockCalledValueUser; +@property (nonatomic, assign) NSUInteger environmentMockCallCount; ++(instancetype)environmentMockForMobileKey:(NSString*)mobileKey config:(LDConfig*)config user:(LDUserModel*)user; +@property (nonatomic, assign) NSUInteger startCallCount; +-(void)start; +@property (nonatomic, assign) NSUInteger stopCallCount; +-(void)stop; +@property (nonatomic, assign) BOOL setOnlineCalledValueOnline; +@property (nonatomic, assign) NSUInteger setOnlineCallCount; +-(void)setOnline:(BOOL)online; +@property (nonatomic, strong) LDUserModel *updateUserCalledValueNewUser; +@property (nonatomic, assign) NSUInteger updateUserCallCount; +-(void)updateUser:(LDUserModel*)newUser; +@property (nonatomic, assign) BOOL reportOnline; +-(BOOL)isOnline; +@property (nonatomic, assign) NSUInteger flushCallCount; +@property (nonatomic, assign) BOOL reportFlushResult; +-(BOOL)flush; +@end + +NS_ASSUME_NONNULL_END diff --git a/DarklyTests/Mocks/LDEnvironmentMock.m b/DarklyTests/Mocks/LDEnvironmentMock.m new file mode 100644 index 00000000..bea65c65 --- /dev/null +++ b/DarklyTests/Mocks/LDEnvironmentMock.m @@ -0,0 +1,65 @@ +// +// LDEnvironmentMock.m +// DarklyTests +// +// Created by Mark Pokorny on 10/31/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDEnvironmentMock.h" +#import "LDConfig.h" +#import "NSString+LaunchDarkly.h" + +@implementation LDEnvironmentMock ++(instancetype)environmentMockForMobileKey:(NSString*)mobileKey config:(LDConfig*)config user:(LDUserModel*)user { + LDEnvironmentMock *environmentMock = [[LDEnvironmentMock alloc] init]; + environmentMock.environmentMockCalledValueMobileKey = mobileKey; + environmentMock.environmentMockCalledValueConfig = config; + environmentMock.environmentMockCalledValueUser = user; + environmentMock.reportFlushResult = YES; + + return environmentMock; +} + +-(void)start { + self.startCallCount += 1; +} + +-(void)stop { + self.stopCallCount += 1; +} + +-(void)setOnline:(BOOL)online { + self.setOnlineCalledValueOnline = online; + self.setOnlineCallCount += 1; +} + +-(void)updateUser:(LDUserModel*)newUser { + self.updateUserCalledValueNewUser = newUser; + self.updateUserCallCount += 1; +} + +-(BOOL)isOnline { + return self.reportOnline; +} + +-(BOOL)flush { + self.flushCallCount += 1; + return YES; +} + +-(NSString*)description { + NSString *description = [NSString stringWithFormat:@"", description]; + return description; +} +@end diff --git a/DarklyTests/Mocks/LDRequestManagerDelegateMock.h b/DarklyTests/Mocks/LDRequestManagerDelegateMock.h new file mode 100644 index 00000000..7305eec1 --- /dev/null +++ b/DarklyTests/Mocks/LDRequestManagerDelegateMock.h @@ -0,0 +1,25 @@ +// +// LDRequestManagerDelegateMock.h +// DarklyTests +// +// Created by Mark Pokorny on 9/13/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import +#import "LDRequestManager.h" + +@interface LDRequestManagerDelegateMock : NSObject +@property (nonatomic, assign) NSInteger processedEventsCallCount; +@property (nonatomic, assign) BOOL processedEventsSuccess; +@property (nonatomic, strong, nullable) NSArray *processedEventsJsonEventArray; +@property (nonatomic, strong, nullable) NSDate *processedEventsResponseDate; +@property (nonatomic, strong) void (^processedEventsCallback)(void); +-(void)processedEvents:(BOOL)success jsonEventArray:(nonnull NSArray*)jsonEventArray responseDate:(nullable NSDate*)responseDate; + +@property (nonatomic, assign) NSInteger processedConfigCallCount; +@property (nonatomic, assign) BOOL processedConfigSuccess; +@property (nonatomic, strong, nullable) NSDictionary *processedConfigJsonConfigDictionary; +@property (nonatomic, strong) void (^processedConfigCallback)(void); +-(void)processedConfig:(BOOL)success jsonConfigDictionary:(nullable NSDictionary*)jsonConfigDictionary; +@end diff --git a/DarklyTests/Mocks/LDRequestManagerDelegateMock.m b/DarklyTests/Mocks/LDRequestManagerDelegateMock.m new file mode 100644 index 00000000..f285dfe2 --- /dev/null +++ b/DarklyTests/Mocks/LDRequestManagerDelegateMock.m @@ -0,0 +1,30 @@ +// +// LDRequestManagerDelegateMock.m +// DarklyTests +// +// Created by Mark Pokorny on 9/13/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDRequestManagerDelegateMock.h" + +@implementation LDRequestManagerDelegateMock +-(void)processedEvents:(BOOL)success jsonEventArray:(nonnull NSArray*)jsonEventArray responseDate:(nullable NSDate*)responseDate { + self.processedEventsCallCount += 1; + self.processedEventsSuccess = success; + self.processedEventsJsonEventArray = jsonEventArray; + self.processedEventsResponseDate = responseDate; + if (self.processedEventsCallback) { + self.processedEventsCallback(); + } +} + +-(void)processedConfig:(BOOL)success jsonConfigDictionary:(nullable NSDictionary*)jsonConfigDictionary { + self.processedConfigCallCount += 1; + self.processedConfigSuccess = success; + self.processedConfigJsonConfigDictionary = jsonConfigDictionary; + if (self.processedConfigCallback) { + self.processedConfigCallback(); + } +} +@end diff --git a/DarklyTests/Models/LDDataManagerTest.m b/DarklyTests/Models/LDDataManagerTest.m index f8cae237..12fa8f52 100644 --- a/DarklyTests/Models/LDDataManagerTest.m +++ b/DarklyTests/Models/LDDataManagerTest.m @@ -5,6 +5,7 @@ #import "DarklyXCTestCase.h" #import "LDFlagConfigModel.h" #import "LDDataManager.h" +#import "LDDataManager+Testable.h" #import "LDUserModel.h" #import "LDUserModel+Testable.h" #import "LDFlagConfigModel.h" @@ -21,45 +22,54 @@ #import "LDFlagConfigTracker.h" #import "LDFlagConfigTracker+Testable.h" #import "LDConfig.h" +#import "LDConfig+LaunchDarkly.h" +#import "LDConfig+Testable.h" #import "NSDate+Testable.h" #import "NSArray+Testable.h" #import "NSNumber+LaunchDarkly.h" +#import "LDUserEnvironment+Testable.h" +#import "NSDictionary+LaunchDarkly.h" NSString * const kMobileKeyMock = @"LDDataManagerTest.mobileKeyMock"; +extern NSString * const kUserDefaultsKeyUserEnvironments; @interface LDDataManager (LDDataManagerTest) +-(NSMutableDictionary*)retrieveStoredUserModels; ++(nonnull NSMutableDictionary*)retrieveStoredUserModels; ++(void)storeUserModels:(NSDictionary *)userModels; ++(NSDictionary*)retrieveUserEnvironments; ++(void)saveUserEnvironments:(NSDictionary*)userEnvironments; +-(void)saveEnvironmentForUser:(LDUserModel*)user completion:(void (^)(void))completion; -(void)saveUser:(LDUserModel*)user asDict:(BOOL)asDict completion:(void (^)(void))completion; +-(void)saveUserEnvironments:(NSDictionary*)userEnvironments; +-(NSDictionary*)retrieveUserEnvironments; +-(void)discardEventsDictionary; ++(void)removeStoredUsers; @end @interface LDDataManagerTest : DarklyXCTestCase -@property (nonatomic, strong) id clientMock; @property (nonatomic, strong) id eventModelMock; @property (nonatomic, strong) LDUserModel *user; @property (nonatomic, strong) LDConfig *config; - +@property (nonatomic, strong) LDDataManager *dataManager; @end @implementation LDDataManagerTest -@synthesize clientMock; -@synthesize user; - (void)setUp { [super setUp]; + [LDDataManager removeStoredUsers]; self.config = [[LDConfig alloc] initWithMobileKey:kMobileKeyMock]; - self.user = [LDUserModel stubWithKey:nil]; - - self.clientMock = OCMClassMock([LDClient class]); - OCMStub(ClassMethod([self.clientMock sharedInstance])).andReturn(clientMock); - OCMStub([self.clientMock ldUser]).andReturn(user); - OCMStub([self.clientMock ldConfig]).andReturn(self.config); + self.user = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + self.user.flagConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"featureFlags-excludeNulls" eventTrackingContext:nil]; //NSNull can't go into the user cache in NSUserDefaults + self.dataManager = [LDDataManager dataManagerWithMobileKey:self.config.mobileKey config:self.config]; } - (void)tearDown { - [self.clientMock stopMocking]; - self.clientMock = nil; [self.eventModelMock stopMocking]; self.eventModelMock = nil; - [[LDDataManager sharedManager] flushEventsDictionary]; + [self.dataManager discardEventsDictionary]; + [LDDataManager removeStoredUsers]; [super tearDown]; } @@ -94,7 +104,7 @@ -(LDFlagConfigValue*)setupCreateDebugEventTestWithLastEventResponseDate:(NSDate* } -(LDFlagConfigValue*)setupCreateDebugEventTestWithLastEventResponseDate:(NSDate*)lastResponse debugUntil:(NSDate*)debugUntil includeTrackingContext:(BOOL)includeTrackingContext { - [LDDataManager sharedManager].lastEventResponseDate = lastResponse; + self.dataManager.lastEventResponseDate = lastResponse; LDEventTrackingContext *eventTrackingContext = includeTrackingContext ? [LDEventTrackingContext contextWithTrackEvents:NO debugEventsUntilDate:debugUntil] : nil; self.user = [LDUserModel stubWithKey:nil usingTracker:nil eventTrackingContext:eventTrackingContext]; LDFlagConfigValue *flagConfigValue = [self.user.flagConfig flagConfigValueForFlagKey:kFlagKeyIsABawler]; @@ -111,26 +121,562 @@ -(LDFlagConfigValue*)setupCreateDebugEventTestWithLastEventResponseDate:(NSDate* return flagConfigValue; } --(void)testCreateFlagEvaluationEvents { +-(void)testInitAndConstructor { + LDDataManager *dataManager = [LDDataManager dataManagerWithMobileKey:self.config.mobileKey config:self.config]; + + XCTAssertNotNil(dataManager); + XCTAssertEqualObjects(dataManager.mobileKey, self.config.mobileKey); + XCTAssertEqualObjects(dataManager.config, self.config); + XCTAssertNotNil(dataManager.eventsArray); + XCTAssertEqual(dataManager.eventsArray.count, 0); + XCTAssertNotNil(dataManager.eventsQueue); + XCTAssertNotNil(dataManager.saveUserQueue); +} + +-(void)testInitAndConstructor_missingMobileKey { + NSString *missingMobileKey; + LDDataManager *dataManager = [LDDataManager dataManagerWithMobileKey:missingMobileKey config:self.config]; + + XCTAssertNil(dataManager); +} + +-(void)testInitAndConstructor_emptyMobileKey { + LDDataManager *dataManager = [LDDataManager dataManagerWithMobileKey:@"" config:self.config]; + + XCTAssertNil(dataManager); +} + +-(void)testInitAndConstructor_missingConfig { + LDConfig *missingConfig; + LDDataManager *dataManager = [LDDataManager dataManagerWithMobileKey:self.config.mobileKey config:missingConfig]; + + XCTAssertNil(dataManager); +} + +-(void)testConvertToEnvironmentBasedCache { + self.config.secondaryMobileKeys = [LDConfig secondaryMobileKeysStub]; + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:kUserCacheSize]; + NSDictionary *users = [LDUserModel stubUsersWithKeys:userKeys]; + [LDDataManager storeUserModels:users]; + + for (NSString *userKey in userKeys) { + [LDDataManager convertToEnvironmentBasedCacheForUser:users[userKey] config:self.config]; + } + + NSDictionary *cachedEnvironments = [LDDataManager retrieveUserEnvironments]; + NSDictionary *cachedUsers = [LDDataManager retrieveStoredUserModels]; + for (NSString *userKey in userKeys) { + LDUserEnvironment *cachedEnvironment = cachedEnvironments[userKey]; + XCTAssertNotNil(cachedEnvironment); + if (cachedEnvironment == nil) { continue; } + XCTAssertEqualObjects(cachedEnvironment.userKey, userKey); + LDUserModel *originalUser = users[userKey]; + for (NSString *mobileKey in self.config.mobileKeys) { + XCTAssertTrue([cachedEnvironment.users[mobileKey] isEqual:originalUser ignoringAttributes:@[kUserAttributeUpdatedAt]]); + } + XCTAssertEqual(cachedEnvironment.users.count, self.config.mobileKeys.count); + XCTAssertNotNil(cachedUsers[userKey]); //Even though the cached user was converted to a UserEnvironment, the cached user remains until the second call to convert the cache + } +} + +-(void)testConvertToEnvironmentBasedCache_singleEnvironment { + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:kUserCacheSize]; + NSDictionary *users = [LDUserModel stubUsersWithKeys:userKeys]; + [LDDataManager storeUserModels:users]; + + for (NSString *userKey in userKeys) { + [LDDataManager convertToEnvironmentBasedCacheForUser:users[userKey] config:self.config]; + } + + NSDictionary *cachedEnvironments = [LDDataManager retrieveUserEnvironments]; + NSDictionary *cachedUsers = [LDDataManager retrieveStoredUserModels]; + for (NSString *userKey in userKeys) { + LDUserEnvironment *cachedEnvironment = cachedEnvironments[userKey]; + XCTAssertNotNil(cachedEnvironment); + if (cachedEnvironment == nil) { continue; } + XCTAssertEqualObjects(cachedEnvironment.userKey, userKey); + LDUserModel *originalUser = users[userKey]; + XCTAssertTrue([cachedEnvironment.users[self.config.mobileKey] isEqual:originalUser ignoringAttributes:@[kUserAttributeUpdatedAt]]); + XCTAssertEqual(cachedEnvironment.users.count, 1); + XCTAssertNotNil(cachedUsers[userKey]); //Even though the cached user was converted to a UserEnvironment, the cached user remains until the second call to convert the cache + } +} + +-(void)testConvertToEnvironmentBasedCache_missingUser { + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:kUserCacheSize]; + NSDictionary *users = [LDUserModel stubUsersWithKeys:userKeys]; + [LDDataManager storeUserModels:users]; + LDUserModel *missingUser; + + [LDDataManager convertToEnvironmentBasedCacheForUser:missingUser config:self.config]; + + XCTAssertTrue([LDDataManager retrieveUserEnvironments].count == 0); +} + +-(void)testConvertToEnvironmentBasedCache_missingConfig { + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:kUserCacheSize]; + NSDictionary *users = [LDUserModel stubUsersWithKeys:userKeys]; + [LDDataManager storeUserModels:users]; + LDConfig *missingConfig; + + [LDDataManager convertToEnvironmentBasedCacheForUser:self.user config:missingConfig]; + + XCTAssertTrue([LDDataManager retrieveUserEnvironments].count == 0); +} + +-(void)testConvertToEnvironmentBasedCache_secondCall { + //After the first call to convert, there should be both a set of userEnvironments and a set of userModels. In that state, the method should just delete the userModel for the user + self.config.secondaryMobileKeys = [LDConfig secondaryMobileKeysStub]; + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:kUserCacheSize]; + NSDictionary *users = [LDUserModel stubUsersWithKeys:userKeys]; + [LDDataManager storeUserModels:users]; + NSDictionary *userEnvironments = [LDUserEnvironment stubUserEnvironmentsForUsersWithKeys:userKeys mobileKeys:self.config.mobileKeys]; + [LDDataManager saveUserEnvironments:userEnvironments]; + + for (NSString *userKey in userKeys) { + [LDDataManager convertToEnvironmentBasedCacheForUser:users[userKey] config:self.config]; + } + + NSDictionary *cachedEnvironments = [LDDataManager retrieveUserEnvironments]; + NSDictionary *cachedUsers = [LDDataManager retrieveStoredUserModels]; + for (NSString *userKey in userKeys) { + LDUserEnvironment *cachedEnvironment = cachedEnvironments[userKey]; + XCTAssertNotNil(cachedEnvironment); + if (cachedEnvironment == nil) { continue; } + XCTAssertEqualObjects(cachedEnvironment.userKey, userKey); + LDUserEnvironment *originalEnvironment = userEnvironments[userKey]; + for (NSString *mobileKey in self.config.mobileKeys) { + LDUserModel *originalUser = originalEnvironment.users[mobileKey]; + XCTAssertTrue([cachedEnvironment.users[mobileKey] isEqual:originalUser ignoringAttributes:@[kUserAttributeUpdatedAt]]); + } + XCTAssertNil(cachedUsers[userKey]); //Since there was a user environment on the second call to convert, the old user cache should no longer have the user + } +} + +-(void)testConvertToEnvironmentBasedCache_noMatchingUserOrEnvironment { + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:kUserCacheSize]; + NSDictionary *users = [LDUserModel stubUsersWithKeys:userKeys]; + [LDDataManager storeUserModels:users]; + NSDictionary *userEnvironments = [LDUserEnvironment stubUserEnvironmentsForUsersWithKeys:userKeys mobileKeys:self.config.mobileKeys]; + [LDDataManager saveUserEnvironments:userEnvironments]; + LDUserModel *uncachedUser = [LDUserModel stubWithKey:[NSUUID UUID].UUIDString]; + + [LDDataManager convertToEnvironmentBasedCacheForUser:uncachedUser config:self.config]; + + NSDictionary *cachedEnvironments = [LDDataManager retrieveUserEnvironments]; + NSDictionary *cachedUsers = [LDDataManager retrieveStoredUserModels]; + for (NSString *userKey in userKeys) { + LDUserEnvironment *cachedEnvironment = cachedEnvironments[userKey]; + XCTAssertNotNil(cachedEnvironment); + if (cachedEnvironment == nil) { continue; } + XCTAssertEqualObjects(cachedEnvironment.userKey, userKey); + LDUserEnvironment *originalEnvironment = userEnvironments[userKey]; + for (NSString *mobileKey in self.config.mobileKeys) { + LDUserModel *originalUser = originalEnvironment.users[mobileKey]; + XCTAssertTrue([cachedEnvironment.users[mobileKey] isEqual:originalUser ignoringAttributes:@[kUserAttributeUpdatedAt]]); + } + XCTAssertNotNil(cachedUsers[userKey]); //Make sure other users have not been affected + } +} + +-(void)testConvertToEnvironmentBasedCache_noMatchingUser { + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:kUserCacheSize]; + NSMutableDictionary *users = [NSMutableDictionary dictionaryWithDictionary:[LDUserModel stubUsersWithKeys:userKeys]]; + LDUserModel *uncachedUser = users[userKeys.firstObject]; + [users removeObjectForKey:userKeys.firstObject]; + [LDDataManager storeUserModels:users]; + NSDictionary *userEnvironments = [LDUserEnvironment stubUserEnvironmentsForUsersWithKeys:userKeys mobileKeys:self.config.mobileKeys]; + [LDDataManager saveUserEnvironments:userEnvironments]; + + [LDDataManager convertToEnvironmentBasedCacheForUser:uncachedUser config:self.config]; + + NSDictionary *cachedEnvironments = [LDDataManager retrieveUserEnvironments]; + NSDictionary *cachedUsers = [LDDataManager retrieveStoredUserModels]; + for (NSString *userKey in userKeys) { + LDUserEnvironment *cachedEnvironment = cachedEnvironments[userKey]; + XCTAssertNotNil(cachedEnvironment); + if (cachedEnvironment == nil) { continue; } + XCTAssertEqualObjects(cachedEnvironment.userKey, userKey); + LDUserEnvironment *originalEnvironment = userEnvironments[userKey]; + for (NSString *mobileKey in self.config.mobileKeys) { + LDUserModel *originalUser = originalEnvironment.users[mobileKey]; + XCTAssertTrue([cachedEnvironment.users[mobileKey] isEqual:originalUser ignoringAttributes:@[kUserAttributeUpdatedAt]]); + } + if ([userKey isEqualToString:uncachedUser.key]) { + XCTAssertNil(cachedUsers[userKey]); //Make sure the uncached user is still uncached. + } else { + XCTAssertNotNil(cachedUsers[userKey]); //Make sure other users have not been affected + } + } +} + +-(void)testSaveAndFindUserWithKey { + //Save, then find several users, up to the kUserCacheSize, to make sure users are saved and retrieved correctly + self.config.secondaryMobileKeys = [LDConfig secondaryMobileKeysStub]; + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:kUserCacheSize]; + NSDictionary *userEnvironments = [LDUserEnvironment stubUserEnvironmentsForUsersWithKeys:userKeys mobileKeys:self.config.mobileKeys]; + //A dataManager works in only one environment + NSMutableDictionary *dataManagers = [NSMutableDictionary dictionaryWithCapacity:self.config.mobileKeys.count]; + for (NSString *mobileKey in self.config.mobileKeys) { + dataManagers[mobileKey] = [LDDataManager dataManagerWithMobileKey:mobileKey config:self.config]; + } + NSMutableArray *saveUserExpectations = [NSMutableArray arrayWithCapacity:kUserCacheSize * userEnvironments.count]; + + for (NSString *userKey in userKeys) { + LDUserEnvironment *userEnvironment = userEnvironments[userKey]; + for (NSString *mobileKey in self.config.mobileKeys) { + XCTestExpectation *saveUserExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"LDDataManagerTest.%@.saveUserExpectation.%@.%@", + NSStringFromSelector(_cmd), userKey, mobileKey]]; + [saveUserExpectations addObject:saveUserExpectation]; + LDUserModel *userInEnvironment = [userEnvironment userForMobileKey:mobileKey]; + LDDataManager *dataManager = dataManagers[mobileKey]; + [dataManager saveEnvironmentForUser:userInEnvironment completion:^{ + [saveUserExpectation fulfill]; + }]; + } + } + [self waitForExpectations: saveUserExpectations timeout:1.0]; + + for (NSString *userKey in userKeys) { + LDUserEnvironment *userEnvironment = userEnvironments[userKey]; + for (NSString *mobileKey in self.config.mobileKeys) { + LDUserModel *originalUserInEnvironment = [userEnvironment userForMobileKey:mobileKey]; + LDDataManager *dataManager = dataManagers[mobileKey]; + LDUserModel *retrievedUserInEnvironment = [dataManager findUserWithKey:userKey]; + + XCTAssertTrue([retrievedUserInEnvironment isEqual:originalUserInEnvironment ignoringAttributes:@[kUserAttributeUpdatedAt]]); + } + } +} + +-(void)testSaveAndFindUserWithKey_singleUser_singleEnvironment { + XCTestExpectation *saveUserExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"LDDataManagerTest.%@.saveUserExpectation", NSStringFromSelector(_cmd)]]; + + [self.dataManager saveEnvironmentForUser:self.user completion:^{ + [saveUserExpectation fulfill]; + }]; + [self waitForExpectations:@[saveUserExpectation] timeout:1.0]; + LDUserModel *retrievedUser = [self.dataManager findUserWithKey:self.user.key]; + + XCTAssertTrue([retrievedUser isEqual:self.user ignoringAttributes:@[kUserAttributeUpdatedAt]]); +} + +-(void)testSaveAndFindUserWithKey_noStoredUserEnvironments { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:kUserDefaultsKeyUserEnvironments]; + XCTestExpectation *saveUserExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"LDDataManagerTest.%@.saveUserExpectation", NSStringFromSelector(_cmd)]]; + + [self.dataManager saveEnvironmentForUser:self.user completion:^{ + [saveUserExpectation fulfill]; + }]; + [self waitForExpectations:@[saveUserExpectation] timeout:1.0]; + LDUserModel *retrievedUser = [self.dataManager findUserWithKey:self.user.key]; + + XCTAssertTrue([retrievedUser isEqual:self.user ignoringAttributes:@[kUserAttributeUpdatedAt]]); +} + +-(void)testSaveAndFindUserWithKey_userWithoutFlagConfig { + [self.dataManager saveUser:self.user]; + LDFlagConfigModel *flagConfig = [[LDFlagConfigModel alloc] init]; + self.user.flagConfig = flagConfig; + XCTestExpectation *userSavedExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"LDDataManagerTest.%@.userSavedExpectation",NSStringFromSelector(_cmd)]]; + + [self.dataManager saveEnvironmentForUser:self.user completion:^{ + [userSavedExpectation fulfill]; + }]; + [self waitForExpectations:@[userSavedExpectation] timeout:1.0]; + + LDUserModel *foundUser = [self.dataManager findUserWithKey:self.user.key]; + XCTAssertTrue(foundUser.flagConfig.isEmpty); +} + +- (void)testSaveAndFindUser_cachedUser { + //Temporarily make the user's flagConfig empty + LDFlagConfigModel *flagConfig = self.user.flagConfig; + self.user.flagConfig = [[LDFlagConfigModel alloc] init]; + [self.dataManager saveUser:self.user]; + //Restore the flag config + self.user.flagConfig = flagConfig; + XCTestExpectation *saveUserExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"LDDataManagerTest.%@.saveUserExpectation", NSStringFromSelector(_cmd)]]; + + [self.dataManager saveEnvironmentForUser:self.user completion:^{ + [saveUserExpectation fulfill]; + }]; + + [self waitForExpectations:@[saveUserExpectation] timeout:1.0]; + LDUserModel *retrievedUser = [self.dataManager findUserWithKey:self.user.key]; + XCTAssertTrue([retrievedUser isEqual:self.user ignoringAttributes:@[@"updatedAt"]]); +} + +-(void)testSaveAndFindUsers_overCapacity { + //Save more users than the cache should hold, and verify the cache retains only the newest users + self.config.secondaryMobileKeys = [LDConfig secondaryMobileKeysStub]; + NSUInteger userCount = kUserCacheSize + 3; + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:userCount]; + NSMutableDictionary *userEnvironments = [NSMutableDictionary dictionaryWithCapacity:userCount]; + NSMutableDictionary *dataManagers = [NSMutableDictionary dictionaryWithCapacity:self.config.mobileKeys.count]; + for (NSString *mobileKey in self.config.mobileKeys) { + dataManagers[mobileKey] = [LDDataManager dataManagerWithMobileKey:mobileKey config:self.config]; + } + NSMutableArray *saveUserExpectations = [NSMutableArray arrayWithCapacity:self.config.mobileKeys.count]; + + //Save each user, 1 at a time, in reverse order. That way the oldest users have the highest indices, making it easier to assert later + for (NSInteger index = userCount - 1; index >= 0; index--) { + NSString *userKey = userKeys[index]; + NSDictionary *userModels = [LDUserEnvironment stubUserModelsForUserWithKey:userKey environmentKeys:self.config.mobileKeys]; + LDUserEnvironment *userEnvironment = [LDUserEnvironment userEnvironmentForUserWithKey:userKey environments:userModels]; + userEnvironments[userKey] = userEnvironment; + for (NSString *mobileKey in self.config.mobileKeys) { + XCTestExpectation *saveUserExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"LDDataManagerTest.%@.saveUserExpectation.%@.%@", + NSStringFromSelector(_cmd), userKey, mobileKey]]; + [saveUserExpectations addObject:saveUserExpectation]; + [dataManagers[mobileKey] saveEnvironmentForUser:userModels[mobileKey] completion:^{ + [saveUserExpectation fulfill]; + }]; + } + //Force each user to save all environments before proceeding. Otherwise newer users could be corrupted by an older user's save block that the system delayed. + //This fits the operational use for saving also...the SDK doesn't save multiple user's at once, rather they are saved one at a time. + [self waitForExpectations: saveUserExpectations timeout:1.0]; + [saveUserExpectations removeAllObjects]; + } + + for (NSInteger index = 0; index < userCount; index++) { + NSString *userKey = userKeys[index]; + LDUserEnvironment *userEnvironment = userEnvironments[userKey]; + for (NSString *mobileKey in self.config.mobileKeys) { + LDUserModel *retrievedUserInEnvironment = [dataManagers[mobileKey] findUserWithKey:userKey]; + + if (index < kUserCacheSize) { + //Newest users (indices 0.. *userSavedExpectations = [NSMutableArray arrayWithCapacity:dataManagerCount]; + NSMutableDictionary *dataManagers = [NSMutableDictionary dictionaryWithCapacity:dataManagerCount]; + NSMutableDictionary *users = [NSMutableDictionary dictionaryWithCapacity:dataManagerCount]; + dispatch_time_t fireTime = dispatch_time(DISPATCH_TIME_NOW, 40000); //40ms from now, arbitrary but seems enough time to setup without delaying the test + for (NSUInteger index = 0; index < dataManagerCount; index++) { + NSString *key = [[NSUUID UUID] UUIDString]; + LDDataManager *dataManager = [LDDataManager dataManagerWithMobileKey:key config:self.config]; + dataManagers[key] = dataManager; + XCTestExpectation *userSavedExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"LDDataManagerTest.%@.userSavedExpectation.%@", NSStringFromSelector(_cmd), key]]; + [userSavedExpectations addObject:userSavedExpectation]; + LDUserModel *user = [LDUserModel stubWithKey:key]; + users[key] = user; + dispatch_queue_t dispatchQueue = dispatch_queue_create([key UTF8String], DISPATCH_QUEUE_SERIAL); + + dispatch_after(fireTime, dispatchQueue, ^{ + [dataManager saveEnvironmentForUser:user completion:^{ + [userSavedExpectation fulfill]; + }]; + }); + } + + [self waitForExpectations:userSavedExpectations timeout:1.0]; + + for (NSString *key in users.allKeys) { + LDUserModel *targetUser = users[key]; + LDDataManager *dataManager = dataManagers[key]; + LDUserModel *retrievedUser = [dataManager findUserWithKey:key]; + XCTAssertTrue([retrievedUser isEqual:targetUser ignoringAttributes:@[kUserAttributeUpdatedAt]]); + } +} + +-(void)testSaveUserWithKey_noUserKey { + XCTestExpectation *saveUserExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"LDDataManagerTest.%@.saveUserExpectation", NSStringFromSelector(_cmd)]]; + LDUserModel *userWithoutKey = [LDUserModel stubWithKey:nil]; + userWithoutKey.key = nil; + + [self.dataManager saveEnvironmentForUser:userWithoutKey completion:^{ + [saveUserExpectation fulfill]; + }]; + [self waitForExpectations:@[saveUserExpectation] timeout:1.0]; + + NSDictionary *userEnvironments = [LDDataManager retrieveUserEnvironments]; + XCTAssertTrue(userEnvironments.count == 0); +} + +-(void)testSaveUser_missingUser { + XCTestExpectation *userSaveExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"%@.%@.userSaveExpectation", NSStringFromClass([self class]), NSStringFromSelector(_cmd)]]; + + LDUserModel *missingUser; + + [self.dataManager saveEnvironmentForUser:missingUser completion:^{ + [userSaveExpectation fulfill]; + }]; + [self waitForExpectations:@[userSaveExpectation] timeout:1.0]; + + NSDictionary *userEnvironments = [LDDataManager retrieveUserEnvironments]; + XCTAssertTrue(userEnvironments.count == 0); +} + +-(void)testFindUser_missingUserKey { + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:kUserCacheSize]; + NSDictionary *userEnvironments = [LDUserEnvironment stubUserEnvironmentsForUsersWithKeys:userKeys]; + [LDDataManager saveUserEnvironments:userEnvironments]; + NSString *missingUserKey; + + LDUserModel *foundUser = [self.dataManager findUserWithKey:missingUserKey]; + + XCTAssertNil(foundUser); +} + +-(void)testFindUser_emptyUserKey { + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:kUserCacheSize]; + NSDictionary *userEnvironments = [LDUserEnvironment stubUserEnvironmentsForUsersWithKeys:userKeys]; + [LDDataManager saveUserEnvironments:userEnvironments]; + + LDUserModel *foundUser = [self.dataManager findUserWithKey:@""]; + + XCTAssertNil(foundUser); +} + +-(void)testRetrieveFlagConfigForUser { + XCTestExpectation *saveUserExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"LDDataManagerTest.%@.saveUserExpectation", NSStringFromSelector(_cmd)]]; + [self.dataManager saveEnvironmentForUser:self.user completion:^{ + [saveUserExpectation fulfill]; + }]; + LDUserBuilder *userBuilder = [LDUserBuilder currentBuilder:self.user]; + LDUserModel *restoredUser = [userBuilder build]; + [self waitForExpectations:@[saveUserExpectation] timeout:1.0]; + + LDFlagConfigModel *flagConfig = [self.dataManager retrieveFlagConfigForUser:restoredUser]; + + XCTAssertTrue([flagConfig isEqualToConfig:self.user.flagConfig]); +} + +-(void)testRetrieveFlagConfigForUser_userNotCached { + LDUserBuilder *userBuilder = [LDUserBuilder currentBuilder:self.user]; + LDUserModel *restoredUser = [userBuilder build]; + + restoredUser.flagConfig = [self.dataManager retrieveFlagConfigForUser:restoredUser]; + + NSArray *ignoredAttributes = @[kUserAttributeUpdatedAt, kUserAttributeConfig]; + XCTAssertTrue([restoredUser isEqual:self.user ignoringAttributes:ignoredAttributes]); + XCTAssertTrue(restoredUser.flagConfig.isEmpty); +} + +-(void)testRetrieveFlagConfigForUser_userNotCached_missingFlagConfig { + LDUserModel *userWithoutFlagConfig = [self.user copy]; + userWithoutFlagConfig.flagConfig = nil; + + LDFlagConfigModel *flagConfig = [self.dataManager retrieveFlagConfigForUser:userWithoutFlagConfig]; + + XCTAssertNotNil(flagConfig); + XCTAssertTrue([flagConfig isEmpty]); + XCTAssertFalse([flagConfig isEqualToConfig:self.user.flagConfig]); +} + +-(void)testRetrieveFlagConfigForUser_missingUser { + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:kUserCacheSize]; + NSDictionary *userEnvironments = [LDUserEnvironment stubUserEnvironmentsForUsersWithKeys:userKeys]; + [LDDataManager saveUserEnvironments:userEnvironments]; + LDUserModel *missingUser; + + LDFlagConfigModel *flagConfig = [self.dataManager retrieveFlagConfigForUser:missingUser]; + + XCTAssertTrue(flagConfig.isEmpty); +} + +-(void)testSaveAndRetrieveUserEnvironments { + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:kUserCacheSize]; + NSDictionary *originalUserEnvironments = [LDUserEnvironment stubUserEnvironmentsForUsersWithKeys:userKeys]; + + [self.dataManager saveUserEnvironments:originalUserEnvironments]; + NSDictionary *retrievedUserEnvironments = [self.dataManager retrieveUserEnvironments]; + + XCTAssertEqual(retrievedUserEnvironments.count, kUserCacheSize); + for (NSString *userKey in userKeys) { + LDUserEnvironment *originalUserEnvironment = originalUserEnvironments[userKey]; + LDUserEnvironment *retrievedUserEnvironment = retrievedUserEnvironments[userKey]; + XCTAssertTrue([retrievedUserEnvironment isEqualToUserEnvironment:originalUserEnvironment]); + } +} + +-(void)testSaveUserEnvironments_invalidEnvironment { + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:kUserCacheSize - 1]; + NSMutableDictionary *originalUserEnvironments = [NSMutableDictionary dictionaryWithDictionary:[LDUserEnvironment stubUserEnvironmentsForUsersWithKeys:userKeys]]; + NSString *invalidEnvironmentKey = [[NSUUID UUID] UUIDString]; + originalUserEnvironments[invalidEnvironmentKey] = @"Bad Environment"; + + [self.dataManager saveUserEnvironments:originalUserEnvironments]; + + NSDictionary *retrievedUserEnvironments = [self.dataManager retrieveUserEnvironments]; + XCTAssertNil(retrievedUserEnvironments[invalidEnvironmentKey]); + XCTAssertEqual(retrievedUserEnvironments.count, kUserCacheSize - 1); + for (NSString *userKey in userKeys) { + LDUserEnvironment *originalUserEnvironment = originalUserEnvironments[userKey]; + LDUserEnvironment *retrievedUserEnvironment = retrievedUserEnvironments[userKey]; + XCTAssertTrue([retrievedUserEnvironment isEqualToUserEnvironment:originalUserEnvironment]); + } +} + +-(void)testRetrieveUserEnvironments_invalidEnvironmentDictionary { + NSArray *userKeys = [LDUserModel stubUserKeysWithCount:kUserCacheSize - 1]; + NSDictionary *originalUserEnvironments = [LDUserEnvironment stubUserEnvironmentsForUsersWithKeys:userKeys]; + [self.dataManager saveUserEnvironments:originalUserEnvironments]; + NSString *invalidEnvironmentDictionaryKey = [[NSUUID UUID] UUIDString]; + NSMutableDictionary *userEnvironmentDictionaries = [NSMutableDictionary dictionaryWithDictionary:[[NSUserDefaults standardUserDefaults] objectForKey:kUserDefaultsKeyUserEnvironments]]; + userEnvironmentDictionaries[invalidEnvironmentDictionaryKey] = @"Bad Environment Dictionary"; + [[NSUserDefaults standardUserDefaults] setObject:[userEnvironmentDictionaries copy] forKey:kUserDefaultsKeyUserEnvironments]; + + NSDictionary *retrievedUserEnvironments = [self.dataManager retrieveUserEnvironments]; + + XCTAssertEqual(retrievedUserEnvironments.count, kUserCacheSize - 1); + XCTAssertNil(retrievedUserEnvironments[invalidEnvironmentDictionaryKey]); + for (NSString *userKey in userKeys) { + LDUserEnvironment *originalUserEnvironment = originalUserEnvironments[userKey]; + LDUserEnvironment *retrievedUserEnvironment = retrievedUserEnvironments[userKey]; + XCTAssertTrue([retrievedUserEnvironment isEqualToUserEnvironment:originalUserEnvironment]); + } +} + +-(void)testRecordFlagEvaluationEvents { id trackerMock = OCMClassMock([LDFlagConfigTracker class]); self.user = [LDUserModel stubWithKey:nil usingTracker:trackerMock eventTrackingContext:nil]; for (NSString *flagKey in [LDFlagConfigValue flagKeys]) { NSArray *flagConfigValues = [LDFlagConfigValue stubFlagConfigValuesForFlagKey:flagKey]; id defaultFlagValue = [LDFlagConfigValue defaultValueForFlagKey:flagKey]; for (LDFlagConfigValue *flagConfigValue in flagConfigValues) { - [[LDDataManager sharedManager] flushEventsDictionary]; + [self.dataManager discardEventsDictionary]; - XCTestExpectation *eventsExpectation = [self expectationWithDescription:@"LDDataManagerTest.testCreateFlagEvaluationEvents.allEvents"]; + XCTestExpectation *eventsExpectation = [self expectationWithDescription:@"LDDataManagerTest.testRecordFlagEvaluationEvents.allEvents"]; [[trackerMock expect] logRequestForFlagKey:flagKey reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultValue:defaultFlagValue]; - [[LDDataManager sharedManager] createFlagEvaluationEventsWithFlagKey:flagKey + [self.dataManager recordFlagEvaluationEventsWithFlagKey:flagKey reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultFlagValue:defaultFlagValue - user:self.user - config:self.config]; + user:self.user]; - [[LDDataManager sharedManager] allEventDictionaries:^(NSArray *eventDictionaries) { + [self.dataManager allEventDictionaries:^(NSArray *eventDictionaries) { XCTAssertEqual(eventDictionaries.count, 2); for (NSString *eventKind in @[kEventModelKindFeature, kEventModelKindDebug]) { NSPredicate *eventPredicate = [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary * _Nullable bindings) { @@ -148,33 +694,33 @@ -(void)testCreateFlagEvaluationEvents { } } --(void)testCreateSummaryEvent_noCounters { +-(void)testRecordSummaryEvent_noCounters { LDFlagConfigTracker *trackerStub = [LDFlagConfigTracker tracker]; XCTestExpectation *expectation = [self expectationWithDescription:@"All events dictionary expectation"]; - [[LDDataManager sharedManager] createSummaryEventWithTracker:trackerStub config:self.config]; + [self.dataManager recordSummaryEventWithTracker:trackerStub]; - [[LDDataManager sharedManager] allEventDictionaries:^(NSArray *eventDictionaries) { + [self.dataManager allEventDictionaries:^(NSArray *eventDictionaries) { XCTAssertEqual(eventDictionaries.count, 0); [expectation fulfill]; }]; [self waitForExpectations:@[expectation] timeout:1]; } --(void)testCreateSummaryEvent_nilTracker { +-(void)testRecordSummaryEvent_nilTracker { LDFlagConfigTracker *tracker = nil; - XCTestExpectation *expectation = [self expectationWithDescription:@"LDDataManagerTest.testCreateSummaryEvent_nilTracker.allEvents"]; + XCTestExpectation *expectation = [self expectationWithDescription:@"LDDataManagerTest.testRecordSummaryEvent_nilTracker.allEvents"]; - [[LDDataManager sharedManager] createSummaryEventWithTracker:tracker config:self.config]; + [self.dataManager recordSummaryEventWithTracker:tracker]; - [[LDDataManager sharedManager] allEventDictionaries:^(NSArray *eventDictionaries) { + [self.dataManager allEventDictionaries:^(NSArray *eventDictionaries) { XCTAssertEqual(eventDictionaries.count, 0); [expectation fulfill]; }]; [self waitForExpectations:@[expectation] timeout:1]; } --(void)testCreateFeatureEvent_trackEvents_YES { +-(void)testRecordFeatureEvent_trackEvents_YES { LDFlagConfigValue *flagConfigValue = [self setupCreateFeatureEventTestWithTrackEvents:YES]; [[self.eventModelMock expect] featureEventWithFlagKey:kFlagKeyIsABawler reportedFlagValue:flagConfigValue.value @@ -183,31 +729,29 @@ -(void)testCreateFeatureEvent_trackEvents_YES { user:self.user inlineUser:NO]; - [[LDDataManager sharedManager] createFeatureEventWithFlagKey:kFlagKeyIsABawler + [self.dataManager recordFeatureEventWithFlagKey:kFlagKeyIsABawler reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultFlagValue:@(NO) - user:self.user - config:self.config]; + user:self.user]; [self.eventModelMock verify]; } --(void)testCreateFeatureEvent_trackEvents_NO { +-(void)testRecordFeatureEvent_trackEvents_NO { LDFlagConfigValue *flagConfigValue = [self setupCreateFeatureEventTestWithTrackEvents:NO]; [[self.eventModelMock reject] featureEventWithFlagKey:[OCMArg any] reportedFlagValue:[OCMArg any] flagConfigValue:[OCMArg any] defaultFlagValue:[OCMArg any] user:[OCMArg any] inlineUser:[OCMArg any]]; - [[LDDataManager sharedManager] createFeatureEventWithFlagKey:kFlagKeyIsABawler + [self.dataManager recordFeatureEventWithFlagKey:kFlagKeyIsABawler reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultFlagValue:@(NO) - user:self.user - config:self.config]; + user:self.user]; [self.eventModelMock verify]; } --(void)testCreateFeatureEvent_eventTrackingContext_nil { +-(void)testRecordFeatureEvent_eventTrackingContext_nil { LDFlagConfigValue *flagConfigValue = [self setupCreateFeatureEventTestWithTrackEvents:YES includeTrackingContext:NO]; [[self.eventModelMock reject] featureEventWithFlagKey:[OCMArg any] reportedFlagValue:[OCMArg any] @@ -216,161 +760,150 @@ -(void)testCreateFeatureEvent_eventTrackingContext_nil { user:[OCMArg any] inlineUser:[OCMArg any]]; - [[LDDataManager sharedManager] createFeatureEventWithFlagKey:kFlagKeyIsABawler + [self.dataManager recordFeatureEventWithFlagKey:kFlagKeyIsABawler reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultFlagValue:@(NO) - user:self.user - config:self.config]; + user:self.user]; [self.eventModelMock verify]; } --(void)testCreateDebugEvent_lastEventResponseDate_systemDate_debugEventsUntilDate_createEvent { +-(void)testRecordDebugEvent_lastEventResponseDate_systemDate_debugEventsUntilDate_createEvent { //lastEventResponseDate < systemDate < debugEventsUntilDate create event LDFlagConfigValue *flagConfigValue = [self setupCreateDebugEventTestWithLastEventResponseDate:[NSDate dateWithTimeIntervalSinceNow:-1.0] debugUntil:[NSDate dateWithTimeIntervalSinceNow:1.0]]; [[self.eventModelMock expect] debugEventWithFlagKey:kFlagKeyIsABawler reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultFlagValue:@(NO) user:self.user]; - [[LDDataManager sharedManager] createDebugEventWithFlagKey:kFlagKeyIsABawler + [self.dataManager recordDebugEventWithFlagKey:kFlagKeyIsABawler reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultFlagValue:@(NO) - user:self.user - config:self.config]; + user:self.user]; [self.eventModelMock verify]; } --(void)testCreateDebugEvent_systemDate_lastEventResponseDate_debugEventsUntilDate_createEvent { +-(void)testRecordDebugEvent_systemDate_lastEventResponseDate_debugEventsUntilDate_createEvent { //systemDate < lastEventResponseDate < debugEventsUntilDate create event //system time not right, set too far in the past, but lastEventResponse hasn't reached debug LDFlagConfigValue *flagConfigValue = [self setupCreateDebugEventTestWithLastEventResponseDate:[NSDate dateWithTimeIntervalSinceNow:1.0] debugUntil:[NSDate dateWithTimeIntervalSinceNow:2.0]]; [[self.eventModelMock expect] debugEventWithFlagKey:kFlagKeyIsABawler reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultFlagValue:@(NO) user:self.user]; - [[LDDataManager sharedManager] createDebugEventWithFlagKey:kFlagKeyIsABawler + [self.dataManager recordDebugEventWithFlagKey:kFlagKeyIsABawler reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultFlagValue:@(NO) - user:self.user - config:self.config]; + user:self.user]; [self.eventModelMock verify]; } --(void)testCreateDebugEvent_lastEventResponseDate_debugEventsUntilDate_systemDate_dontCreateEvent { +-(void)testRecordDebugEvent_lastEventResponseDate_debugEventsUntilDate_systemDate_dontCreateEvent { //lastEventResponseDate < debugEventsUntilDate < systemDate no event LDFlagConfigValue *flagConfigValue = [self setupCreateDebugEventTestWithLastEventResponseDate:[NSDate dateWithTimeIntervalSinceNow:-1.0] debugUntil:[NSDate date]]; [[self.eventModelMock reject] debugEventWithFlagKey:[OCMArg any] reportedFlagValue:[OCMArg any] flagConfigValue:[OCMArg any] defaultFlagValue:[OCMArg any] user:[OCMArg any]]; - [[LDDataManager sharedManager] createDebugEventWithFlagKey:kFlagKeyIsABawler + [self.dataManager recordDebugEventWithFlagKey:kFlagKeyIsABawler reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultFlagValue:@(NO) - user:self.user - config:self.config]; + user:self.user]; [self.eventModelMock verify]; } --(void)testCreateDebugEvent_debugEventsUntilDate_lastEventResponseDate_systemDate_dontCreateEvent { +-(void)testRecordDebugEvent_debugEventsUntilDate_lastEventResponseDate_systemDate_dontCreateEvent { //debugEventsUntilDate < lastEventResponseDate < systemDate no event LDFlagConfigValue *flagConfigValue = [self setupCreateDebugEventTestWithLastEventResponseDate:[NSDate dateWithTimeIntervalSinceNow:-1.0] debugUntil:[NSDate dateWithTimeIntervalSinceNow:-2.0]]; [[self.eventModelMock reject] debugEventWithFlagKey:[OCMArg any] reportedFlagValue:[OCMArg any] flagConfigValue:[OCMArg any] defaultFlagValue:[OCMArg any] user:[OCMArg any]]; - [[LDDataManager sharedManager] createDebugEventWithFlagKey:kFlagKeyIsABawler + [self.dataManager recordDebugEventWithFlagKey:kFlagKeyIsABawler reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultFlagValue:@(NO) - user:self.user - config:self.config]; + user:self.user]; [self.eventModelMock verify]; } --(void)testCreateDebugEvent_debugEventsUntilDate_systemDate_lastEventResponseDate_dontCreateEvent { +-(void)testRecordDebugEvent_debugEventsUntilDate_systemDate_lastEventResponseDate_dontCreateEvent { //debugEventsUntilDate < systemDate < lastEventResponseDate no event LDFlagConfigValue *flagConfigValue = [self setupCreateDebugEventTestWithLastEventResponseDate:[NSDate dateWithTimeIntervalSinceNow:1.0] debugUntil:[NSDate dateWithTimeIntervalSinceNow:-1.0]]; [[self.eventModelMock reject] debugEventWithFlagKey:[OCMArg any] reportedFlagValue:[OCMArg any] flagConfigValue:[OCMArg any] defaultFlagValue:[OCMArg any] user:[OCMArg any]]; - [[LDDataManager sharedManager] createDebugEventWithFlagKey:kFlagKeyIsABawler + [self.dataManager recordDebugEventWithFlagKey:kFlagKeyIsABawler reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultFlagValue:@(NO) - user:self.user - config:self.config]; + user:self.user]; [self.eventModelMock verify]; } --(void)testCreateDebugEvent_systemDate_debugEventsUntilDate_lastEventResponseDate_dontCreateEvent { +-(void)testRecordDebugEvent_systemDate_debugEventsUntilDate_lastEventResponseDate_dontCreateEvent { //systemDate < debugEventsUntilDate < lastEventResponseDate no event //system time not right, set too far in the past, lastEventResponse past debug LDFlagConfigValue *flagConfigValue = [self setupCreateDebugEventTestWithLastEventResponseDate:[NSDate dateWithTimeIntervalSinceNow:2.0] debugUntil:[NSDate dateWithTimeIntervalSinceNow:1.0]]; [[self.eventModelMock reject] debugEventWithFlagKey:[OCMArg any] reportedFlagValue:[OCMArg any] flagConfigValue:[OCMArg any] defaultFlagValue:[OCMArg any] user:[OCMArg any]]; - [[LDDataManager sharedManager] createDebugEventWithFlagKey:kFlagKeyIsABawler + [self.dataManager recordDebugEventWithFlagKey:kFlagKeyIsABawler reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultFlagValue:@(NO) - user:self.user - config:self.config]; + user:self.user]; [self.eventModelMock verify]; } --(void)testCreateDebugEvent_missingDebugEventsUntilDate_dontCreateEvent { +-(void)testRecordDebugEvent_missingDebugEventsUntilDate_dontCreateEvent { LDFlagConfigValue *flagConfigValue = [self setupCreateDebugEventTestWithLastEventResponseDate:[NSDate dateWithTimeIntervalSinceNow:-1.0] debugUntil:nil]; [[self.eventModelMock reject] debugEventWithFlagKey:[OCMArg any] reportedFlagValue:[OCMArg any] flagConfigValue:[OCMArg any] defaultFlagValue:[OCMArg any] user:[OCMArg any]]; - [[LDDataManager sharedManager] createDebugEventWithFlagKey:kFlagKeyIsABawler + [self.dataManager recordDebugEventWithFlagKey:kFlagKeyIsABawler reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultFlagValue:@(NO) - user:self.user - config:self.config]; + user:self.user]; [self.eventModelMock verify]; } --(void)testCreateDebugEvent_missingEventTrackingContext_dontCreateEvent { +-(void)testRecordDebugEvent_missingEventTrackingContext_dontCreateEvent { LDFlagConfigValue *flagConfigValue = [self setupCreateDebugEventTestWithLastEventResponseDate:[NSDate dateWithTimeIntervalSinceNow:-1.0] debugUntil:nil includeTrackingContext:NO]; [[self.eventModelMock reject] debugEventWithFlagKey:[OCMArg any] reportedFlagValue:[OCMArg any] flagConfigValue:[OCMArg any] defaultFlagValue:[OCMArg any] user:[OCMArg any]]; - [[LDDataManager sharedManager] createDebugEventWithFlagKey:kFlagKeyIsABawler + [self.dataManager recordDebugEventWithFlagKey:kFlagKeyIsABawler reportedFlagValue:flagConfigValue.value flagConfigValue:flagConfigValue defaultFlagValue:@(NO) - user:self.user - config:self.config]; + user:self.user]; [self.eventModelMock verify]; } -(void)testAllEventsDictionaryArray { LDEventModel *featureEvent = [LDEventModel stubEventWithKind:kEventModelKindFeature user:self.user config:self.config]; - [[LDDataManager sharedManager] createFeatureEventWithFlagKey:featureEvent.key + [self.dataManager recordFeatureEventWithFlagKey:featureEvent.key reportedFlagValue:featureEvent.reportedValue flagConfigValue:featureEvent.flagConfigValue defaultFlagValue:featureEvent.defaultValue - user:self.user - config:self.config]; + user:self.user]; LDEventModel *customEvent = [LDEventModel stubEventWithKind:kEventModelKindCustom user:self.user config:self.config]; - [[LDDataManager sharedManager] createCustomEventWithKey:customEvent.key customData:customEvent.data user:self.user config:self.config]; + [self.dataManager recordCustomEventWithKey:customEvent.key customData:customEvent.data user:self.user]; LDEventModel *identifyEvent = [LDEventModel stubEventWithKind:kEventModelKindIdentify user:self.user config:self.config]; - [[LDDataManager sharedManager] createIdentifyEventWithUser:self.user config:self.config]; + [self.dataManager recordIdentifyEventWithUser:self.user]; LDFlagConfigTracker *trackerStub = [LDFlagConfigTracker stubTracker]; LDEventModel *summaryEvent = [LDEventModel summaryEventWithTracker:trackerStub]; - [[LDDataManager sharedManager] createSummaryEventWithTracker:trackerStub config:self.config]; + [self.dataManager recordSummaryEventWithTracker:trackerStub]; LDEventModel *debugEvent = [LDEventModel stubEventWithKind:kEventModelKindDebug user:self.user config:self.config]; - [[LDDataManager sharedManager] createDebugEventWithFlagKey:debugEvent.key + [self.dataManager recordDebugEventWithFlagKey:debugEvent.key reportedFlagValue:debugEvent.reportedValue flagConfigValue:debugEvent.flagConfigValue defaultFlagValue:debugEvent.defaultValue - user:self.user - config:self.config]; + user:self.user]; NSArray *eventStubs = @[featureEvent, customEvent, identifyEvent, summaryEvent, debugEvent]; XCTestExpectation *expectation = [self expectationWithDescription:@"All events dictionary expectation"]; - [[LDDataManager sharedManager] allEventDictionaries:^(NSArray *eventDictionaries) { + [self.dataManager allEventDictionaries:^(NSArray *eventDictionaries) { for (LDEventModel *event in eventStubs) { NSDictionary *eventDictionary = [eventDictionaries dictionaryForEvent:event]; @@ -416,89 +949,59 @@ -(void)testAllEventsDictionaryArray { [self waitForExpectations:@[expectation] timeout:1]; } --(void)testSaveAndFindUser { - XCTestExpectation *userSavedExpectation = [self expectationWithDescription:@"LDDataManagerTest.testSaveAndFindUser.userSavedExpectation"]; - - [[LDDataManager sharedManager] saveUser:self.user asDict:YES completion:^{ - [userSavedExpectation fulfill]; - }]; - [self waitForExpectations:@[userSavedExpectation] timeout:1.0]; - LDUserModel *foundUser = [[LDDataManager sharedManager] findUserWithkey:self.user.key]; - - XCTAssertNotNil(foundUser); - XCTAssertTrue([foundUser isEqual:self.user ignoringAttributes:@[kUserAttributeUpdatedAt]]); -} - --(void)testSaveAndFindUser_backwardCompatibility { - XCTestExpectation *userSavedExpectation = [self expectationWithDescription:@"LDDataManagerTest.testSaveAndFindUser.userSavedExpectation"]; +-(void)testRecordEventAfterCapacityReached { + self.config.capacity = @(2); + XCTestExpectation *expectation = [self expectationWithDescription:@"All events dictionary expectation"]; + [self.dataManager.eventsArray removeAllObjects]; + [self.dataManager recordCustomEventWithKey:@"aKey" customData: @{@"carrot": @"cake"} user:self.user]; + [self.dataManager recordCustomEventWithKey:@"aKey" customData: @{@"carrot": @"cake"} user:self.user]; + [self.dataManager recordCustomEventWithKey:@"aKey" customData: @{@"carrot": @"cake"} user:self.user]; + LDFlagConfigValue *flagConfigValue = [LDFlagConfigValue flagConfigValueFromJsonFileNamed:@"boolConfigIsABool-true" + flagKey:kLDFlagKeyIsABool + eventTrackingContext:[LDEventTrackingContext stub]]; + [self.dataManager recordFeatureEventWithFlagKey: @"anotherKey" + reportedFlagValue:flagConfigValue.value + flagConfigValue:flagConfigValue + defaultFlagValue:@(NO) + user:self.user]; - [[LDDataManager sharedManager] saveUser:self.user asDict:NO completion:^{ - [userSavedExpectation fulfill]; + [self.dataManager allEventDictionaries:^(NSArray *array) { + XCTAssertEqual([array count],2); + [expectation fulfill]; }]; - [self waitForExpectations:@[userSavedExpectation] timeout:1.0]; - LDUserModel *foundUser = [[LDDataManager sharedManager] findUserWithkey:self.user.key]; - XCTAssertNotNil(foundUser); - XCTAssertTrue([foundUser isEqual:self.user ignoringAttributes:@[kUserAttributeUpdatedAt]]); + [self waitForExpectations:@[expectation] timeout:1.0]; } --(void)testSaveAndFindUsers_overCapacity { - NSMutableArray *users = [NSMutableArray arrayWithCapacity:kUserCacheSize + 3]; - for(int index = 0; index < kUserCacheSize + 3; index++) { - LDUserModel *user = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; - //Keep users ordered most recent first - if (users.count == 0) { - [users addObject:user]; - } else { - [users insertObject:user atIndex:0]; - } - XCTestExpectation *userSavedExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"LDDataManagerTest.testSaveAndFindUser.userSavedExpectation.%d", index]]; +-(void)testDeleteProcessedEvents { + NSArray *events = [LDEventModel stubEventDictionariesForUser:self.user config:self.config]; + for (NSInteger processedCount = 0; processedCount <= events.count; processedCount++) { + NSArray *processedEvents = [events subarrayWithRange:NSMakeRange(0, processedCount)]; + NSArray *unprocessedEvents = [events subarrayWithRange:NSMakeRange(processedCount, events.count - processedCount)]; + self.dataManager.eventsArray = [NSMutableArray arrayWithArray:events]; - [[LDDataManager sharedManager] saveUser:user asDict:YES completion:^{ - [userSavedExpectation fulfill]; - }]; + [self.dataManager deleteProcessedEvents:processedEvents]; //asynchronous, but without a completion block - [self waitForExpectations:@[userSavedExpectation] timeout:1.0]; - for(int usersIndex = 0; usersIndex < users.count; usersIndex++) { - LDUserModel *targetUser = users[usersIndex]; - LDUserModel *foundUser = [[LDDataManager sharedManager] findUserWithkey:targetUser.key]; - if (usersIndex < kUserCacheSize) { - XCTAssertNotNil(foundUser); - XCTAssertTrue([foundUser isEqual:targetUser ignoringAttributes:@[kUserAttributeUpdatedAt]]); - } else { - XCTAssertNil(foundUser); - } - } + XCTestExpectation *allEventDictionaryExpectation = [self expectationWithDescription:[NSString stringWithFormat:@"testDeleteProcessedEvents-%ld events", processedCount]]; + [self.dataManager allEventDictionaries:^(NSArray *eventDictionaries) { + XCTAssertEqualObjects(eventDictionaries, unprocessedEvents); + [allEventDictionaryExpectation fulfill]; + }]; + [self waitForExpectations:@[allEventDictionaryExpectation] timeout:1.0]; } } --(void)testCreateEventAfterCapacityReached { - self.config.capacity = @(2); +-(void)testDeleteProcessedEvents_nilProcessedJsonArray { + NSArray *events = [LDEventModel stubEventDictionariesForUser:self.user config:self.config]; + self.dataManager.eventsArray = [NSMutableArray arrayWithArray:events]; - XCTestExpectation *expectation = [self expectationWithDescription:@"All events dictionary expectation"]; + [self.dataManager deleteProcessedEvents:nil]; //asynchronous, but without a completion block - LDDataManager *manager = [LDDataManager sharedManager]; - [manager.eventsArray removeAllObjects]; - - [manager createCustomEventWithKey:@"aKey" customData: @{@"carrot": @"cake"} user:self.user config:self.config]; - [manager createCustomEventWithKey:@"aKey" customData: @{@"carrot": @"cake"} user:self.user config:self.config]; - [manager createCustomEventWithKey:@"aKey" customData: @{@"carrot": @"cake"} user:self.user config:self.config]; - LDFlagConfigValue *flagConfigValue = [LDFlagConfigValue flagConfigValueFromJsonFileNamed:@"boolConfigIsABool-true" - flagKey:kLDFlagKeyIsABool - eventTrackingContext:[LDEventTrackingContext stub]]; - [manager createFeatureEventWithFlagKey: @"anotherKey" - reportedFlagValue:flagConfigValue.value - flagConfigValue:flagConfigValue - defaultFlagValue:@(NO) - user:self.user - config:self.config]; - - [manager allEventDictionaries:^(NSArray *array) { - XCTAssertEqual([array count],2); - [expectation fulfill]; + XCTestExpectation *allEventDictionaryExpectation = [self expectationWithDescription:@"testDeleteProcessedEvents_nilProcessedJsonArray"]; + [self.dataManager allEventDictionaries:^(NSArray *eventDictionaries) { + XCTAssertEqualObjects(eventDictionaries, events); + [allEventDictionaryExpectation fulfill]; }]; - - [self waitForExpectations:@[expectation] timeout:1.0]; + [self waitForExpectations:@[allEventDictionaryExpectation] timeout:1.0]; } - @end diff --git a/DarklyTests/Models/LDFlagConfig/LDFlagConfigModelTest.m b/DarklyTests/Models/LDFlagConfig/LDFlagConfigModelTest.m index bc5c8d3b..d4303007 100644 --- a/DarklyTests/Models/LDFlagConfig/LDFlagConfigModelTest.m +++ b/DarklyTests/Models/LDFlagConfig/LDFlagConfigModelTest.m @@ -99,6 +99,20 @@ -(void)testDictionaryValue_nonConfigValues { XCTAssertTrue(flagConfigModelDictionary.count == 0); } +-(void)testAllFlagValues { + LDFlagConfigModel *flagConfigModel = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"featureFlags"]; + + NSDictionary *allFlags = flagConfigModel.allFlagValues; + + for (NSString *flagKey in flagConfigModel.featuresJsonDictionary) { + if ([flagKey isEqualToString:@"isConnected"]) { + XCTAssertNil(allFlags[flagKey]); + } else { + XCTAssertEqualObjects(allFlags[flagKey], [flagConfigModel flagValueForFlagKey:flagKey]); + } + } +} + -(void)testFlagConfigValueForFlagKey { LDFlagConfigModel *subject = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"featureFlags"]; NSDictionary *flagValues = [NSJSONSerialization jsonObjectFromFileNamed:@"featureFlags"]; @@ -139,15 +153,15 @@ -(void)testFlagVersionForFlagKey { XCTAssertTrue([config flagModelVersionForFlagKey:@"someMissingKey"] == kLDFlagConfigValueItemDoesNotExist); } -- (void)testDoesFlagConfigValueExistForFlagKey { +- (void)testContainsFlagKey { LDFlagConfigModel *config = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"featureFlags"]; NSDictionary *flagValues = [NSJSONSerialization jsonObjectFromFileNamed:@"featureFlags"]; for (NSString *key in [flagValues.allKeys copy]) { - XCTAssertTrue([config doesFlagConfigValueExistForFlagKey:key]); + XCTAssertTrue([config containsFlagKey:key]); } - XCTAssertFalse([config doesFlagConfigValueExistForFlagKey:@"someMissingKey"]); + XCTAssertFalse([config containsFlagKey:@"someMissingKey"]); } - (void)testAddOrReplaceFromDictionaryWhenKeyDoesntExist { @@ -158,7 +172,7 @@ - (void)testAddOrReplaceFromDictionaryWhenKeyDoesntExist { [config addOrReplaceFromDictionary:patch]; - XCTAssertTrue([config doesFlagConfigValueExistForFlagKey:patchedFlagKey]); + XCTAssertTrue([config containsFlagKey:patchedFlagKey]); XCTAssertEqualObjects([config flagConfigValueForFlagKey:patchedFlagKey], flagConfigValue); } @@ -170,7 +184,7 @@ - (void)testAddOrReplaceFromDictionaryWhenKeyExistsWithPreviousVersion { [config addOrReplaceFromDictionary:patch]; - XCTAssertTrue([config doesFlagConfigValueExistForFlagKey:patchedFlagKey]); + XCTAssertTrue([config containsFlagKey:patchedFlagKey]); XCTAssertEqualObjects([config flagConfigValueForFlagKey:patchedFlagKey], flagConfigValue); } @@ -182,7 +196,7 @@ - (void)testAddOrReplaceFromDictionaryWhenPatchValueIsNull { [config addOrReplaceFromDictionary:patch]; - XCTAssertTrue([config doesFlagConfigValueExistForFlagKey:patchedFlagKey]); + XCTAssertTrue([config containsFlagKey:patchedFlagKey]); XCTAssertEqualObjects([config flagConfigValueForFlagKey:patchedFlagKey], flagConfigValue); } @@ -194,7 +208,7 @@ - (void)testAddOrReplaceFromDictionaryWhenKeyExistsWithSameVersion { [config addOrReplaceFromDictionary:patch]; - XCTAssertTrue([config doesFlagConfigValueExistForFlagKey:patchedFlagKey]); + XCTAssertTrue([config containsFlagKey:patchedFlagKey]); XCTAssertEqualObjects([config flagConfigValueForFlagKey:patchedFlagKey], originalFlagConfigValue); } @@ -206,7 +220,7 @@ - (void)testAddOrReplaceFromDictionaryWhenKeyExistsWithLaterVersion { [config addOrReplaceFromDictionary:patch]; - XCTAssertTrue([config doesFlagConfigValueExistForFlagKey:patchedFlagKey]); + XCTAssertTrue([config containsFlagKey:patchedFlagKey]); XCTAssertEqualObjects([config flagConfigValueForFlagKey:patchedFlagKey], originalFlagConfigValue); } @@ -283,7 +297,7 @@ - (void)testDeleteFromDictionaryWhenKeyExistsWithPreviousVersion { [config deleteFromDictionary:delete]; - XCTAssertFalse([config doesFlagConfigValueExistForFlagKey:deletedFlagKey]); + XCTAssertFalse([config containsFlagKey:deletedFlagKey]); } - (void)testDeleteFromDictionaryWhenKeyDoesntExist { @@ -457,6 +471,94 @@ - (void)testHasFeaturesEqualToDictionary { XCTAssertFalse([subject hasFeaturesEqualToDictionary:differentDictionary]); } +-(void)testDifferingFlagKeysFromConfig_matchingConfig { + LDFlagConfigModel *flagConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"featureFlags"]; + LDFlagConfigModel *otherConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"featureFlags"]; + + XCTAssertNil([flagConfig differingFlagKeysFromConfig:otherConfig]); +} + +-(void)testDifferingFlagKeysFromConfig_differentValues { + LDFlagConfigModel *flagConfig = [LDFlagConfigModel stub]; + NSMutableArray *flagKeysWithDifferentValues = [NSMutableArray arrayWithCapacity:[LDFlagConfigValue flagKeys].count]; + for (NSString *flagKeyWithDifferentValue in [LDFlagConfigValue flagKeys]) { + if ([flagKeyWithDifferentValue isEqualToString:kLDFlagKeyIsANull]) { continue; } //Null has no alternate value + [flagKeysWithDifferentValues addObject:flagKeyWithDifferentValue]; + LDFlagConfigModel *otherConfig = [LDFlagConfigModel stubWithAlternateValuesForFlagKeys:flagKeysWithDifferentValues]; + + XCTAssertEqualObjects([NSSet setWithArray:[flagConfig differingFlagKeysFromConfig:otherConfig]], [NSSet setWithArray:flagKeysWithDifferentValues]); + } + + //repeat varying the selector instead of the parameter + LDFlagConfigModel *otherConfig = [LDFlagConfigModel stub]; + [flagKeysWithDifferentValues removeAllObjects]; + for (NSString *flagKeyWithDifferentValue in [LDFlagConfigValue flagKeys]) { + if ([flagKeyWithDifferentValue isEqualToString:kLDFlagKeyIsANull]) { continue; } //Null has no alternate value + [flagKeysWithDifferentValues addObject:flagKeyWithDifferentValue]; + LDFlagConfigModel *flagConfig = [LDFlagConfigModel stubWithAlternateValuesForFlagKeys:flagKeysWithDifferentValues]; + + XCTAssertEqualObjects([NSSet setWithArray:[flagConfig differingFlagKeysFromConfig:otherConfig]], [NSSet setWithArray:flagKeysWithDifferentValues]); + } +} + +-(void)testDifferingFlagKeysFromConfig_omittedValues { + LDFlagConfigModel *flagConfig = [LDFlagConfigModel stub]; + NSMutableArray *omittedFlagKeys = [NSMutableArray arrayWithCapacity:[LDFlagConfigValue flagKeys].count]; + for (NSString *omittedFlagKey in [LDFlagConfigValue flagKeys]) { + [omittedFlagKeys addObject:omittedFlagKey]; + LDFlagConfigModel *otherConfig = [LDFlagConfigModel stubOmittingFlagKeys:omittedFlagKeys]; + + XCTAssertEqualObjects([NSSet setWithArray:[flagConfig differingFlagKeysFromConfig:otherConfig]], [NSSet setWithArray:omittedFlagKeys]); + } + + //repeat varying the selector instead of the parameter + LDFlagConfigModel *otherConfig = [LDFlagConfigModel stub]; + [omittedFlagKeys removeAllObjects]; + for (NSString *omittedFlagKey in [LDFlagConfigValue flagKeys]) { + [omittedFlagKeys addObject:omittedFlagKey]; + LDFlagConfigModel *flagConfig = [LDFlagConfigModel stubOmittingFlagKeys:omittedFlagKeys]; + + XCTAssertEqualObjects([NSSet setWithArray:[flagConfig differingFlagKeysFromConfig:otherConfig]], [NSSet setWithArray:omittedFlagKeys]); + } +} + +-(void)testDifferingFlagKeysFromConfig_addedValue { + LDFlagConfigModel *flagConfig = [LDFlagConfigModel stub]; + LDFlagConfigModel *otherConfig = [LDFlagConfigModel stub]; + LDFlagConfigValue *addedFlagConfigValue = [LDFlagConfigValue stubForFlagKey:kLDFlagKeyIsABool]; + NSString *addedFlagKey = [NSUUID UUID].UUIDString; + [otherConfig setFlagConfigValue:addedFlagConfigValue forKey:addedFlagKey]; + + XCTAssertEqualObjects([flagConfig differingFlagKeysFromConfig:otherConfig], @[addedFlagKey]); + + //repeat adding the flagConfigValue to otherConfig + flagConfig = [LDFlagConfigModel stub]; + otherConfig = [LDFlagConfigModel stub]; + [flagConfig setFlagConfigValue:addedFlagConfigValue forKey:addedFlagKey]; + + XCTAssertEqualObjects([flagConfig differingFlagKeysFromConfig:otherConfig], @[addedFlagKey]); +} + +-(void)testDifferingFlagKeysFromConfig_emptyConfig { + LDFlagConfigModel *flagConfig = [LDFlagConfigModel stub]; + LDFlagConfigModel *otherConfig = [[LDFlagConfigModel alloc] init]; + + XCTAssertEqualObjects([NSSet setWithArray:[flagConfig differingFlagKeysFromConfig:otherConfig]], [NSSet setWithArray:[LDFlagConfigValue flagKeys]]); + + //repeat making flagConfig empty + flagConfig = [[LDFlagConfigModel alloc] init]; + otherConfig = [LDFlagConfigModel stub]; + + XCTAssertEqualObjects([NSSet setWithArray:[flagConfig differingFlagKeysFromConfig:otherConfig]], [NSSet setWithArray:[LDFlagConfigValue flagKeys]]); +} + +-(void)testDifferingFlagKeysFromConfig_missingConfig { + LDFlagConfigModel *flagConfig = [LDFlagConfigModel stub]; + LDFlagConfigModel *otherConfig = nil; + + XCTAssertEqualObjects([NSSet setWithArray:[flagConfig differingFlagKeysFromConfig:otherConfig]], [NSSet setWithArray:[LDFlagConfigValue flagKeys]]); +} + -(void)testUpdateEventTrackingContextFromConfig { LDEventTrackingContext *eventTrackingContext = [LDEventTrackingContext contextWithTrackEvents:NO debugEventsUntilDate:nil]; LDFlagConfigModel *subject = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"featureFlags" eventTrackingContext:eventTrackingContext omitKey:nil]; @@ -470,4 +572,16 @@ -(void)testUpdateEventTrackingContextFromConfig { XCTAssertEqualObjects(flagConfigValue.eventTrackingContext, updatedEventTrackingContext); } } + +-(void)testCopy { + LDFlagConfigModel *originalConfig = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"featureFlags"]; + + LDFlagConfigModel *copiedConfig = [originalConfig copy]; + + XCTAssertTrue([copiedConfig isEqualToConfig:originalConfig]); + XCTAssertFalse(copiedConfig == originalConfig); //copy is not the same object + for (NSString *flagKey in originalConfig.featuresJsonDictionary) { + XCTAssertFalse(originalConfig.featuresJsonDictionary[flagKey] == copiedConfig.featuresJsonDictionary[flagKey]); //flagConfigValue copy is not the same object as the original + } +} @end diff --git a/DarklyTests/Models/LDUserEnvironmentTest.m b/DarklyTests/Models/LDUserEnvironmentTest.m new file mode 100644 index 00000000..980f1f04 --- /dev/null +++ b/DarklyTests/Models/LDUserEnvironmentTest.m @@ -0,0 +1,250 @@ +// +// LDUserEnvironmentTest.m +// DarklyTests +// +// Created by Mark Pokorny on 10/12/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "DarklyXCTestCase.h" +#import "LDUserEnvironment.h" +#import "LDUserEnvironment+Testable.h" +#import "LDUserModel.h" +#import "LDUserModel+Testable.h" +#import "LDFlagConfigModel+Testable.h" +#import "LDEventTrackingContext.h" +#import "NSDictionary+Testable.h" +#import "NSDate+ReferencedDate.h" + +@interface LDUserEnvironmentTest : DarklyXCTestCase +@property (nonatomic, strong) NSString *userKey; +@property (nonatomic, strong) NSDictionary *users; +@property (nonatomic, strong) LDUserEnvironment *userEnvironment; +@end + +@implementation LDUserEnvironmentTest + +-(void)setUp { + [super setUp]; + + self.userKey = [[NSUUID UUID] UUIDString]; + self.users = [LDUserEnvironment stubUserModelsForUserWithKey:self.userKey environmentKeys:LDUserEnvironment.environmentKeys]; + + self.userEnvironment = [LDUserEnvironment userEnvironmentForUserWithKey:self.userKey environments:self.users]; +} + +-(void)tearDown { + [super tearDown]; +} + +-(void)testInitAndConstructor { + XCTAssertEqualObjects(self.userEnvironment.userKey, self.userKey); + XCTAssertTrue([self.userEnvironment.users isEqualToUserEnvironmentUsersDictionary:self.users]); +} + +-(void)testInitAndConstructor_mismatchedUserKey { + NSString *mismatchedKey = [[NSUUID UUID] UUIDString]; + self.userEnvironment = [LDUserEnvironment userEnvironmentForUserWithKey:mismatchedKey environments:self.users]; + + XCTAssertEqualObjects(self.userEnvironment.userKey, mismatchedKey); + XCTAssertNotNil(self.userEnvironment.users); + XCTAssertTrue(self.userEnvironment.users.count == 0); +} + +-(void)testInitAndConstructor_missingKey { + NSString *missingUserKey; + self.userEnvironment = [LDUserEnvironment userEnvironmentForUserWithKey:missingUserKey environments:self.users]; + + XCTAssertNil(self.userEnvironment); +} + +-(void)testInitAndConstructor_missingEnvironments { + self.userEnvironment = [LDUserEnvironment userEnvironmentForUserWithKey:self.userKey environments:nil]; + + XCTAssertEqualObjects(self.userEnvironment.userKey, self.userKey); + XCTAssertNotNil(self.userEnvironment.users); + XCTAssertTrue(self.userEnvironment.users.count == 0); +} + +-(void)testInitAndConstructor_invalidEnvironment { + NSMutableDictionary *environmentsWithInvalidItem = [NSMutableDictionary dictionaryWithDictionary:self.users]; + environmentsWithInvalidItem[@"invalidItemKey"] = @"invalid item"; + self.userEnvironment = [LDUserEnvironment userEnvironmentForUserWithKey:self.userKey environments:[environmentsWithInvalidItem copy]]; + + XCTAssertEqualObjects(self.userEnvironment.userKey, self.userKey); + XCTAssertTrue([self.userEnvironment.users isEqualToUserEnvironmentUsersDictionary:self.users]); +} + +-(void)testEncodeAndDecodeWithCoder { + NSData *userEnvironmentData = [NSKeyedArchiver archivedDataWithRootObject:self.userEnvironment]; + XCTAssertNotNil(userEnvironmentData); + + LDUserEnvironment *restoredUserEnvironment = [NSKeyedUnarchiver unarchiveObjectWithData:userEnvironmentData]; + XCTAssertTrue([restoredUserEnvironment isEqualToUserEnvironment:restoredUserEnvironment]); +} + +-(void)testEncodeAndDecodeWithCoder_missingKey { + self.userEnvironment.userKey = nil; + NSData *userEnvironmentData = [NSKeyedArchiver archivedDataWithRootObject:self.userEnvironment]; + XCTAssertNotNil(userEnvironmentData); + + LDUserEnvironment *restoredUserEnvironment = [NSKeyedUnarchiver unarchiveObjectWithData:userEnvironmentData]; + XCTAssertNil(restoredUserEnvironment); +} + +-(void)testEncodeAndDecodeWithCoder_missingEnvironments { + self.userEnvironment.users = nil; + NSData *userEnvironmentData = [NSKeyedArchiver archivedDataWithRootObject:self.userEnvironment]; + XCTAssertNotNil(userEnvironmentData); + + LDUserEnvironment *restoredUserEnvironment = [NSKeyedUnarchiver unarchiveObjectWithData:userEnvironmentData]; + XCTAssertEqualObjects(restoredUserEnvironment.userKey, self.userKey); + XCTAssertNotNil(restoredUserEnvironment.users); + XCTAssertTrue(restoredUserEnvironment.users.count == 0); +} + +-(void)testDictionaryValueAndInitWithDictionary { + NSDictionary *userEnvironmentDictionary = [self.userEnvironment dictionaryValue]; + + LDUserEnvironment *restoredUserEnvironment = [[LDUserEnvironment alloc] initWithDictionary:userEnvironmentDictionary]; + XCTAssertTrue([restoredUserEnvironment isEqualToUserEnvironment:restoredUserEnvironment]); +} + +-(void)testDictionaryValueAndInitWithDictionary_missingKey { + self.userEnvironment.userKey = nil; + NSDictionary *userEnvironmentDictionary = [self.userEnvironment dictionaryValue]; + + LDUserEnvironment *restoredUserEnvironment = [[LDUserEnvironment alloc] initWithDictionary:userEnvironmentDictionary]; + XCTAssertNil(restoredUserEnvironment); +} + +-(void)testDictionaryValueAndInitWithDictionary_missingUsers { + self.userEnvironment.users = nil; + NSDictionary *userEnvironmentDictionary = [self.userEnvironment dictionaryValue]; + + LDUserEnvironment *restoredUserEnvironment = [[LDUserEnvironment alloc] initWithDictionary:userEnvironmentDictionary]; + XCTAssertEqualObjects(restoredUserEnvironment.userKey, self.userKey); + XCTAssertNotNil(restoredUserEnvironment.users); + XCTAssertTrue(restoredUserEnvironment.users.count == 0); +} + +-(void)testLastUpdated { + for (LDUserModel *userInEnvironment in self.userEnvironment.users.allValues) { + //lastUpdated should be the latest updatedAt of all users. + XCTAssertTrue([self.userEnvironment.lastUpdated isLaterThan:userInEnvironment.updatedAt] || [self.userEnvironment.lastUpdated isEqualToDate:userInEnvironment.updatedAt]); + } +} + +-(void)testLastUpdated_singleEnvironment { + LDUserModel *user = self.users[kEnvironmentKeyPrimary]; + LDUserEnvironment *userEnvironment = [LDUserEnvironment userEnvironmentForUserWithKey:self.userKey environments:@{kEnvironmentKeyPrimary: user}]; + + XCTAssertTrue([userEnvironment.lastUpdated isEqualToDate:user.updatedAt]); +} + +-(void)testLastUpdated_missingEnvironments { + LDUserEnvironment *userEnvironment = [LDUserEnvironment userEnvironmentForUserWithKey:self.userKey environments:nil]; + + XCTAssertNil(userEnvironment.lastUpdated); +} + +-(void)testUserForMobileKey { + for (NSString *mobileKey in self.users.allKeys) { + LDUserModel *targetUser = self.users[mobileKey]; + + LDUserModel *foundUser = [self.userEnvironment userForMobileKey:mobileKey]; + + XCTAssertTrue([foundUser isEqual:targetUser ignoringAttributes:@[kUserAttributeUpdatedAt]]); + } +} + +-(void)testUserForMobileKey_mobileKeyNotFound { + LDUserModel *foundUser = [self.userEnvironment userForMobileKey:[[NSUUID UUID] UUIDString]]; + + XCTAssertNil(foundUser); +} + +-(void)testUserForMobileKey_missingMobileKey { + NSString *missingUserKey; + LDUserModel *foundUser = [self.userEnvironment userForMobileKey:missingUserKey]; + + XCTAssertNil(foundUser); +} + +-(void)testSetUserForMobileKey { + LDUserModel *newUser = [LDUserModel stubWithKey:self.userKey]; + NSString *newMobileKey = [[NSUUID UUID] UUIDString]; + + [self.userEnvironment setUser:newUser mobileKey:newMobileKey]; + + XCTAssertTrue([[self.userEnvironment userForMobileKey:newMobileKey] isEqual:newUser ignoringAttributes:@[kUserAttributeUpdatedAt]]); + for (NSString *mobileKey in self.users.allKeys) { + XCTAssertTrue([[self.userEnvironment userForMobileKey:mobileKey] isEqual:self.users[mobileKey] ignoringAttributes:@[kUserAttributeUpdatedAt]]); + } +} + +-(void)testSetUserForMobileKey_missingMobileKey { + LDUserModel *newUser = [LDUserModel stubWithKey:self.userKey]; + NSString *missingMobileKey; + + [self.userEnvironment setUser:newUser mobileKey:missingMobileKey]; + + XCTAssertNil([self.userEnvironment userForMobileKey:missingMobileKey]); + for (NSString *mobileKey in self.users.allKeys) { + XCTAssertTrue([[self.userEnvironment userForMobileKey:mobileKey] isEqual:self.users[mobileKey] ignoringAttributes:@[kUserAttributeUpdatedAt]]); + } +} + +-(void)testSetUserForMobileKey_mismatchedUserKey { + LDUserModel *newUser = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + NSString *newMobileKey = [[NSUUID UUID] UUIDString]; + + [self.userEnvironment setUser:newUser mobileKey:newMobileKey]; + + XCTAssertNil([self.userEnvironment userForMobileKey:newMobileKey]); + for (NSString *mobileKey in self.users.allKeys) { + XCTAssertTrue([[self.userEnvironment userForMobileKey:mobileKey] isEqual:self.users[mobileKey] ignoringAttributes:@[kUserAttributeUpdatedAt]]); + } +} + +-(void)testSetUserForMobileKey_missingUserKey { + LDUserModel *newUser = [LDUserModel stubWithKey:self.userKey]; + newUser.key = nil; + NSString *newMobileKey = [[NSUUID UUID] UUIDString]; + + [self.userEnvironment setUser:newUser mobileKey:newMobileKey]; + + XCTAssertNil([self.userEnvironment userForMobileKey:newMobileKey]); + for (NSString *mobileKey in self.users.allKeys) { + XCTAssertTrue([[self.userEnvironment userForMobileKey:mobileKey] isEqual:self.users[mobileKey] ignoringAttributes:@[kUserAttributeUpdatedAt]]); + } +} + +-(void)testRemoveUserForMobileKey { + NSUInteger remainingUserCount = self.users.count; + for (NSString *mobileKey in self.users.allKeys) { + [self.userEnvironment removeUserForMobileKey:mobileKey]; + remainingUserCount -= 1; + + XCTAssertNil([self.userEnvironment userForMobileKey:mobileKey]); + XCTAssertEqual(self.userEnvironment.users.count, remainingUserCount); + } +} + +-(void)testRemoveUserForMobileKey_missingMobileKey { + NSString *missingMobileKey; + + [self.userEnvironment removeUserForMobileKey:missingMobileKey]; + + XCTAssertEqual(self.userEnvironment.users.count, self.users.count); +} + +-(void)testRemoveUserForMobileKey_mobileKeyNotFound { + NSString *notFoundMobileKey = [[NSUUID UUID] UUIDString]; + + [self.userEnvironment removeUserForMobileKey:notFoundMobileKey]; + + XCTAssertEqual(self.userEnvironment.users.count, self.users.count); +} + +@end diff --git a/DarklyTests/Models/LDUserModelTest.m b/DarklyTests/Models/LDUserModelTest.m index 4104a63e..1d7cb8d9 100644 --- a/DarklyTests/Models/LDUserModelTest.m +++ b/DarklyTests/Models/LDUserModelTest.m @@ -17,6 +17,7 @@ #import "NSDate+Testable.h" #import "LDFlagConfigTracker.h" #import "LDFlagConfigTracker+Testable.h" +#import "NSDictionary+LaunchDarkly.h" @interface LDUserModelTest : XCTestCase @end @@ -489,6 +490,32 @@ - (void)testResetTracker { } } +-(void)testCopy { + LDUserModel *originalUser = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; //flagConfig includes a null feature flag + originalUser.privateAttributes = [LDUserModel allUserAttributes]; + + LDUserModel *copiedUser = [originalUser copy]; + + XCTAssertTrue([copiedUser isEqual:originalUser ignoringAttributes:@[kUserAttributeUpdatedAt]]); + XCTAssertFalse(originalUser == copiedUser); //Verify different user objects + XCTAssertFalse(originalUser.flagConfig == copiedUser.flagConfig); //Verify different flagConfig objects + XCTAssertTrue([copiedUser.flagConfig isEqualToConfig:originalUser.flagConfig]); + XCTAssertFalse(originalUser.flagConfigTracker == copiedUser.flagConfigTracker); //Verify different flagConfigTracker objects + XCTAssertFalse(originalUser.privateAttributes == copiedUser.privateAttributes); //Verify different flagConfig objects +} + +-(void)testCopy_noPrivateAttributes { + LDUserModel *originalUser = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + + LDUserModel *copiedUser = [originalUser copy]; + + XCTAssertTrue([copiedUser isEqual:originalUser ignoringAttributes:@[kUserAttributeUpdatedAt]]); + XCTAssertFalse(originalUser == copiedUser); //Verify different user objects + XCTAssertFalse(originalUser.flagConfig == copiedUser.flagConfig); //Verify different flagConfig objects + XCTAssertFalse(originalUser.flagConfigTracker == copiedUser.flagConfigTracker); //Verify different flagConfigTracker objects + XCTAssertNil(copiedUser.privateAttributes); +} + #pragma mark - Helpers ///Trims out null values, and config -(NSDictionary*)targetUserDictionaryFrom:(NSDictionary*)userDictionary withConfig:(BOOL)withConfig { @@ -516,12 +543,6 @@ -(NSDictionary*)serverJson { return [NSJSONSerialization jsonObjectFromFileNamed:@"featureFlags"]; } --(NSMutableDictionary*)customDictionary { - return [NSMutableDictionary dictionaryWithDictionary:@{@"foo": @"Foo", - @"device": @"iPad", - @"os": @"IOS 9.2.1"}]; -} - -(NSMutableDictionary*)userDictionaryWithUserKey:(NSString*)userKey userName:(NSString*)userName customDictionary:(NSDictionary*)customDictionary { return [[NSMutableDictionary alloc] initWithDictionary:@{ @"key": userKey, @"ip": @"123.456.789", diff --git a/Framework/Darkly.h b/Framework/Darkly.h index 278d7e28..ebe4f43a 100644 --- a/Framework/Darkly.h +++ b/Framework/Darkly.h @@ -16,15 +16,8 @@ FOUNDATION_EXPORT const unsigned char DarklyVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import +#import #import -#import #import -#import - -#import -#import -#import -#import -#import -#import -#import +#import +#import diff --git a/Gemfile.lock b/Gemfile.lock index d9b019a6..45f7c140 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,7 @@ GEM remote: https://rubygems.org/ specs: CFPropertyList (3.0.0) - activesupport (4.2.10) + activesupport (4.2.11) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) @@ -33,7 +33,7 @@ GEM fuzzy_match (~> 2.0.4) nap (~> 1.0) cocoapods-deintegrate (1.0.2) - cocoapods-downloader (1.2.1) + cocoapods-downloader (1.2.2) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.0) @@ -43,7 +43,7 @@ GEM netrc (~> 0.11) cocoapods-try (1.1.0) colored2 (3.1.2) - concurrent-ruby (1.0.5) + concurrent-ruby (1.1.3) escape (0.0.4) fourflusher (2.0.1) fuzzy_match (2.0.4) @@ -55,11 +55,11 @@ GEM nanaimo (0.2.6) nap (1.1.0) netrc (0.11.0) - ruby-macho (1.2.0) + ruby-macho (1.3.1) thread_safe (0.3.6) tzinfo (1.2.5) thread_safe (~> 0.1) - xcodeproj (1.6.0) + xcodeproj (1.7.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index 0995146d..4b215133 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "LaunchDarkly" - s.version = "2.13.9" + s.version = "2.14.0" s.summary = "iOS SDK for LaunchDarkly" s.description = <<-DESC @@ -23,7 +23,7 @@ Pod::Spec.new do |s| s.tvos.deployment_target = "9.0" s.osx.deployment_target = '10.10' - s.source = { :git => "https://github.com/launchdarkly/ios-client.git", :tag => "2.13.9" } + s.source = { :git => "https://github.com/launchdarkly/ios-client.git", :tag => "2.14.0" } s.source_files = 'Darkly/**/*.{h,m}' diff --git a/Podfile.lock b/Podfile.lock index b1a10b2a..1911eee3 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -25,6 +25,6 @@ SPEC CHECKSUMS: OCMock: ebe9ee1dca7fbed0ff9193ac0b3e2d8862ea56f6 OHHTTPStubs: b393565822317305b87a1440d4c7aff131679f66 -PODFILE CHECKSUM: cd0bb87005474a494ef0a05f05261187e11de2d2 +PODFILE CHECKSUM: 88da00e36037ebcf8c33df87329f85bfb5d3a0eb COCOAPODS: 1.4.0 diff --git a/Pods/Manifest.lock b/Pods/Manifest.lock index b1a10b2a..1911eee3 100644 --- a/Pods/Manifest.lock +++ b/Pods/Manifest.lock @@ -25,6 +25,6 @@ SPEC CHECKSUMS: OCMock: ebe9ee1dca7fbed0ff9193ac0b3e2d8862ea56f6 OHHTTPStubs: b393565822317305b87a1440d4c7aff131679f66 -PODFILE CHECKSUM: cd0bb87005474a494ef0a05f05261187e11de2d2 +PODFILE CHECKSUM: 88da00e36037ebcf8c33df87329f85bfb5d3a0eb COCOAPODS: 1.4.0 diff --git a/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-acknowledgements.markdown b/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-acknowledgements.markdown index 3e76186c..7f9a1620 100644 --- a/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-acknowledgements.markdown +++ b/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-acknowledgements.markdown @@ -1,28 +1,6 @@ # Acknowledgements This application makes use of the following third party libraries: -## DarklyEventSource - -Copyright (c) 2013 Neil Cowburn (http://github.com/neilco/) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - ## OCMock @@ -215,4 +193,26 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## DarklyEventSource + +Copyright (c) 2013 Neil Cowburn (http://github.com/neilco/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. Generated by CocoaPods - https://cocoapods.org diff --git a/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-acknowledgements.plist b/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-acknowledgements.plist index 531e4078..1d9ba704 100644 --- a/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-acknowledgements.plist +++ b/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-acknowledgements.plist @@ -12,34 +12,6 @@ Type PSGroupSpecifier - - FooterText - Copyright (c) 2013 Neil Cowburn (http://github.com/neilco/) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - License - MIT (see LICENSE.txt) - Title - DarklyEventSource - Type - PSGroupSpecifier - FooterText @@ -245,6 +217,34 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI Type PSGroupSpecifier + + FooterText + Copyright (c) 2013 Neil Cowburn (http://github.com/neilco/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + License + MIT (see LICENSE.txt) + Title + DarklyEventSource + Type + PSGroupSpecifier + FooterText Generated by CocoaPods - https://cocoapods.org diff --git a/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-frameworks.sh b/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-frameworks.sh index f6329f45..7c0b4cf7 100755 --- a/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-frameworks.sh +++ b/Pods/Target Support Files/Pods-DarklyTests/Pods-DarklyTests-frameworks.sh @@ -134,14 +134,14 @@ strip_invalid_archs() { if [[ "$CONFIGURATION" == "Debug" ]]; then - install_framework "${BUILT_PRODUCTS_DIR}/DarklyEventSource-iOS/DarklyEventSource.framework" install_framework "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework" install_framework "${BUILT_PRODUCTS_DIR}/OHHTTPStubs/OHHTTPStubs.framework" + install_framework "${BUILT_PRODUCTS_DIR}/DarklyEventSource-iOS/DarklyEventSource.framework" fi if [[ "$CONFIGURATION" == "Release" ]]; then - install_framework "${BUILT_PRODUCTS_DIR}/DarklyEventSource-iOS/DarklyEventSource.framework" install_framework "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework" install_framework "${BUILT_PRODUCTS_DIR}/OHHTTPStubs/OHHTTPStubs.framework" + install_framework "${BUILT_PRODUCTS_DIR}/DarklyEventSource-iOS/DarklyEventSource.framework" fi if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then wait diff --git a/README.md b/README.md index e0359bd0..1696b193 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ $ brew install carthage To integrate LaunchDarkly into your Xcode project using Carthage, specify it in your `Cartfile`: ```ogdl -github "launchdarkly/ios-client" "2.13.9" +github "launchdarkly/ios-client" "2.14.0" ``` Run `carthage` to build the framework and drag the built `Darkly.framework` into your Xcode project. @@ -61,7 +61,7 @@ Quick setup 1. Add the SDK to your `Podfile`: - pod 'LaunchDarkly', '2.13.9' + pod 'LaunchDarkly', '2.14.0' 2. Import the LaunchDarkly client: