diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dad37da..34811371 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the LaunchDarkly iOS SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.10.0] - 2018-02-01 +### Added +- Support for specifying [private user attributes](https://docs.launchdarkly.com/docs/private-user-attributes) in order to prevent user attributes from being sent in analytics events back to LaunchDarkly. See the `allUserAttributesPrivate` and `privateUserAttributes` properties of `LDConfig` as well as the `privateAttributes` property of `LDUserBuilder`. + ## [2.9.1] - 2017-12-05 ### Fixed - Carthage builds no longer crash due to a missing DarklyEventSource library. diff --git a/Darkly.xcodeproj/project.pbxproj b/Darkly.xcodeproj/project.pbxproj index 80e06c03..90b66e9f 100644 --- a/Darkly.xcodeproj/project.pbxproj +++ b/Darkly.xcodeproj/project.pbxproj @@ -192,8 +192,19 @@ 83889B1C1F8F28AB00A4EF69 /* NSURLResponse+Unauthorized.m in Sources */ = {isa = PBXBuildFile; fileRef = 83889B161F8F28AB00A4EF69 /* NSURLResponse+Unauthorized.m */; }; 83889B1D1F8F28AB00A4EF69 /* NSURLResponse+Unauthorized.m in Sources */ = {isa = PBXBuildFile; fileRef = 83889B161F8F28AB00A4EF69 /* NSURLResponse+Unauthorized.m */; }; 83889B1E1F8F28AB00A4EF69 /* NSURLResponse+Unauthorized.m in Sources */ = {isa = PBXBuildFile; fileRef = 83889B161F8F28AB00A4EF69 /* NSURLResponse+Unauthorized.m */; }; + 839956E820053081009707D1 /* LDUserModel+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 839956E720053081009707D1 /* LDUserModel+Testable.m */; }; 839D6D271FD1B57B000BE6BD /* DarklyEventSource.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 839D6D261FD1B57B000BE6BD /* DarklyEventSource.framework */; }; 839D6D291FD1B58E000BE6BD /* DarklyEventSource.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 839D6D281FD1B58E000BE6BD /* DarklyEventSource.framework */; }; + 83B8C24C1FEB1CD20082B8A9 /* LDUserModel+Stub.m in Sources */ = {isa = PBXBuildFile; fileRef = 83B8C24B1FEB1CD20082B8A9 /* LDUserModel+Stub.m */; }; + 83B8C24F1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B8C24D1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h */; }; + 83B8C2501FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B8C24D1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h */; }; + 83B8C2511FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B8C24D1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h */; }; + 83B8C2521FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 83B8C24D1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h */; }; + 83B8C2531FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 83B8C24E1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m */; }; + 83B8C2541FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 83B8C24E1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m */; }; + 83B8C2551FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 83B8C24E1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m */; }; + 83B8C2561FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 83B8C24E1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m */; }; + 83B8C2581FEC4C3B0082B8A9 /* userStubFlags.json in Resources */ = {isa = PBXBuildFile; fileRef = 83B8C2571FEC4C3B0082B8A9 /* userStubFlags.json */; }; 83B975D31FD1CA6000A4EF4E /* DarklyEventSource.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 83B975D21FD1CA6000A4EF4E /* DarklyEventSource.framework */; }; 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 */; }; @@ -338,8 +349,15 @@ 83889B131F8E93A100A4EF69 /* LDEvent+Testable.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDEvent+Testable.m"; sourceTree = ""; }; 83889B151F8F28AB00A4EF69 /* NSURLResponse+Unauthorized.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSURLResponse+Unauthorized.h"; sourceTree = ""; }; 83889B161F8F28AB00A4EF69 /* NSURLResponse+Unauthorized.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSURLResponse+Unauthorized.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 = ""; }; 839D6D261FD1B57B000BE6BD /* DarklyEventSource.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DarklyEventSource.framework; path = Carthage/Build/watchOS/DarklyEventSource.framework; sourceTree = ""; }; 839D6D281FD1B58E000BE6BD /* DarklyEventSource.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DarklyEventSource.framework; path = Carthage/Build/tvOS/DarklyEventSource.framework; sourceTree = ""; }; + 83B8C24A1FEB1CD20082B8A9 /* LDUserModel+Stub.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LDUserModel+Stub.h"; sourceTree = ""; }; + 83B8C24B1FEB1CD20082B8A9 /* LDUserModel+Stub.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "LDUserModel+Stub.m"; sourceTree = ""; }; + 83B8C24D1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSDateFormatter+LDUserModel.h"; sourceTree = ""; }; + 83B8C24E1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSDateFormatter+LDUserModel.m"; sourceTree = ""; }; + 83B8C2571FEC4C3B0082B8A9 /* userStubFlags.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = userStubFlags.json; sourceTree = ""; }; 83B975D21FD1CA6000A4EF4E /* DarklyEventSource.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DarklyEventSource.framework; path = Carthage/Build/Mac/DarklyEventSource.framework; 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 = ""; }; @@ -555,6 +573,7 @@ 836947791F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyC.json */, 836947811F20125F0047697C /* ldClientManagerTestConfigA.json */, 836947821F20125F0047697C /* ldClientManagerTestConfigB.json */, + 83B8C2571FEC4C3B0082B8A9 /* userStubFlags.json */, ); path = Fixtures; sourceTree = ""; @@ -568,6 +587,10 @@ 8349F51D1F19352300B1F3DB /* NSDictionary+StringKey_Matchable.m */, 832C788B1F2977B800E334A2 /* NSString+RemoveWhitespace.h */, 832C788C1F2977B800E334A2 /* NSString+RemoveWhitespace.m */, + 83B8C24A1FEB1CD20082B8A9 /* LDUserModel+Stub.h */, + 83B8C24B1FEB1CD20082B8A9 /* LDUserModel+Stub.m */, + 839956E620053081009707D1 /* LDUserModel+Testable.h */, + 839956E720053081009707D1 /* LDUserModel+Testable.m */, 8349F51F1F195BCF00B1F3DB /* LDUserModel+Equatable.h */, 8349F5201F195BCF00B1F3DB /* LDUserModel+Equatable.m */, 832C788E1F2A8DF600E334A2 /* LDUserModel+JsonDecodeable.h */, @@ -605,6 +628,8 @@ 83F5B4771F95096A00174DF7 /* NSHTTPURLResponse+Unauthorized.m */, 83EF677F1F979B4100403126 /* LDEvent+Unauthorized.h */, 83EF67801F979B4100403126 /* LDEvent+Unauthorized.m */, + 83B8C24D1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h */, + 83B8C24E1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m */, ); name = Categories; sourceTree = ""; @@ -631,6 +656,7 @@ 690346F91E68990000E45133 /* LDClientManager.h in Headers */, 690346FF1E68990000E45133 /* LDFlagConfigModel.h in Headers */, 83EF67811F979B4100403126 /* LDEvent+Unauthorized.h in Headers */, + 83B8C24F1FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h in Headers */, 690346F51E68990000E45133 /* DarklyConstants.h in Headers */, 690346FB1E68990000E45133 /* LDConfig.h in Headers */, 690347011E68990000E45133 /* LDPollingManager.h in Headers */, @@ -658,6 +684,7 @@ 69A87E9E1E74712800B88B23 /* LDClientManager.h in Headers */, 69A87EA41E74712800B88B23 /* LDFlagConfigModel.h in Headers */, 83EF67841F979B4100403126 /* LDEvent+Unauthorized.h in Headers */, + 83B8C2521FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h in Headers */, 69A87E9A1E74712800B88B23 /* DarklyConstants.h in Headers */, 69A87EA01E74712800B88B23 /* LDConfig.h in Headers */, 69A87EA61E74712800B88B23 /* LDPollingManager.h in Headers */, @@ -685,6 +712,7 @@ 69BD7E1F1E6C79910056D70F /* LDClientManager.h in Headers */, 69BD7E251E6C79910056D70F /* LDFlagConfigModel.h in Headers */, 83EF67831F979B4100403126 /* LDEvent+Unauthorized.h in Headers */, + 83B8C2511FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h in Headers */, 69BD7E1B1E6C79910056D70F /* DarklyConstants.h in Headers */, 69BD7E211E6C79910056D70F /* LDConfig.h in Headers */, 69BD7E271E6C79910056D70F /* LDPollingManager.h in Headers */, @@ -712,6 +740,7 @@ 69F3F6A11E6BF82C00079A09 /* LDPollingManager.h in Headers */, 69F3F6941E6BF80800079A09 /* LDDataManager.h in Headers */, 83EF67821F979B4100403126 /* LDEvent+Unauthorized.h in Headers */, + 83B8C2501FEC19500082B8A9 /* NSDateFormatter+LDUserModel.h in Headers */, 69F3F6991E6BF82C00079A09 /* LDClientManager.h in Headers */, 69F3F6A51E6BF82C00079A09 /* LDUserBuilder.h in Headers */, 69F3F6A91E6BF82C00079A09 /* LDUtil.h in Headers */, @@ -911,6 +940,7 @@ 6903472F1E689B9F00E45133 /* feature_flags.json in Resources */, 836947631F1FEEB40047697C /* numberConfigIsANumber-2.json in Resources */, 8369475C1F1FED400047697C /* boolConfigIsABool-false.json in Resources */, + 83B8C2581FEC4C3B0082B8A9 /* userStubFlags.json in Resources */, 836947831F20125F0047697C /* ldClientManagerTestConfigA.json in Resources */, 836947721F1FF45B0047697C /* arrayConfigIsAnArrayA-123.json in Resources */, 8369477C1F1FF80A0047697C /* dictionaryConfigIsADictionary-KeyA.json in Resources */, @@ -1155,6 +1185,7 @@ 83F5B47C1F95096A00174DF7 /* NSHTTPURLResponse+Unauthorized.m in Sources */, 690346F81E68990000E45133 /* LDClient.m in Sources */, 690346F61E68990000E45133 /* DarklyConstants.m in Sources */, + 83B8C2531FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m in Sources */, 83EF67851F979B4100403126 /* LDEvent+Unauthorized.m in Sources */, 6903470E1E68990000E45133 /* NSDictionary+JSON.m in Sources */, 690347021E68990000E45133 /* LDPollingManager.m in Sources */, @@ -1172,6 +1203,7 @@ files = ( 83EF678D1F98FC9200403126 /* LDFlagConfigModel+Testable.m in Sources */, 8349F51E1F19352300B1F3DB /* NSDictionary+StringKey_Matchable.m in Sources */, + 839956E820053081009707D1 /* LDUserModel+Testable.m in Sources */, 690347291E689B9F00E45133 /* LDEventModelTest.m in Sources */, 690347301E689B9F00E45133 /* LDConfigTest.m in Sources */, 690347261E689B9F00E45133 /* LDUserBuilderTest.m in Sources */, @@ -1191,6 +1223,7 @@ 83258A401F3244D0008C2133 /* LDUserBuilder+Testable.m in Sources */, 690347331E689B9F00E45133 /* NSArray+UnitTests.m in Sources */, 6903472C1E689B9F00E45133 /* LDClientTest.m in Sources */, + 83B8C24C1FEB1CD20082B8A9 /* LDUserModel+Stub.m in Sources */, 832C78901F2A8DF600E334A2 /* LDUserModel+JsonDecodeable.m in Sources */, 8358F25A1F4202A300ECE1AF /* LDConfig+Testable.m in Sources */, 83F5B4751F91560300174DF7 /* LDDataManager+Testable.m in Sources */, @@ -1211,6 +1244,7 @@ 83F5B47F1F95096A00174DF7 /* NSHTTPURLResponse+Unauthorized.m in Sources */, 69A87EA51E74712800B88B23 /* LDFlagConfigModel.m in Sources */, 69A87EB11E74712800B88B23 /* NSDictionary+JSON.m in Sources */, + 83B8C2561FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m in Sources */, 83EF67881F979B4100403126 /* LDEvent+Unauthorized.m in Sources */, 69A87EAF1E74712800B88B23 /* LDUtil.m in Sources */, 69A87EA71E74712800B88B23 /* LDPollingManager.m in Sources */, @@ -1236,6 +1270,7 @@ 83F5B47E1F95096A00174DF7 /* NSHTTPURLResponse+Unauthorized.m in Sources */, 69BD7E261E6C79910056D70F /* LDFlagConfigModel.m in Sources */, 69BD7E321E6C79910056D70F /* NSDictionary+JSON.m in Sources */, + 83B8C2551FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m in Sources */, 83EF67871F979B4100403126 /* LDEvent+Unauthorized.m in Sources */, 69BD7E301E6C79910056D70F /* LDUtil.m in Sources */, 69BD7E281E6C79910056D70F /* LDPollingManager.m in Sources */, @@ -1261,6 +1296,7 @@ 83F5B47D1F95096A00174DF7 /* NSHTTPURLResponse+Unauthorized.m in Sources */, 69F3F6A01E6BF82C00079A09 /* LDFlagConfigModel.m in Sources */, 69F3F6AC1E6BF82C00079A09 /* NSDictionary+JSON.m in Sources */, + 83B8C2541FEC19500082B8A9 /* NSDateFormatter+LDUserModel.m in Sources */, 83EF67861F979B4100403126 /* LDEvent+Unauthorized.m in Sources */, 69F3F6AA1E6BF82C00079A09 /* LDUtil.m in Sources */, 69F3F6A21E6BF82C00079A09 /* LDPollingManager.m in Sources */, diff --git a/Darkly/DarklyConstants.m b/Darkly/DarklyConstants.m index 66aa35e8..24a12ddc 100644 --- a/Darkly/DarklyConstants.m +++ b/Darkly/DarklyConstants.m @@ -4,7 +4,7 @@ #import "DarklyConstants.h" -NSString * const kClientVersion = @"2.9.1"; +NSString * const kClientVersion = @"2.10.0"; NSString * const kBaseUrl = @"https://app.launchdarkly.com"; NSString * const kEventsUrl = @"https://mobile.launchdarkly.com"; NSString * const kStreamUrl = @"https://clientstream.launchdarkly.com/mping"; diff --git a/Darkly/LDClient.m b/Darkly/LDClient.m index cdc85e40..4300f983 100644 --- a/Darkly/LDClient.m +++ b/Darkly/LDClient.m @@ -114,7 +114,7 @@ - (BOOL)boolVariation:(NSString *)featureKey fallback:(BOOL)fallback{ returnValue = [(NSNumber *)flagValue boolValue]; } - [[LDDataManager sharedManager] createFeatureEvent: featureKey keyValue:[NSNumber numberWithBool:returnValue] defaultKeyValue:[NSNumber numberWithBool:fallback]]; + [[LDDataManager sharedManager] createFeatureEvent: featureKey keyValue:[NSNumber numberWithBool:returnValue] defaultKeyValue:[NSNumber numberWithBool:fallback] user:self.ldUser config:self.ldConfig]; return returnValue; } else { DEBUG_LOGX(@"LDClient not started yet!"); @@ -136,7 +136,7 @@ - (NSNumber*)numberVariation:(NSString *)featureKey fallback:(NSNumber*)fallback returnValue = (NSNumber *)flagValue; } - [[LDDataManager sharedManager] createFeatureEvent: featureKey keyValue:returnValue defaultKeyValue:fallback]; + [[LDDataManager sharedManager] createFeatureEvent: featureKey keyValue:returnValue defaultKeyValue:fallback user:self.ldUser config:self.ldConfig]; return returnValue; } else { DEBUG_LOGX(@"LDClient not started yet!"); @@ -161,7 +161,7 @@ - (double)doubleVariation:(NSString *)featureKey fallback:(double)fallback { returnValue = [((NSNumber *)flagValue) doubleValue]; } - [[LDDataManager sharedManager] createFeatureEvent:featureKey keyValue:[NSNumber numberWithDouble:returnValue] defaultKeyValue:[NSNumber numberWithDouble:fallback]]; + [[LDDataManager sharedManager] createFeatureEvent:featureKey keyValue:[NSNumber numberWithDouble:returnValue] defaultKeyValue:[NSNumber numberWithDouble:fallback] user:self.ldUser config:self.ldConfig]; return returnValue; } @@ -179,7 +179,7 @@ - (NSString*)stringVariation:(NSString *)featureKey fallback:(NSString*)fallback returnValue = (NSString *)flagValue; } - [[LDDataManager sharedManager] createFeatureEvent: featureKey keyValue:returnValue defaultKeyValue:fallback]; + [[LDDataManager sharedManager] createFeatureEvent: featureKey keyValue:returnValue defaultKeyValue:fallback user:self.ldUser config:self.ldConfig]; return returnValue; } else { DEBUG_LOGX(@"LDClient not started yet!"); @@ -204,7 +204,7 @@ - (NSArray*)arrayVariation:(NSString *)featureKey fallback:(NSArray*)fallback{ returnValue = (NSArray *)flagValue; } - [[LDDataManager sharedManager] createFeatureEvent: featureKey keyValue:returnValue defaultKeyValue:fallback]; + [[LDDataManager sharedManager] createFeatureEvent: featureKey keyValue:returnValue defaultKeyValue:fallback user:self.ldUser config:self.ldConfig]; return returnValue; } @@ -225,7 +225,7 @@ - (NSDictionary*)dictionaryVariation:(NSString *)featureKey fallback:(NSDictiona returnValue = (NSDictionary *)flagValue; } - [[LDDataManager sharedManager] createFeatureEvent: featureKey keyValue:returnValue defaultKeyValue:fallback]; + [[LDDataManager sharedManager] createFeatureEvent: featureKey keyValue:returnValue defaultKeyValue:fallback user:self.ldUser config:self.ldConfig]; return returnValue; } @@ -233,8 +233,7 @@ - (BOOL)track:(NSString *)eventName data:(NSDictionary *)dataDictionary { DEBUG_LOG(@"LDClient track method called for event=%@ and data=%@", eventName, dataDictionary); if (self.clientStarted) { - [[LDDataManager sharedManager] createCustomEvent:eventName - withCustomValuesDictionary: dataDictionary]; + [[LDDataManager sharedManager] createCustomEvent:eventName withCustomValuesDictionary: dataDictionary user:self.ldUser config:self.ldConfig]; return YES; } else { DEBUG_LOGX(@"LDClient not started yet!"); diff --git a/Darkly/LDClientManager.m b/Darkly/LDClientManager.m index e12ea562..cf641852 100644 --- a/Darkly/LDClientManager.m +++ b/Darkly/LDClientManager.m @@ -176,9 +176,9 @@ -(void)syncWithServerForEvents { DEBUG_LOGX(@"ClientManager syncing events with server"); - [[LDDataManager sharedManager] allEventsJsonArray:^(NSArray *array) { - if (array) { - [[LDRequestManager sharedInstance] performEventRequest:array]; + [[LDDataManager sharedManager] allEventDictionaries:^(NSArray *eventDictionaries) { + if (eventDictionaries) { + [[LDRequestManager sharedInstance] performEventRequest:eventDictionaries]; } else { DEBUG_LOGX(@"ClientManager has no events so won't sync events with server"); } diff --git a/Darkly/LDConfig.h b/Darkly/LDConfig.h index 56268edf..d502f8cd 100644 --- a/Darkly/LDConfig.h +++ b/Darkly/LDConfig.h @@ -69,6 +69,24 @@ */ @property (nonatomic) BOOL streaming; +/** + List of user attributes and top level custom dictionary keys to treat as private for event reporting. + Private attribute values will not be included in events reported to Launch Darkly, but the attribute name will still + be sent. All user attributes can be declared private except key, anonymous, device, & os. Access the user attribute names + that can be declared private through the identifiers included in LDUserModel.h. To declare all user attributes private, + either set privateUserAttributes to [LDUserModel allUserAttributes] or set LDConfig.allUserAttributesPrivate. In either case, + setting attributes to private in the config causes the LDClient to treat the attribute(s) as private for all users. + The default is nil. + */ +@property (nonatomic, strong, nullable) NSArray* privateUserAttributes; + +/** + Flag that tells the LDClient to treat all user attributes as private for all users. When set, ignores any values in + either LDConfig.privateUserAttributes or LDUserModel.privateAttributes. The LDClient will not send any private attributes + in event reports as described for privateUserAttributes. The default is NO. + */ +@property (nonatomic, assign) BOOL allUserAttributesPrivate; + /** Flag that enables REPORT HTTP method for feature flag requests. When useReport is false, feature flag requests use the GET HTTP method. The default is NO. diff --git a/Darkly/LDConfig.m b/Darkly/LDConfig.m index d8749429..7fb4c1c6 100644 --- a/Darkly/LDConfig.m +++ b/Darkly/LDConfig.m @@ -32,6 +32,7 @@ - (instancetype)initWithMobileKey:(NSString *)mobileKey { // @(kHTTPStatusCodeNotImplemented)]; self.flagRetryStatusCodes = @[]; //Temporarily, leave these codes empty to disable the REPORT fallback using GET capability self.useReport = NO; + self.allUserAttributesPrivate = NO; return self; } @@ -116,6 +117,11 @@ - (void)setStreaming:(BOOL)streaming { DEBUG_LOG(@"Set LDConfig streaming enabled: %d", streaming); } +- (void)setPrivateUserAttributes:(NSArray*)privateAttributes { + _privateUserAttributes = privateAttributes; + DEBUG_LOG(@"Set LDConfig privateAttributes set: %@", privateAttributes.description); +} + - (void)setDebugEnabled:(BOOL)debugEnabled { _debugEnabled = debugEnabled; DEBUG_LOG(@"Set LDConfig debug enabled: %d", debugEnabled); diff --git a/Darkly/LDDataManager.h b/Darkly/LDDataManager.h index 690ac43e..88e8201b 100644 --- a/Darkly/LDDataManager.h +++ b/Darkly/LDDataManager.h @@ -12,12 +12,12 @@ extern int const kUserCacheSize; +(LDDataManager *)sharedManager; --(void) allEventsJsonArray:(void (^)(NSArray *array))completion; +-(void) allEventDictionaries:(void (^)(NSArray *eventDictionaries))completion; -(NSMutableDictionary *)retrieveUserDictionary; -(NSMutableArray *)retrieveEventsArray; -(LDUserModel *)findUserWithkey: (NSString *)key; --(void) createFeatureEvent: (NSString *)featureKey keyValue:(NSObject*)keyValue defaultKeyValue:(NSObject*)defaultKeyValue; --(void) createCustomEvent: (NSString *)eventKey withCustomValuesDictionary: (NSDictionary *)customDict; +-(void) createFeatureEvent: (NSString *)featureKey keyValue:(NSObject*)keyValue defaultKeyValue:(NSObject*)defaultKeyValue user:(LDUserModel*)user config:(LDConfig*)config; +-(void) createCustomEvent: (NSString *)eventKey withCustomValuesDictionary: (NSDictionary *)customDict user:(LDUserModel*)user config:(LDConfig*)config; -(void) purgeOldUser: (NSMutableDictionary *)dictionary; -(void) saveUser: (LDUserModel *) user; -(void) saveUserDeprecated:(LDUserModel *)user __deprecated_msg("Use saveUser: instead"); diff --git a/Darkly/LDDataManager.m b/Darkly/LDDataManager.m index b640db23..b831f1dc 100644 --- a/Darkly/LDDataManager.m +++ b/Darkly/LDDataManager.m @@ -108,7 +108,8 @@ - (void)compareConfigForUser:(LDUserModel *)user withNewUser:(LDUserModel *)newU - (void)storeUserDictionary:(NSDictionary *)userDictionary { NSMutableDictionary *archiveDictionary = [[NSMutableDictionary alloc] init]; for (NSString *key in userDictionary) { - [archiveDictionary setObject:[[userDictionary objectForKey:key] dictionaryValue] forKey:key]; + 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]; @@ -146,36 +147,36 @@ - (NSMutableDictionary *)retrieveUserDictionary { #pragma mark - events --(void) createFeatureEvent: (NSString *)featureKey keyValue:(NSObject*)keyValue defaultKeyValue:(NSObject*)defaultKeyValue { +-(void) createFeatureEvent: (NSString *)featureKey keyValue:(NSObject*)keyValue defaultKeyValue:(NSObject*)defaultKeyValue user:(LDUserModel*)user config:(LDConfig*)config { if(![self isAtEventCapacity:_eventsArray]) { DEBUG_LOG(@"Creating event for feature:%@ with value:%@ and fallback:%@", featureKey, keyValue, defaultKeyValue); - [self addEvent:[[LDEventModel alloc] initFeatureEventWithKey: featureKey keyValue:keyValue defaultKeyValue:defaultKeyValue userValue:[LDClient sharedInstance].ldUser]]; + [self addEventDictionary:[[[LDEventModel alloc] initFeatureEventWithKey:featureKey keyValue:keyValue defaultKeyValue:defaultKeyValue userValue:user] dictionaryValueUsingConfig:config]]; } else { DEBUG_LOG(@"Events have surpassed capacity. Discarding feature event %@", featureKey); } } --(void) createCustomEvent: (NSString *)eventKey withCustomValuesDictionary: (NSDictionary *)customDict { +-(void) createCustomEvent: (NSString *)eventKey withCustomValuesDictionary: (NSDictionary *)customDict user:(LDUserModel*)user config:(LDConfig*)config { if(![self isAtEventCapacity:_eventsArray]) { DEBUG_LOG(@"Creating event for custom key:%@ and value:%@", eventKey, customDict); - [self addEvent:[[LDEventModel alloc] initCustomEventWithKey: eventKey andDataDictionary: customDict userValue:[LDClient sharedInstance].ldUser]]; + [self addEventDictionary:[[[LDEventModel alloc] initCustomEventWithKey:eventKey andDataDictionary: customDict userValue:user] dictionaryValueUsingConfig:config]]; } else { DEBUG_LOG(@"Events have surpassed capacity. Discarding event %@ with dictionary %@", eventKey, customDict); } } --(void)addEvent:(LDEventModel*)event { +-(void)addEventDictionary:(NSDictionary*)eventDictionary { dispatch_async(eventsQueue, ^{ if (!_eventsArray) { _eventsArray = [[NSMutableArray alloc] init]; } if(![self isAtEventCapacity:_eventsArray]) { - [_eventsArray addObject:event]; + [_eventsArray addObject:eventDictionary]; } else { - DEBUG_LOG(@"Events have surpassed capacity. Discarding event %@", event.key); + DEBUG_LOG(@"Events have surpassed capacity. Discarding event %@", eventDictionary[@"key"]); } }); } @@ -192,17 +193,12 @@ -(void) deleteProcessedEvents: (NSArray *) processedJsonArray { [_eventsArray removeObjectsInRange:NSMakeRange(0, count)]; }); } --(void) allEventsJsonArray:(void (^)(NSArray *array))completion { + +-(void) allEventDictionaries:(void (^)(NSArray *))completion { dispatch_async(eventsQueue, ^{ - NSMutableArray *array = [self retrieveEventsArray]; - if (array && [array count]) { - - NSMutableArray *eventArray = [[NSMutableArray alloc] init]; - for (LDEventModel *currentEvent in array) { - [eventArray addObject:[currentEvent dictionaryValue]]; - } - - completion(eventArray); + NSMutableArray *eventDictionaries = [self retrieveEventsArray]; + if (eventDictionaries && [eventDictionaries count]) { + completion(eventDictionaries); } else { completion(nil); } diff --git a/Darkly/LDEventModel.h b/Darkly/LDEventModel.h index 44867e2b..b91e9b9d 100644 --- a/Darkly/LDEventModel.h +++ b/Darkly/LDEventModel.h @@ -9,6 +9,7 @@ #import @class LDUserModel; +@class LDConfig; @interface LDEventModel : NSObject @property (nullable, nonatomic, strong) NSString *key; @@ -21,7 +22,7 @@ @property (nonnull, nonatomic, strong) NSObject *isDefault; -(nonnull id)initWithDictionary:(nonnull NSDictionary *)dictionary; --(nonnull NSDictionary *)dictionaryValue; +-(nonnull NSDictionary *)dictionaryValueUsingConfig:(nonnull LDConfig*)config; -(nonnull instancetype)initFeatureEventWithKey:(nonnull NSString *)featureKey keyValue:(NSObject * _Nullable)keyValue defaultKeyValue:(NSObject * _Nullable)defaultKeyValue userValue:(nonnull LDUserModel *)userValue; -(nonnull instancetype)initCustomEventWithKey: (nonnull NSString *)featureKey diff --git a/Darkly/LDEventModel.m b/Darkly/LDEventModel.m index 2ab05db3..68849320 100644 --- a/Darkly/LDEventModel.m +++ b/Darkly/LDEventModel.m @@ -100,7 +100,7 @@ - (instancetype)init { } --(NSDictionary *)dictionaryValue{ +-(NSDictionary *)dictionaryValueUsingConfig:(LDConfig*)config { NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; self.key ? [dictionary setObject:self.key forKey: kKeyKey] : nil; @@ -109,7 +109,7 @@ -(NSDictionary *)dictionaryValue{ self.data ? [dictionary setObject:self.data forKey: kDataKey] : nil; self.value ? [dictionary setObject:self.value forKey: kFeatureKeyValueServerKey] : nil; self.isDefault ? [dictionary setObject:self.isDefault forKey: kIsDefaultServerKey] : nil; - self.user ? [dictionary setObject:[self.user dictionaryValue] forKey: kUserKey] : nil; + self.user ? [dictionary setObject:[self.user dictionaryValueWithFlagConfig:YES includePrivateAttributes:NO config:config] forKey: kUserKey] : nil; return dictionary; } diff --git a/Darkly/LDRequestManager.h b/Darkly/LDRequestManager.h index 8196be2e..164af52d 100644 --- a/Darkly/LDRequestManager.h +++ b/Darkly/LDRequestManager.h @@ -24,6 +24,6 @@ extern NSString * const kHeaderMobileKey; -(void)performFeatureFlagRequest:(LDUserModel *)user; --(void)performEventRequest:(NSArray *)jsonEventArray; +-(void)performEventRequest:(NSArray *)eventDictionaries; @end diff --git a/Darkly/LDRequestManager.m b/Darkly/LDRequestManager.m index 939c02e3..3fbf4293 100644 --- a/Darkly/LDRequestManager.m +++ b/Darkly/LDRequestManager.m @@ -7,6 +7,7 @@ #import "LDClientManager.h" #import "LDConfig.h" #import "NSURLResponse+Unauthorized.h" +#import "NSDictionary+JSON.h" static NSString * const kFeatureFlagGetUrl = @"/msdk/eval/users/"; static NSString * const kFeatureFlagReportUrl = @"/msdk/eval/user"; @@ -136,14 +137,14 @@ -(void)performFlagRequest:(NSURLRequest*)request completionHandler:(void (^)(NSD dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); } --(void)performEventRequest:(NSArray *)jsonEventArray { +-(void)performEventRequest:(NSArray *)eventDictionaries { [self configure:[LDClient sharedInstance].ldConfig]; if (!mobileKey) { DEBUG_LOGX(@"RequestManager unable to sync events to server since no mobileKey"); return; } - if (!jsonEventArray || jsonEventArray.count == 0) { + if (!eventDictionaries || eventDictionaries.count == 0) { DEBUG_LOGX(@"RequestManager unable to sync events to server since no events"); return; } @@ -165,7 +166,7 @@ -(void)performEventRequest:(NSArray *)jsonEventArray { [self addEventRequestHeaders:request]; NSError *error; - NSData *postData = [NSJSONSerialization dataWithJSONObject:jsonEventArray options:0 error:&error]; + NSData *postData = [NSJSONSerialization dataWithJSONObject:eventDictionaries options:0 error:&error]; [request setHTTPMethod:@"POST"]; [request setHTTPBody:postData]; @@ -181,7 +182,7 @@ -(void)performEventRequest:(NSArray *)jsonEventArray { dispatch_semaphore_signal(semaphore); dispatch_async(dispatch_get_main_queue(), ^{ BOOL processedEvents = !error; - [delegate processedEvents:processedEvents jsonEventArray:jsonEventArray]; + [delegate processedEvents:processedEvents jsonEventArray:eventDictionaries]; }); }]; @@ -196,7 +197,7 @@ -(NSURLRequest*)flagRequestUsingReportMethodForUser:(LDUserModel*)user { DEBUG_LOGX(@"RequestManager unable to sync config to server since no user"); return nil; } - NSString *userJson = [user convertToJson]; + NSString *userJson = [[user dictionaryValueWithPrivateAttributesAndFlagConfig:NO] jsonString]; if (!userJson) { DEBUG_LOGX(@"RequestManager could not convert user to json, aborting sync config to server"); return nil; @@ -217,7 +218,7 @@ -(NSURLRequest*)flagRequestUsingGetMethodForUser:(LDUserModel*)user { DEBUG_LOGX(@"RequestManager unable to sync config to server since no user"); return nil; } - NSString *userJson = [user convertToJson]; + NSString *userJson = [[user dictionaryValueWithPrivateAttributesAndFlagConfig:NO] jsonString]; if (!userJson) { DEBUG_LOGX(@"RequestManager could not convert user to json, aborting sync config to server"); return nil; diff --git a/Darkly/LDUserBuilder.h b/Darkly/LDUserBuilder.h index 856c62c7..ff0a4a62 100644 --- a/Darkly/LDUserBuilder.h +++ b/Darkly/LDUserBuilder.h @@ -61,6 +61,16 @@ */ @property (nonatomic) BOOL isAnonymous; +/** + * List of user attributes and top level custom dictionary keys to treat as private for event reporting. + * Private attribute values will not be included in events reported to LaunchDarkly, but the attribute name will still + * be sent. All user attributes can be declared private except `key` and `anonymous`. Access the user attribute names that + * can be declared private through the identifiers included in `LDUserModel.h`. To declare all user attributes private, set + * `privateAttributes` to `[LDUserModel allUserAttributes]`. By setting the attribute private in the user, + * the attribute will be treated private for this user only. The default is nil. (Optional) + */ +@property (nonatomic, strong, nullable) NSArray* privateAttributes; + /** * Provide custom String data for the dictionary associated with * the user. (Optional) @@ -97,13 +107,12 @@ */ - (void)customArray:(nonnull NSString *)inputKey value:(nonnull NSArray *)value; - -(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("User `currentBuilder:` instead"); ++ (nonnull LDUserBuilder *)retrieveCurrentBuilder:(nonnull LDUserModel *)iUser __deprecated_msg("Use `currentBuilder:` instead"); /** * Provide a key to the user builder to identify the user. If this key diff --git a/Darkly/LDUserBuilder.m b/Darkly/LDUserBuilder.m index d66d92f0..9511beb9 100644 --- a/Darkly/LDUserBuilder.m +++ b/Darkly/LDUserBuilder.m @@ -38,6 +38,9 @@ + (LDUserModel *)compareNewBuilder:(LDUserBuilder *)iBuilder withUser:(LDUserMod iUser.custom = iBuilder.customDictionary; } iUser.anonymous = iBuilder.isAnonymous; + if (iBuilder.privateAttributes || iUser.privateAttributes) { + iUser.privateAttributes = iBuilder.privateAttributes; + } return iUser; } @@ -77,6 +80,9 @@ + (LDUserBuilder *)currentBuilder:(LDUserModel *)iUser { userBuilder.customDictionary = [iUser.custom mutableCopy]; } userBuilder.isAnonymous = iUser.anonymous; + if (iUser.privateAttributes) { + userBuilder.privateAttributes = iUser.privateAttributes; + } return userBuilder; } @@ -201,6 +207,10 @@ - (LDUserModel *)build { 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; + } [[LDDataManager sharedManager] saveUser:user]; return user; diff --git a/Darkly/LDUserModel.h b/Darkly/LDUserModel.h index 5cd66587..da5719c8 100644 --- a/Darkly/LDUserModel.h +++ b/Darkly/LDUserModel.h @@ -7,9 +7,19 @@ // #import +#import "LDConfig.h" @class LDFlagConfigModel; +extern NSString * __nonnull const kUserAttributeIp; +extern NSString * __nonnull const kUserAttributeCountry; +extern NSString * __nonnull const kUserAttributeName; +extern NSString * __nonnull const kUserAttributeFirstName; +extern NSString * __nonnull const kUserAttributeLastName; +extern NSString * __nonnull const kUserAttributeEmail; +extern NSString * __nonnull const kUserAttributeAvatar; +extern NSString * __nonnull const kUserAttributeCustom; + @interface LDUserModel : NSObject @property (nullable, nonatomic, strong, setter=key:) NSString *key; @property (nullable, nonatomic, strong) NSString *ip; @@ -22,17 +32,19 @@ @property (nullable, nonatomic, strong) NSDictionary *custom; @property (nullable, nonatomic, strong) NSDate *updatedAt; @property (nullable, nonatomic, strong) LDFlagConfigModel *config; +@property (nonatomic, strong, nullable) NSArray* privateAttributes; @property (nonatomic, assign) BOOL anonymous; @property (nullable, nonatomic, strong) NSString *device; @property (nullable, nonatomic, strong) NSString *os; -(nonnull id)initWithDictionary:(nonnull NSDictionary *)dictionary; --(nonnull NSString *) convertToJson; --(nonnull NSDictionary *)dictionaryValue; --(nonnull NSDictionary *)dictionaryValueWithConfig:(BOOL)withConfig; +-(nonnull NSDictionary *)dictionaryValueWithPrivateAttributesAndFlagConfig:(BOOL)includeFlags; +-(nonnull NSDictionary *)dictionaryValueWithFlagConfig:(BOOL)includeFlags includePrivateAttributes:(BOOL)includePrivate config:(nullable LDConfig*)config; -(NSObject * __nonnull) flagValue: ( NSString * __nonnull )keyName; -(BOOL) doesFlagExist: (nonnull NSString *)keyName; ++(NSArray * __nonnull) allUserAttributes; + @end diff --git a/Darkly/LDUserModel.m b/Darkly/LDUserModel.m index d59bb41d..74885831 100644 --- a/Darkly/LDUserModel.m +++ b/Darkly/LDUserModel.m @@ -9,96 +9,133 @@ #import "LDUserModel.h" #import "LDFlagConfigModel.h" #import "LDUtil.h" +#import "NSDateFormatter+LDUserModel.h" -static NSString * const kKeyKey = @"key"; -static NSString * const kIpKey = @"ip"; -static NSString * const kCountryKey = @"country"; -static NSString * const kNameKey = @"name"; -static NSString * const kFirstNameKey = @"firstName"; -static NSString * const kLastNameKey = @"lastName"; -static NSString * const kEmailKey = @"email"; -static NSString * const kAvatarKey = @"avatar"; -static NSString * const kCustomKey = @"custom"; -static NSString * const kUpdatedAtKey = @"updatedAt"; -static NSString * const kConfigKey = @"config"; -static NSString * const kAnonymousKey = @"anonymous"; -static NSString * const kDeviceKey = @"device"; -static NSString * const kOsKey = @"os"; +NSString * const kUserAttributeKey = @"key"; +NSString * const kUserAttributeIp = @"ip"; +NSString * const kUserAttributeCountry = @"country"; +NSString * const kUserAttributeName = @"name"; +NSString * const kUserAttributeFirstName = @"firstName"; +NSString * const kUserAttributeLastName = @"lastName"; +NSString * const kUserAttributeEmail = @"email"; +NSString * const kUserAttributeAvatar = @"avatar"; +NSString * const kUserAttributeCustom = @"custom"; +NSString * const kUserAttributeUpdatedAt = @"updatedAt"; +NSString * const kUserAttributeConfig = @"config"; +NSString * const kUserAttributeAnonymous = @"anonymous"; +NSString * const kUserAttributeDevice = @"device"; +NSString * const kUserAttributeOs = @"os"; +NSString * const kUserAttributePrivateAttributes = @"privateAttrs"; @implementation LDUserModel --(NSDictionary *)dictionaryValue { - return [self dictionaryValueWithConfig:YES]; +-(NSDictionary *)dictionaryValueWithPrivateAttributesAndFlagConfig:(BOOL)includeFlags { + return [self dictionaryValueWithFlagConfig:includeFlags includePrivateAttributes:YES config:nil]; } --(NSDictionary *)dictionaryValueWithConfig:(BOOL)includeConfig { +-(NSDictionary *)dictionaryValueWithFlagConfig:(BOOL)includeFlags includePrivateAttributes:(BOOL)includePrivate config:(LDConfig*)config { + NSMutableArray *combinedPrivateAttributes = [NSMutableArray arrayWithArray:self.privateAttributes]; + [combinedPrivateAttributes addObjectsFromArray:config.privateUserAttributes]; + if (config.allUserAttributesPrivate) { combinedPrivateAttributes = [[LDUserModel allUserAttributes] mutableCopy]; } + NSMutableDictionary *dictionary = [[NSMutableDictionary alloc] init]; - - NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; - [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]; - [formatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]]; - - self.key ? [dictionary setObject:self.key forKey: kKeyKey] : nil; - self.ip ? [dictionary setObject:self.ip forKey: kIpKey] : nil; - self.country ? [dictionary setObject:self.country forKey: kCountryKey] : nil; - self.name ? [dictionary setObject:self.name forKey: kNameKey] : nil; - self.firstName ? [dictionary setObject:self.firstName forKey: kFirstNameKey] : nil; - self.lastName ? [dictionary setObject:self.lastName forKey: kLastNameKey] : nil; - self.email ? [dictionary setObject:self.email forKey: kEmailKey] : nil; - self.avatar ? [dictionary setObject:self.avatar forKey: kAvatarKey] : nil; - self.custom ? [dictionary setObject:self.custom forKey: kCustomKey] : nil; - self.anonymous ? [dictionary setObject:[NSNumber numberWithBool: self.anonymous ] forKey: kAnonymousKey] : nil; - self.updatedAt ? [dictionary setObject:[formatter stringFromDate:self.updatedAt] forKey:kUpdatedAtKey] : nil; - - NSMutableDictionary *customDict = [[NSMutableDictionary alloc] initWithDictionary:[dictionary objectForKey:kCustomKey]]; - self.device ? [customDict setObject:self.device forKey:kDeviceKey] : nil; - self.os ? [customDict setObject:self.os forKey:kOsKey] : nil; - - [dictionary setObject:customDict forKey:kCustomKey]; - - if (includeConfig && self.config.featuresJsonDictionary) { - [dictionary setObject:[[self.config dictionaryValue] objectForKey:kFeaturesJsonDictionaryKey] forKey:kConfigKey]; + NSMutableSet *redactedPrivateAttributes = [NSMutableSet set]; + + if (self.key) { dictionary[kUserAttributeKey] = self.key; } + [self storeValue:self.ip in:dictionary forAttribute:kUserAttributeIp includePrivate:includePrivate privateAttributes:combinedPrivateAttributes redactedAttributes:redactedPrivateAttributes]; + [self storeValue:self.country in:dictionary forAttribute:kUserAttributeCountry includePrivate:includePrivate privateAttributes:combinedPrivateAttributes redactedAttributes:redactedPrivateAttributes]; + [self storeValue:self.name in:dictionary forAttribute:kUserAttributeName includePrivate:includePrivate privateAttributes:combinedPrivateAttributes redactedAttributes:redactedPrivateAttributes]; + [self storeValue:self.firstName in:dictionary forAttribute:kUserAttributeFirstName includePrivate:includePrivate privateAttributes:combinedPrivateAttributes redactedAttributes:redactedPrivateAttributes]; + [self storeValue:self.lastName in:dictionary forAttribute:kUserAttributeLastName includePrivate:includePrivate privateAttributes:combinedPrivateAttributes redactedAttributes:redactedPrivateAttributes]; + [self storeValue:self.email in:dictionary forAttribute:kUserAttributeEmail includePrivate:includePrivate privateAttributes:combinedPrivateAttributes redactedAttributes:redactedPrivateAttributes]; + [self storeValue:self.avatar in:dictionary forAttribute:kUserAttributeAvatar includePrivate:includePrivate privateAttributes:combinedPrivateAttributes redactedAttributes:redactedPrivateAttributes]; + [self storeValue:@(self.anonymous) in:dictionary forAttribute:kUserAttributeAnonymous includePrivate:includePrivate privateAttributes:combinedPrivateAttributes redactedAttributes:redactedPrivateAttributes]; + [self storeValue:[[NSDateFormatter userDateFormatter] stringFromDate:self.updatedAt] in:dictionary forAttribute:kUserAttributeUpdatedAt includePrivate:includePrivate privateAttributes:combinedPrivateAttributes redactedAttributes:redactedPrivateAttributes]; + + NSDictionary *customDict = [self customDictionaryIncludingPrivate:includePrivate privateAttributes:combinedPrivateAttributes redactedAttributes:redactedPrivateAttributes]; + if (customDict.count > 0) { + dictionary[kUserAttributeCustom] = customDict; } - - return dictionary; + + if (!includePrivate && redactedPrivateAttributes.count > 0) { + dictionary[kUserAttributePrivateAttributes] = [redactedPrivateAttributes allObjects]; + } + + if (includeFlags && self.config.featuresJsonDictionary) { + dictionary[kUserAttributeConfig] = [[self.config dictionaryValue] objectForKey:kFeaturesJsonDictionaryKey]; + } + + return [dictionary copy]; +} + +- (void)storeValue:(id)value in:(NSMutableDictionary*)dictionary forAttribute:(NSString*)attribute includePrivate:(BOOL)includePrivate privateAttributes:(NSArray*)privateAttributes redactedAttributes:(NSMutableSet*)redactedAttributes { + if (!value) { return; } + if (!includePrivate && [privateAttributes containsObject:attribute]) { + [redactedAttributes addObject:attribute]; + return; + } + dictionary[attribute] = value; +} + +- (NSDictionary*)customDictionaryIncludingPrivate:(BOOL)includePrivate privateAttributes:(NSArray*)privateAttributes redactedAttributes:(NSMutableSet*)redactedAttributes { + NSMutableDictionary *customDict = [[NSMutableDictionary alloc] initWithDictionary:self.custom]; + if (!includePrivate) { + if (customDict.count > 0 && [privateAttributes containsObject:kUserAttributeCustom]) { + [customDict removeAllObjects]; + [redactedAttributes addObject:kUserAttributeCustom]; + } else { + for (NSString *customKey in [self.custom allKeys]) { + if (self.custom[customKey] && [privateAttributes containsObject:customKey]) { + [customDict removeObjectForKey:customKey]; + [redactedAttributes addObject:customKey]; + } + } + } + } + + if (self.device) { customDict[kUserAttributeDevice] = self.device; } + if (self.os) { customDict[kUserAttributeOs] = self.os; } + return [customDict copy]; } - (void)encodeWithCoder:(NSCoder *)encoder { //Encode properties, other class variables, etc - [encoder encodeObject:self.key forKey:kKeyKey]; - [encoder encodeObject:self.ip forKey:kIpKey]; - [encoder encodeObject:self.country forKey:kCountryKey]; - [encoder encodeObject:self.name forKey:kNameKey]; - [encoder encodeObject:self.firstName forKey:kFirstNameKey]; - [encoder encodeObject:self.lastName forKey:kLastNameKey]; - [encoder encodeObject:self.email forKey:kEmailKey]; - [encoder encodeObject:self.avatar forKey:kAvatarKey]; - [encoder encodeObject:self.custom forKey:kCustomKey]; - [encoder encodeObject:self.updatedAt forKey:kUpdatedAtKey]; - [encoder encodeObject:self.config forKey:kConfigKey]; - [encoder encodeBool:self.anonymous forKey:kAnonymousKey]; - [encoder encodeObject:self.device forKey:kDeviceKey]; - [encoder encodeObject:self.os forKey:kOsKey]; + [encoder encodeObject:self.key forKey:kUserAttributeKey]; + [encoder encodeObject:self.ip forKey:kUserAttributeIp]; + [encoder encodeObject:self.country forKey:kUserAttributeCountry]; + [encoder encodeObject:self.name forKey:kUserAttributeName]; + [encoder encodeObject:self.firstName forKey:kUserAttributeFirstName]; + [encoder encodeObject:self.lastName forKey:kUserAttributeLastName]; + [encoder encodeObject:self.email forKey:kUserAttributeEmail]; + [encoder encodeObject:self.avatar forKey:kUserAttributeAvatar]; + [encoder encodeObject:self.custom forKey:kUserAttributeCustom]; + [encoder encodeObject:self.updatedAt forKey:kUserAttributeUpdatedAt]; + [encoder encodeObject:self.config forKey:kUserAttributeConfig]; + [encoder encodeBool:self.anonymous forKey:kUserAttributeAnonymous]; + [encoder encodeObject:self.device forKey:kUserAttributeDevice]; + [encoder encodeObject:self.os forKey:kUserAttributeOs]; + [encoder encodeObject:self.privateAttributes forKey:kUserAttributePrivateAttributes]; } - (id)initWithCoder:(NSCoder *)decoder { if((self = [super init])) { //Decode properties, other class vars - self.key = [decoder decodeObjectForKey:kKeyKey]; - self.ip = [decoder decodeObjectForKey:kIpKey]; - self.country = [decoder decodeObjectForKey:kCountryKey]; - self.name = [decoder decodeObjectForKey:kNameKey]; - self.firstName = [decoder decodeObjectForKey:kFirstNameKey]; - self.lastName = [decoder decodeObjectForKey:kLastNameKey]; - self.email = [decoder decodeObjectForKey:kEmailKey]; - self.avatar = [decoder decodeObjectForKey:kAvatarKey]; - self.custom = [decoder decodeObjectForKey:kCustomKey]; - self.updatedAt = [decoder decodeObjectForKey:kUpdatedAtKey]; - self.config = [decoder decodeObjectForKey:kConfigKey]; - self.anonymous = [decoder decodeBoolForKey:kAnonymousKey]; - self.device = [decoder decodeObjectForKey:kDeviceKey]; - self.os = [decoder decodeObjectForKey:kOsKey]; + self.key = [decoder decodeObjectForKey:kUserAttributeKey]; + self.ip = [decoder decodeObjectForKey:kUserAttributeIp]; + self.country = [decoder decodeObjectForKey:kUserAttributeCountry]; + self.name = [decoder decodeObjectForKey:kUserAttributeName]; + self.firstName = [decoder decodeObjectForKey:kUserAttributeFirstName]; + self.lastName = [decoder decodeObjectForKey:kUserAttributeLastName]; + self.email = [decoder decodeObjectForKey:kUserAttributeEmail]; + self.avatar = [decoder decodeObjectForKey:kUserAttributeAvatar]; + self.custom = [decoder decodeObjectForKey:kUserAttributeCustom]; + self.updatedAt = [decoder decodeObjectForKey:kUserAttributeUpdatedAt]; + self.config = [decoder decodeObjectForKey:kUserAttributeConfig]; + self.anonymous = [decoder decodeBoolForKey:kUserAttributeAnonymous]; + self.device = [decoder decodeObjectForKey:kUserAttributeDevice]; + self.os = [decoder decodeObjectForKey:kUserAttributeOs]; + self.privateAttributes = [decoder decodeObjectForKey:kUserAttributePrivateAttributes]; } return self; } @@ -107,26 +144,23 @@ - (id)initWithDictionary:(NSDictionary *)dictionary { if((self = [self init])) { //Process json that comes down from server - NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; - [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]; - [formatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]]; - - self.key = [dictionary objectForKey: kKeyKey]; - self.ip = [dictionary objectForKey: kIpKey]; - self.country = [dictionary objectForKey: kCountryKey]; - self.email = [dictionary objectForKey: kEmailKey]; - self.name = [dictionary objectForKey: kNameKey]; - self.firstName = [dictionary objectForKey: kFirstNameKey]; - self.lastName = [dictionary objectForKey: kLastNameKey]; - self.avatar = [dictionary objectForKey: kAvatarKey]; - self.custom = [dictionary objectForKey: kCustomKey]; + self.key = [dictionary objectForKey: kUserAttributeKey]; + self.ip = [dictionary objectForKey: kUserAttributeIp]; + self.country = [dictionary objectForKey: kUserAttributeCountry]; + self.email = [dictionary objectForKey: kUserAttributeEmail]; + self.name = [dictionary objectForKey: kUserAttributeName]; + self.firstName = [dictionary objectForKey: kUserAttributeFirstName]; + self.lastName = [dictionary objectForKey: kUserAttributeLastName]; + self.avatar = [dictionary objectForKey: kUserAttributeAvatar]; + self.custom = [dictionary objectForKey: kUserAttributeCustom]; if (self.custom) { - self.device = [self.custom objectForKey: kDeviceKey]; - self.os = [self.custom objectForKey: kOsKey]; + self.device = [self.custom objectForKey: kUserAttributeDevice]; + self.os = [self.custom objectForKey: kUserAttributeOs]; } - self.anonymous = [[dictionary objectForKey: kAnonymousKey] boolValue]; - self.updatedAt = [formatter dateFromString:[dictionary objectForKey:kUpdatedAtKey]]; - self.config = [[LDFlagConfigModel alloc] initWithDictionary:[dictionary objectForKey:kConfigKey]]; + self.anonymous = [[dictionary objectForKey: kUserAttributeAnonymous] boolValue]; + self.updatedAt = [[NSDateFormatter userDateFormatter] dateFromString:[dictionary objectForKey:kUserAttributeUpdatedAt]]; + self.config = [[LDFlagConfigModel alloc] initWithDictionary:[dictionary objectForKey:kUserAttributeConfig]]; + self.privateAttributes = [dictionary objectForKey:kUserAttributePrivateAttributes]; } return self; } @@ -156,15 +190,6 @@ - (instancetype)init { return self; } -- (nonnull NSString *) convertToJson { - NSError *writeError = nil; - - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:[self dictionaryValueWithConfig:NO] options:0 error:&writeError]; - NSString *result = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; - - return result; -} - -(NSObject *) flagValue: ( NSString * __nonnull )keyName { return [self.config configFlagValue: keyName]; } @@ -175,7 +200,11 @@ -(BOOL) doesFlagExist: ( NSString * __nonnull )keyName { } -(NSString*) description { - return [self.dictionaryValue description]; + return [[self dictionaryValueWithPrivateAttributesAndFlagConfig:YES] description]; +} + ++(NSArray * __nonnull) allUserAttributes { + return @[kUserAttributeIp, kUserAttributeCountry, kUserAttributeName, kUserAttributeFirstName, kUserAttributeLastName, kUserAttributeEmail, kUserAttributeAvatar, kUserAttributeCustom]; } @end diff --git a/Darkly/NSDateFormatter+LDUserModel.h b/Darkly/NSDateFormatter+LDUserModel.h new file mode 100644 index 00000000..85511944 --- /dev/null +++ b/Darkly/NSDateFormatter+LDUserModel.h @@ -0,0 +1,13 @@ +// +// NSDateFormatter+LDUserModel.h +// Darkly +// +// Created by Mark Pokorny on 12/21/17. +JMJ +// Copyright © 2017 LaunchDarkly. All rights reserved. +// + +#import + +@interface NSDateFormatter (LDUserModel) ++(instancetype)userDateFormatter; +@end diff --git a/Darkly/NSDateFormatter+LDUserModel.m b/Darkly/NSDateFormatter+LDUserModel.m new file mode 100644 index 00000000..b68dff67 --- /dev/null +++ b/Darkly/NSDateFormatter+LDUserModel.m @@ -0,0 +1,18 @@ +// +// NSDateFormatter+LDUserModel.m +// Darkly +// +// Created by Mark Pokorny on 12/21/17. +JMJ +// Copyright © 2017 LaunchDarkly. All rights reserved. +// + +#import "NSDateFormatter+LDUserModel.h" + +@implementation NSDateFormatter (LDUserModel) ++(instancetype)userDateFormatter { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]; + [formatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]]; + return formatter; +} +@end diff --git a/Darkly/NSDictionary+JSON.h b/Darkly/NSDictionary+JSON.h index 403a26c6..35108695 100644 --- a/Darkly/NSDictionary+JSON.h +++ b/Darkly/NSDictionary+JSON.h @@ -5,5 +5,5 @@ #import @interface NSDictionary (BVJSONString) --(NSString*) ld_jsonString; +-(nullable NSString*) jsonString; @end diff --git a/Darkly/NSDictionary+JSON.m b/Darkly/NSDictionary+JSON.m index fbe59a3a..9435d3d1 100644 --- a/Darkly/NSDictionary+JSON.m +++ b/Darkly/NSDictionary+JSON.m @@ -6,14 +6,10 @@ @implementation NSDictionary (BVJSONString) --(NSString*) ld_jsonString { - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self - options:0 - error:nil]; - if (! jsonData) { - return nil; - } else { - return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; - } +-(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/DarklyTests/Categories/LDUserModel+Equatable.h b/DarklyTests/Categories/LDUserModel+Equatable.h index 60b29273..51c63c37 100644 --- a/DarklyTests/Categories/LDUserModel+Equatable.h +++ b/DarklyTests/Categories/LDUserModel+Equatable.h @@ -8,6 +8,15 @@ #import +extern NSString * const kUserAttributeKey; +extern NSString * const kUserAttributeUpdatedAt; +extern NSString * const kUserAttributeConfig; +extern NSString * const kUserAttributeAnonymous; +extern NSString * const kUserAttributePrivateAttributes; +extern NSString * const kUserAttributeDevice; +extern NSString * const kUserAttributeOs; + @interface LDUserModel (Equatable) --(BOOL) isEqual:(id)object ignoringProperties:(NSArray*)ignoredProperties; +-(BOOL)isEqual:(id)object ignoringAttributes:(NSArray*)ignoredAttributes; +-(BOOL)matchesDictionary:(NSDictionary *)dictionary includeFlags:(BOOL)includeConfig includePrivateAttributes:(BOOL)includePrivate privateAttributes:(NSArray *)privateAttributes; @end diff --git a/DarklyTests/Categories/LDUserModel+Equatable.m b/DarklyTests/Categories/LDUserModel+Equatable.m index 02ad1314..7cfedaae 100644 --- a/DarklyTests/Categories/LDUserModel+Equatable.m +++ b/DarklyTests/Categories/LDUserModel+Equatable.m @@ -7,17 +7,195 @@ // #import "LDUserModel+Equatable.h" +#import "LDUserModel+Testable.h" #import "NSDictionary+StringKey_Matchable.h" +#import "NSDateFormatter+LDUserModel.h" @implementation LDUserModel (Equatable) --(BOOL) isEqual:(id)object ignoringProperties:(NSArray*)ignoredProperties { +-(BOOL) isEqual:(id)object ignoringAttributes:(NSArray*)ignoredAttributes { LDUserModel *otherUser = (LDUserModel*)object; if (otherUser == nil) { return NO; } - NSDictionary *dictionary = [self dictionaryValue]; - NSDictionary *otherDictionary = [otherUser dictionaryValue]; - NSArray *differingKeys = [dictionary keysWithDifferentValuesIn: otherDictionary ignoringKeys: ignoredProperties]; + NSDictionary *dictionary = [self dictionaryValueWithFlags:YES includePrivateAttributes:YES config:nil includePrivateAttributeList:YES]; + NSDictionary *otherDictionary = [otherUser dictionaryValueWithFlags:YES includePrivateAttributes:YES config:nil includePrivateAttributeList:YES]; + NSArray *differingKeys = [dictionary keysWithDifferentValuesIn: otherDictionary ignoringKeys: ignoredAttributes]; return (differingKeys == nil || [differingKeys count] == 0); } + +-(BOOL)matchesDictionary:(NSDictionary *)dictionary includeFlags:(BOOL)includeConfig includePrivateAttributes:(BOOL)includePrivate privateAttributes:(NSArray *)privateAttributes { + NSString *matchingFailureReason = @"Dictionary value does not match LDUserModel attribute: %@"; + NSString *dictionaryContainsAttributeFailureReason = @"Dictionary contains private attribute: %@"; + NSString *privateAttributeListContainsFailureReason = @"Private Attributes List contains private attribute: %@"; + NSString *privateAttributeListDoesNotContainFailureReason = @"Private Attributes List does not contain private attribute: %@"; + + if (![self.key isEqualToString:dictionary[kUserAttributeKey]]) { + NSLog(matchingFailureReason, kUserAttributeKey); + return NO; + } + + NSArray *stringAttributes = @[kUserAttributeIp, kUserAttributeCountry, kUserAttributeName, kUserAttributeFirstName, kUserAttributeLastName, kUserAttributeEmail, kUserAttributeAvatar]; + for (NSString *attribute in stringAttributes) { + if (!includePrivate && [privateAttributes containsObject:attribute]) { + if (dictionary[attribute] != nil) { + NSLog(@"Dictionary contains attribute %@", attribute); + return NO; + } + continue; + } + id property = [self propertyForAttribute:attribute]; + NSString *dictionaryAttribute = dictionary[attribute]; + if (!property && !dictionaryAttribute) { continue; } + if (![[self propertyForAttribute:attribute] isEqualToString:dictionary[attribute]]) { + NSLog(matchingFailureReason, attribute); + return NO; + } + } + + if (![[[NSDateFormatter userDateFormatter] stringFromDate:self.updatedAt] isEqualToString:dictionary[kUserAttributeUpdatedAt]]) { + NSLog(matchingFailureReason, kUserAttributeUpdatedAt); + return NO; + } + + if (!includePrivate && [privateAttributes containsObject:kUserAttributeCustom]) { + if (dictionary[kUserAttributeCustom] != nil) { + NSMutableDictionary *customDictionary = [dictionary[kUserAttributeCustom] mutableCopy]; + + for (NSString *attribute in @[kUserAttributeDevice, kUserAttributeOs]) { + id property = [self propertyForAttribute:attribute]; + NSString *dictionaryAttribute = customDictionary[attribute]; + if (property && ![property isEqualToString:dictionaryAttribute]) { + NSLog(matchingFailureReason, attribute); + return NO; + } + } + + [customDictionary removeObjectsForKeys:@[kUserAttributeDevice, kUserAttributeOs]]; + + if (customDictionary.count > 0) { + NSLog(dictionaryContainsAttributeFailureReason, kUserAttributeCustom); + return NO; + } + } + } else { + NSDictionary *customDictionary = dictionary[kUserAttributeCustom]; + + for (NSString *customAttribute in self.custom.allKeys) { + if (!includePrivate && [privateAttributes containsObject:customAttribute]) { + if (customDictionary[customAttribute] != nil) { + NSLog(dictionaryContainsAttributeFailureReason, customAttribute); + return NO; + } + continue; + } + + //NOTE: The stubbed custom dictionary only contains string values... + if ([self.custom[customAttribute] isKindOfClass:[NSString class]] && ![self.custom[customAttribute] isEqualToString:customDictionary[customAttribute]]) { + NSLog(dictionaryContainsAttributeFailureReason, customAttribute); + return NO; + } + if (![self.custom[customAttribute] isKindOfClass:[NSString class]]) { NSLog(@"WARNING: Non-string type contained in LDUserModel.custom at the key %@", customAttribute); } + } + + for (NSString *attribute in @[kUserAttributeDevice, kUserAttributeOs]) { + if ([self propertyForAttribute:attribute] == nil) { continue; } + if (!includePrivate && [privateAttributes containsObject:attribute]) { + if (customDictionary[attribute] != nil) { + NSLog(dictionaryContainsAttributeFailureReason, attribute); + return NO; + } + continue; + } + if (![[self propertyForAttribute:attribute] isEqualToString:customDictionary[attribute]]) { + NSLog(matchingFailureReason, attribute); + return NO; + } + } + } + + if (self.anonymous != [dictionary[kUserAttributeAnonymous] boolValue]) { + NSLog(matchingFailureReason, kUserAttributeAnonymous); + return NO; + } + + NSDictionary *dictionaryConfig = dictionary[kUserAttributeConfig]; + if (includeConfig) { + NSDictionary *config = self.config.featuresJsonDictionary; + if ( (config && ![config isEqual:dictionaryConfig]) || (!config && dictionaryConfig) ) { + NSLog(matchingFailureReason, kUserAttributeConfig); + return NO; + } + } else { + if (dictionaryConfig) { + NSLog(matchingFailureReason, kUserAttributeConfig); + return NO; + } + } + + NSArray *privateAttributeList = dictionary[kUserAttributePrivateAttributes]; + if (includePrivate) { + if (privateAttributeList) { + NSLog(dictionaryContainsAttributeFailureReason, kUserAttributePrivateAttributes); + return NO; + } + } else { + // !includePrivate + if (privateAttributeList && privateAttributeList.count == 0) { + NSLog(dictionaryContainsAttributeFailureReason, kUserAttributePrivateAttributes); + return NO; + } + for (NSString *attribute in privateAttributes) { + id property = [self propertyForAttribute:attribute]; + if ([attribute isEqualToString:kUserAttributeCustom]) { + //Specialized handling because the dictionary can exist with ONLY the device & os, but no other keys + NSMutableDictionary *customProperty = [property mutableCopy]; + [customProperty removeObjectsForKeys:@[kUserAttributeDevice, kUserAttributeOs]]; + if (customProperty.count == 0 && [privateAttributeList containsObject:kUserAttributeCustom]) { + NSLog(privateAttributeListContainsFailureReason, kUserAttributeCustom); + return NO; + } + if (customProperty.count > 0 && ![privateAttributeList containsObject:kUserAttributeCustom]) { + NSLog(privateAttributeListDoesNotContainFailureReason, kUserAttributeCustom); + return NO; + } + } else { + if (!property && [privateAttributeList containsObject:attribute] ) { + NSLog(privateAttributeListContainsFailureReason, attribute); + return NO; + } + if (property && ![privateAttributeList containsObject:attribute]) { + NSLog(privateAttributeListDoesNotContainFailureReason, attribute); + return NO; + } + } + } + } + + return YES; +} + +-(id)propertyForAttribute:(NSString*)attribute { + NSArray *attributeList = @[kUserAttributeKey, kUserAttributeIp, kUserAttributeCountry, kUserAttributeName, kUserAttributeFirstName, kUserAttributeLastName, kUserAttributeEmail, kUserAttributeAvatar, kUserAttributeCustom, kUserAttributeUpdatedAt, kUserAttributeConfig, kUserAttributeAnonymous, kUserAttributeDevice, kUserAttributeOs]; + NSUInteger attributeIndex = [attributeList indexOfObject:attribute]; + if (attributeIndex != NSNotFound) { + switch (attributeIndex) { + case 0: return self.key; + case 1: return self.ip; + case 2: return self.country; + case 3: return self.name; + case 4: return self.firstName; + case 5: return self.lastName; + case 6: return self.email; + case 7: return self.avatar; + case 8: return self.custom; + case 9: return self.updatedAt; + case 10: return self.config; + case 11: return @(self.anonymous); + case 12: return self.device; + case 13: return self.os; + } + } + + return self.custom[attribute]; +} @end diff --git a/DarklyTests/Categories/LDUserModel+Stub.h b/DarklyTests/Categories/LDUserModel+Stub.h new file mode 100644 index 00000000..89e3d1f9 --- /dev/null +++ b/DarklyTests/Categories/LDUserModel+Stub.h @@ -0,0 +1,26 @@ +// +// LDUserModel+Stub.h +// DarklyTests +// +// Created by Mark Pokorny on 12/20/17. +JMJ +// Copyright © 2017 LaunchDarkly. All rights reserved. +// + +#import + +extern NSString * const userModelStubIp; +extern NSString * const userModelStubCountry; +extern NSString * const userModelStubName; +extern NSString * const userModelStubFirstName; +extern NSString * const userModelStubLastName; +extern NSString * const userModelStubEmail; +extern NSString * const userModelStubAvatar; +extern NSString * const userModelStubDevice; +extern NSString * const userModelStubOs; +extern NSString * const userModelStubCustomKey; +extern NSString * const userModelStubCustomValue; + +@interface LDUserModel (Stub) ++(instancetype)stubWithKey:(NSString*)key; ++(NSDictionary*)customStub; +@end diff --git a/DarklyTests/Categories/LDUserModel+Stub.m b/DarklyTests/Categories/LDUserModel+Stub.m new file mode 100644 index 00000000..02fc42eb --- /dev/null +++ b/DarklyTests/Categories/LDUserModel+Stub.m @@ -0,0 +1,48 @@ +// +// LDUserModel+Stub.m +// DarklyTests +// +// Created by Mark Pokorny on 12/20/17. +JMJ +// Copyright © 2017 LaunchDarkly. All rights reserved. +// + +#import "Darkly/LDUserModel.h" +#import "LDUserModel+Stub.h" +#import "LDUserModel+Equatable.h" +#import "LDFlagConfigModel+Testable.h" + +NSString * const userModelStubIp = @"123.456.789.000"; +NSString * const userModelStubCountry = @"stubCountry"; +NSString * const userModelStubName = @"stubName"; +NSString * const userModelStubFirstName = @"stubFirstName"; +NSString * const userModelStubLastName = @"stubLastName"; +NSString * const userModelStubEmail = @"stub@email.com"; +NSString * const userModelStubAvatar = @"stubAvatar"; +NSString * const userModelStubDevice = @"iPhone"; +NSString * const userModelStubOs = @"IOS 11.2.1"; +NSString * const userModelStubCustomKey = @"userModelStubCustomKey"; +NSString * const userModelStubCustomValue = @"userModelStubCustomValue"; + +@implementation LDUserModel (Stub) ++(instancetype)stubWithKey:(NSString*)key { + LDUserModel *stub = [[LDUserModel alloc] init]; + stub.key = key.length ? key : [[NSUUID UUID] UUIDString]; + stub.ip = userModelStubIp; + stub.country = userModelStubCountry; + stub.name = userModelStubName; + stub.firstName = userModelStubFirstName; + stub.lastName = userModelStubLastName; + stub.email = userModelStubEmail; + stub.avatar = userModelStubAvatar; + stub.custom = [LDUserModel customStub]; + stub.config = [LDFlagConfigModel flagConfigFromJsonFileNamed:@"userStubFlags"]; + + return stub; +} + ++(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. + return @{userModelStubCustomKey: userModelStubCustomValue}; +} +@end diff --git a/DarklyTests/Categories/LDUserModel+Testable.h b/DarklyTests/Categories/LDUserModel+Testable.h new file mode 100644 index 00000000..0ee6cbd0 --- /dev/null +++ b/DarklyTests/Categories/LDUserModel+Testable.h @@ -0,0 +1,16 @@ +// +// LDUserModel+Testable.h +// DarklyTests +// +// Created by Mark Pokorny on 1/9/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import + +@interface LDUserModel (Testable) +/** + -[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. + */ +-(NSDictionary *)dictionaryValueWithFlags:(BOOL)includeFlags includePrivateAttributes:(BOOL)includePrivate config:(LDConfig*)config includePrivateAttributeList:(BOOL)includePrivateList; +@end diff --git a/DarklyTests/Categories/LDUserModel+Testable.m b/DarklyTests/Categories/LDUserModel+Testable.m new file mode 100644 index 00000000..55a1d761 --- /dev/null +++ b/DarklyTests/Categories/LDUserModel+Testable.m @@ -0,0 +1,18 @@ +// +// LDUserModel+Testable.m +// DarklyTests +// +// Created by Mark Pokorny on 1/9/18. +JMJ +// Copyright © 2018 LaunchDarkly. All rights reserved. +// + +#import "LDUserModel+Testable.h" +#import "LDUserModel+Equatable.h" + +@implementation LDUserModel (Testable) +-(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; + return dictionary; +} +@end diff --git a/DarklyTests/Fixtures/userStubFlags.json b/DarklyTests/Fixtures/userStubFlags.json new file mode 100644 index 00000000..7a6576d2 --- /dev/null +++ b/DarklyTests/Fixtures/userStubFlags.json @@ -0,0 +1,16 @@ +{ + "_links": + { + "href":"/api/eval/users/eyJrZXkiOiAiamVmZkB0ZXN0LmNvbSJ9", + "type":"application/json" + }, + "devices.hasipad":false, + "isAString":"test", + "isAArray":[0,1,2], + "isAObject": + { + "key": 0 + }, + "isANumber":0, + "isABawler":true +} diff --git a/DarklyTests/LDClientManagerTest.m b/DarklyTests/LDClientManagerTest.m index 5a722ced..c1506daa 100644 --- a/DarklyTests/LDClientManagerTest.m +++ b/DarklyTests/LDClientManagerTest.m @@ -177,7 +177,7 @@ - (void)testSyncWithServerForEventsWhenEventsExist { LDClientManager *clientManager = [LDClientManager sharedInstance]; [clientManager setOnline:YES]; - [dataManagerMock allEventsJsonArray:^(NSArray *array) { + [dataManagerMock allEventDictionaries:^(NSArray *array) { OCMStub(array).andReturn(testData); [clientManager syncWithServerForEvents]; @@ -201,7 +201,7 @@ - (void)testDoNotSyncWithServerForEventsWhenEventsDoNotExist { - (void)testSyncWithServerForEventsNotProcessedWhenOffline { NSData *testData = [[NSData alloc] init]; - [dataManagerMock allEventsJsonArray:^(NSArray *array) { + [dataManagerMock allEventDictionaries:^(NSArray *array) { OCMStub(array).andReturn(testData); [[requestManagerMock reject] performEventRequest:[OCMArg isEqual:testData]]; @@ -256,9 +256,10 @@ - (void)testWillEnterForeground { - (void)testProcessedEventsSuccessWithProcessedEvents { LDEventModel *event = [[LDEventModel alloc] initFeatureEventWithKey:@"blah" keyValue:[NSNumber numberWithBool:NO] defaultKeyValue:[NSNumber numberWithBool:NO] userValue:[[LDClient sharedInstance] ldUser]]; + LDConfig *config = [[LDConfig alloc] initWithMobileKey:@"testMobileKey"]; LDClientManager *clientManager = [LDClientManager sharedInstance]; - [clientManager processedEvents:YES jsonEventArray:@[[event dictionaryValue]]]; + [clientManager processedEvents:YES jsonEventArray:@[[event dictionaryValueUsingConfig:config]]]; OCMVerify([dataManagerMock deleteProcessedEvents:[OCMArg any]]); } @@ -397,7 +398,7 @@ - (void)testFlushEventsWhenOnline { LDClientManager *clientManager = [LDClientManager sharedInstance]; clientManager.online = YES; - [dataManagerMock allEventsJsonArray:^(NSArray *array) { + [dataManagerMock allEventDictionaries:^(NSArray *array) { OCMStub(array).andReturn(testData); [clientManager flushEvents]; @@ -411,7 +412,7 @@ - (void)testFlushEventsWhenOffline { LDClientManager *clientManager = [LDClientManager sharedInstance]; clientManager.online = NO; - [dataManagerMock allEventsJsonArray:^(NSArray *array) { + [dataManagerMock allEventDictionaries:^(NSArray *array) { OCMStub(array).andReturn(testData); [clientManager flushEvents]; diff --git a/DarklyTests/LDClientTest.m b/DarklyTests/LDClientTest.m index 1d06d4e3..f29ceb0a 100644 --- a/DarklyTests/LDClientTest.m +++ b/DarklyTests/LDClientTest.m @@ -458,11 +458,11 @@ -(void)testToggleCreatesEventWithCorrectArguments { NSString *toggleName = @"test"; BOOL fallbackValue = YES; LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; - OCMStub([self.dataManagerMock createFeatureEvent:[OCMArg any] keyValue:[OCMArg any] defaultKeyValue:[OCMArg any]]); + OCMStub([self.dataManagerMock createFeatureEvent:[OCMArg any] keyValue:[OCMArg any] defaultKeyValue:[OCMArg any] user:[OCMArg any] config:[OCMArg any]]); [[LDClient sharedInstance] start:config withUserBuilder:nil]; [[LDClient sharedInstance] boolVariation:toggleName fallback:fallbackValue]; - OCMVerify([self.dataManagerMock createFeatureEvent:toggleName keyValue:[NSNumber numberWithBool:fallbackValue] defaultKeyValue:[NSNumber numberWithBool:fallbackValue]]); + OCMVerify([self.dataManagerMock createFeatureEvent:toggleName keyValue:[NSNumber numberWithBool:fallbackValue] defaultKeyValue:[NSNumber numberWithBool:fallbackValue] user:[OCMArg isKindOfClass:[LDUserModel class]] config:config]); [self.dataManagerMock stopMocking]; } @@ -475,12 +475,11 @@ - (void)testTrackWithStart { LDConfig *config = [[LDConfig alloc] initWithMobileKey:kTestMobileKey]; [[LDClient sharedInstance] start:config withUserBuilder:nil]; - OCMStub([self.dataManagerMock createCustomEvent:[OCMArg isKindOfClass:[NSString class]] withCustomValuesDictionary:[OCMArg isKindOfClass:[NSDictionary class]]]); + OCMStub([self.dataManagerMock createCustomEvent:[OCMArg isKindOfClass:[NSString class]] withCustomValuesDictionary:[OCMArg isKindOfClass:[NSDictionary class]] user:[OCMArg any] config:[OCMArg any]]); XCTAssertTrue([[LDClient sharedInstance] track:@"test" data:customData]); - OCMVerify([self.dataManagerMock createCustomEvent: @"test" - withCustomValuesDictionary: customData]); + OCMVerify([self.dataManagerMock createCustomEvent: @"test" withCustomValuesDictionary: customData user:[OCMArg isKindOfClass:[LDUserModel class]] config:config]); } - (void)testOfflineWithoutStart { diff --git a/DarklyTests/LDConfigTest.m b/DarklyTests/LDConfigTest.m index aca77d5c..f77b3ccb 100644 --- a/DarklyTests/LDConfigTest.m +++ b/DarklyTests/LDConfigTest.m @@ -124,6 +124,14 @@ - (void)testConfigOverrideStreaming { XCTAssertFalse(config.streaming); } +- (void)testConfigSetPrivateAttributes { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; + XCTAssertNil(config.privateUserAttributes); + + config.privateUserAttributes = LDUserModel.allUserAttributes; + XCTAssertEqualObjects(config.privateUserAttributes, LDUserModel.allUserAttributes); +} + - (void)testConfigOverrideDebug { LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; config.debugEnabled = YES; diff --git a/DarklyTests/LDUserBuilderTest.m b/DarklyTests/LDUserBuilderTest.m index 8aecd1a7..3d83b7b2 100644 --- a/DarklyTests/LDUserBuilderTest.m +++ b/DarklyTests/LDUserBuilderTest.m @@ -32,6 +32,7 @@ - (void)testUserDefaultValues { XCTAssertTrue([user anonymous]); XCTAssertNotNil([user device]); XCTAssertNotNil([user os]); + XCTAssertNil([user privateAttributes]); } - (void)testUserWithInputValues { @@ -81,4 +82,17 @@ - (void)testUserSetAnonymous { XCTAssertTrue(user.anonymous); } +- (void)testSetPrivateAttributes { + 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]); +} + @end diff --git a/DarklyTests/Models/LDDataManagerTest.m b/DarklyTests/Models/LDDataManagerTest.m index 397a1d8e..193023c4 100644 --- a/DarklyTests/Models/LDDataManagerTest.m +++ b/DarklyTests/Models/LDDataManagerTest.m @@ -61,20 +61,20 @@ - (void)testisFlagOnForKey { -(void)testAllEventsDictionaryArray { NSString *eventKey1 = @"foo"; NSString *eventKey2 = @"fi"; - - [[LDDataManager sharedManager] createFeatureEvent:eventKey1 keyValue:[NSNumber numberWithBool:NO] defaultKeyValue:[NSNumber numberWithBool:NO]]; - [[LDDataManager sharedManager] createCustomEvent:eventKey2 withCustomValuesDictionary:@{@"carrot": @"cake"}]; + LDConfig *config = [[LDConfig alloc] initWithMobileKey:@"stubMobileKey"]; + [[LDDataManager sharedManager] createFeatureEvent:eventKey1 keyValue:[NSNumber numberWithBool:NO] defaultKeyValue:[NSNumber numberWithBool:NO] user:self.user config:config]; + [[LDDataManager sharedManager] createCustomEvent:eventKey2 withCustomValuesDictionary:@{@"carrot": @"cake"} user:self.user config:config]; XCTestExpectation *expectation = [self expectationWithDescription:@"All events dictionary expectation"]; - [[LDDataManager sharedManager] allEventsJsonArray:^(NSArray *array) { - NSMutableArray *eventKeyArray = [[NSMutableArray alloc] init]; - for (NSDictionary *eventDictionary in array) { - [eventKeyArray addObject:[eventDictionary objectForKey:@"key"]]; + [[LDDataManager sharedManager] allEventDictionaries:^(NSArray *eventDictionaries) { + NSMutableArray *eventKeys = [[NSMutableArray alloc] init]; + for (NSDictionary *eventDictionary in eventDictionaries) { + [eventKeys addObject:[eventDictionary objectForKey:@"key"]]; } - XCTAssertTrue([eventKeyArray containsObject:eventKey1]); - XCTAssertTrue([eventKeyArray containsObject:eventKey2]); + XCTAssertTrue([eventKeys containsObject:eventKey1]); + XCTAssertTrue([eventKeys containsObject:eventKey2]); [expectation fulfill]; }]; @@ -82,20 +82,22 @@ -(void)testAllEventsDictionaryArray { } --(void)testAllEventsJsonData { - [[LDDataManager sharedManager] createCustomEvent:@"foo" withCustomValuesDictionary:nil]; - [[LDDataManager sharedManager] createCustomEvent:@"fi" withCustomValuesDictionary:nil]; +-(void)testAllEventDictionaries { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:@"stubMobileKey"]; + [[LDDataManager sharedManager] createCustomEvent:@"foo" withCustomValuesDictionary:nil user:self.user config:config]; + [[LDDataManager sharedManager] createCustomEvent:@"fi" withCustomValuesDictionary:nil user:self.user config:config]; XCTestExpectation *expectation = [self expectationWithDescription:@"All events json data expectation"]; - [[LDDataManager sharedManager] allEventsJsonArray:^(NSArray *array) { + [[LDDataManager sharedManager] allEventDictionaries:^(NSArray *eventDictionaries) { - NSMutableDictionary *eventDictionary = [[NSMutableDictionary alloc] init]; - for (NSDictionary *currentEventDictionary in array) { - [eventDictionary setObject:[[LDEventModel alloc] initWithDictionary:currentEventDictionary] forKey:[currentEventDictionary objectForKey:@"key"]]; + NSMutableDictionary *events = [[NSMutableDictionary alloc] init]; + for (NSDictionary *eventDictionary in eventDictionaries) { + XCTAssertTrue([eventDictionary[@"user"] isKindOfClass:[NSDictionary class]]); + [events setObject:[[LDEventModel alloc] initWithDictionary:eventDictionary] forKey:[eventDictionary objectForKey:@"key"]]; } - XCTAssertEqual([eventDictionary count], 2); + XCTAssertEqual([events count], 2); [expectation fulfill]; }]; @@ -160,18 +162,17 @@ -(void)testCreateEventAfterCapacityReached { LDDataManager *manager = [LDDataManager sharedManager]; [manager.eventsArray removeAllObjects]; - [manager createCustomEvent:@"aKey" withCustomValuesDictionary: @{@"carrot": @"cake"}]; - [manager createCustomEvent:@"aKey" withCustomValuesDictionary: @{@"carrot": @"cake"}]; - [manager createCustomEvent:@"aKey" withCustomValuesDictionary: @{@"carrot": @"cake"}]; - [manager createFeatureEvent: @"anotherKet" keyValue: [NSNumber numberWithBool:YES] defaultKeyValue: [NSNumber numberWithBool:NO]]; + [manager createCustomEvent:@"aKey" withCustomValuesDictionary: @{@"carrot": @"cake"} user:self.user config:config]; + [manager createCustomEvent:@"aKey" withCustomValuesDictionary: @{@"carrot": @"cake"} user:self.user config:config]; + [manager createCustomEvent:@"aKey" withCustomValuesDictionary: @{@"carrot": @"cake"} user:self.user config:config]; + [manager createFeatureEvent: @"anotherKet" keyValue: [NSNumber numberWithBool:YES] defaultKeyValue: [NSNumber numberWithBool:NO] user:self.user config:config]; - [manager allEventsJsonArray:^(NSArray *array) { + [manager allEventDictionaries:^(NSArray *array) { XCTAssertEqual([array count],2); [expectation fulfill]; }]; [self waitForExpectations:@[expectation] timeout:10]; - } @end diff --git a/DarklyTests/Models/LDUserModelTest.m b/DarklyTests/Models/LDUserModelTest.m index bd4a3e1e..61df0b24 100644 --- a/DarklyTests/Models/LDUserModelTest.m +++ b/DarklyTests/Models/LDUserModelTest.m @@ -5,6 +5,9 @@ #import #import "LDUserModel.h" #import "LDDataManager.h" +#import "LDUserModel.h" +#import "LDUserModel+Stub.h" +#import "LDUserModel+Testable.h" #import "LDUserModel+Equatable.h" #import "LDUserModel+JsonDecodeable.h" #import "NSMutableDictionary+NullRemovable.h" @@ -29,47 +32,403 @@ -(void)testNewUserSetupProperly { XCTAssertNotNil(user.os); XCTAssertNotNil(user.device); XCTAssertNotNil(user.updatedAt); + XCTAssertNil(user.privateAttributes); } --(void)testDictionaryValue { - NSMutableDictionary *userDict = [self userDictionaryWithUserKey:@"aKey" userName:@"John Doe" customDictionary:[self customDictionary]]; - LDUserModel *user = [[LDUserModel alloc] initWithDictionary:userDict]; - NSDictionary *targetUserDictionary = [self targetUserDictionaryFrom:userDict withConfig:YES]; - - NSDictionary *dictionaryFromUser = [user dictionaryValue]; - - XCTAssertTrue([targetUserDictionary isEqualToDictionary:dictionaryFromUser]); +-(void)testDictionaryValueWithFlags_Yes_AndPrivateProperties_Yes { + LDUserModel *userStub = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + NSMutableArray *allAttributes = [NSMutableArray arrayWithArray:[LDUserModel allUserAttributes]]; + [allAttributes addObjectsFromArray:userStub.custom.allKeys]; + + LDConfig *config = [[LDConfig alloc] initWithMobileKey:@"customMobileKey"]; + NSDictionary *testDictionary; + + for (NSString *attribute in allAttributes) { + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = @[attribute]; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:YES privateAttributes:@[attribute]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = @[attribute]; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:YES privateAttributes:@[attribute]]); + } + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = allAttributes; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:YES privateAttributes:[LDUserModel allUserAttributes]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = allAttributes; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:YES privateAttributes:[LDUserModel allUserAttributes]]); + + config.allUserAttributesPrivate = YES; + config.privateUserAttributes = nil; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:YES privateAttributes:[LDUserModel allUserAttributes]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:YES privateAttributes:nil]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = @[]; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:YES privateAttributes:@[]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = @[]; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:YES privateAttributes:@[]]); + + LDUserModel *emptyUser = [[LDUserModel alloc] init]; + emptyUser.key = [[NSUUID UUID] UUIDString]; + emptyUser.privateAttributes = [LDUserModel allUserAttributes]; + for (NSString *attribute in [LDUserModel allUserAttributes]) { + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + emptyUser.privateAttributes = @[attribute]; + testDictionary = [emptyUser dictionaryValueWithFlagConfig:YES includePrivateAttributes:YES config:config]; + XCTAssertTrue([emptyUser matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:YES privateAttributes:@[attribute]]); + } + + //Custom dictionary test cases + LDUserModel *testUser = [[LDUserModel alloc] init]; + testUser.key = [[NSUUID UUID] UUIDString]; + testUser.device = userModelStubDevice; + testUser.os = userModelStubOs; + testUser.custom = nil; + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + testUser.privateAttributes = @[kUserAttributeCustom]; + testDictionary = [testUser dictionaryValueWithFlagConfig:YES includePrivateAttributes:YES config:config]; + XCTAssertTrue([testUser matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:YES privateAttributes:@[kUserAttributeCustom]]); + + testUser = [[LDUserModel alloc] init]; + testUser.key = [[NSUUID UUID] UUIDString]; + testUser.custom = [LDUserModel customStub]; + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + testUser.privateAttributes = @[kUserAttributeCustom]; + testDictionary = [testUser dictionaryValueWithFlagConfig:YES includePrivateAttributes:YES config:config]; + XCTAssertTrue([testUser matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:YES privateAttributes:@[kUserAttributeCustom]]); } --(void)testDictionaryValueWithConfig_Yes { - NSMutableDictionary *userDict = [self userDictionaryWithUserKey:@"aKey" userName:@"John Doe" customDictionary:[self customDictionary]]; - LDUserModel *user = [[LDUserModel alloc] initWithDictionary:userDict]; - NSDictionary *targetUserDictionary = [self targetUserDictionaryFrom:userDict withConfig:YES]; - - NSDictionary *dictionaryFromUser = [user dictionaryValueWithConfig:YES]; - - XCTAssertNotNil([dictionaryFromUser objectForKey: @"config"]); - - XCTAssertTrue([targetUserDictionary isEqualToDictionary:dictionaryFromUser]); +-(void)testDictionaryValueWithFlags_Yes_AndPrivateProperties_No { + LDUserModel *userStub = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + NSMutableArray *allAttributes = [NSMutableArray arrayWithArray:[LDUserModel allUserAttributes]]; + [allAttributes addObjectsFromArray:userStub.custom.allKeys]; + LDConfig *config = [[LDConfig alloc] initWithMobileKey:@"customMobileKey"]; + NSDictionary *testDictionary; + + for (NSString *attribute in allAttributes) { + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = @[attribute]; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:NO privateAttributes:@[attribute]]); + + config.privateUserAttributes = @[attribute]; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:NO privateAttributes:@[attribute]]); + + } + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = allAttributes; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:NO privateAttributes:[LDUserModel allUserAttributes]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = allAttributes; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:NO privateAttributes:[LDUserModel allUserAttributes]]); + + config.allUserAttributesPrivate = YES; + config.privateUserAttributes = nil; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:NO privateAttributes:[LDUserModel allUserAttributes]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:NO privateAttributes:nil]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = @[]; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:NO privateAttributes:@[]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = @[]; + testDictionary = [userStub dictionaryValueWithFlagConfig:YES includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:NO privateAttributes:@[]]); + + LDUserModel *emptyUser = [[LDUserModel alloc] init]; + emptyUser.key = [[NSUUID UUID] UUIDString]; + emptyUser.privateAttributes = [LDUserModel allUserAttributes]; + for (NSString *attribute in [LDUserModel allUserAttributes]) { + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + emptyUser.privateAttributes = @[attribute]; + testDictionary = [emptyUser dictionaryValueWithFlagConfig:YES includePrivateAttributes:NO config:config]; + XCTAssertTrue([emptyUser matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:NO privateAttributes:@[attribute]]); + } + + //Custom dictionary test cases + LDUserModel *testUser = [[LDUserModel alloc] init]; + testUser.key = [[NSUUID UUID] UUIDString]; + testUser.device = userModelStubDevice; + testUser.os = userModelStubOs; + testUser.custom = nil; + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + testUser.privateAttributes = @[kUserAttributeCustom]; + testDictionary = [testUser dictionaryValueWithFlagConfig:YES includePrivateAttributes:NO config:config]; + XCTAssertTrue([testUser matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:NO privateAttributes:@[kUserAttributeCustom]]); + + testUser = [[LDUserModel alloc] init]; + testUser.key = [[NSUUID UUID] UUIDString]; + testUser.custom = [LDUserModel customStub]; + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + testUser.privateAttributes = @[kUserAttributeCustom]; + testDictionary = [testUser dictionaryValueWithFlagConfig:YES includePrivateAttributes:NO config:config]; + XCTAssertTrue([testUser matchesDictionary:testDictionary includeFlags:YES includePrivateAttributes:NO privateAttributes:@[kUserAttributeCustom]]); } --(void)testDictionaryValueWithConfig_No { - NSMutableDictionary *userDict = [self userDictionaryWithUserKey:@"aKey" userName:@"John Doe" customDictionary:[self customDictionary]]; - LDUserModel *user = [[LDUserModel alloc] initWithDictionary:userDict]; - NSDictionary *targetUserDictionary = [self targetUserDictionaryFrom:userDict withConfig:NO]; - - NSDictionary *dictionaryFromUser = [user dictionaryValueWithConfig:NO]; - - XCTAssertNil([dictionaryFromUser objectForKey: @"config"]); - - XCTAssertTrue([targetUserDictionary isEqualToDictionary:dictionaryFromUser]); +-(void)testDictionaryValueWithFlags_No_AndPrivateProperties_Yes { + LDUserModel *userStub = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + NSMutableArray *allAttributes = [NSMutableArray arrayWithArray:[LDUserModel allUserAttributes]]; + [allAttributes addObjectsFromArray:userStub.custom.allKeys]; + LDConfig *config = [[LDConfig alloc] initWithMobileKey:@"customMobileKey"]; + NSDictionary *testDictionary; + + for (NSString *attribute in allAttributes) { + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = @[attribute]; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:YES privateAttributes:@[attribute]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = @[attribute]; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:YES privateAttributes:@[attribute]]); + + } + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = allAttributes; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:YES privateAttributes:[LDUserModel allUserAttributes]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = allAttributes; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:YES privateAttributes:[LDUserModel allUserAttributes]]); + + config.allUserAttributesPrivate = YES; + config.privateUserAttributes = nil; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:YES privateAttributes:[LDUserModel allUserAttributes]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:YES privateAttributes:nil]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = @[]; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:YES privateAttributes:@[]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = @[]; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:YES config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:YES privateAttributes:@[]]); + + LDUserModel *emptyUser = [[LDUserModel alloc] init]; + emptyUser.key = [[NSUUID UUID] UUIDString]; + emptyUser.privateAttributes = [LDUserModel allUserAttributes]; + for (NSString *attribute in [LDUserModel allUserAttributes]) { + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + emptyUser.privateAttributes = @[attribute]; + testDictionary = [emptyUser dictionaryValueWithFlagConfig:NO includePrivateAttributes:YES config:config]; + XCTAssertTrue([emptyUser matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:YES privateAttributes:@[attribute]]); + } + + //Custom dictionary test cases + LDUserModel *testUser = [[LDUserModel alloc] init]; + testUser.key = [[NSUUID UUID] UUIDString]; + testUser.device = userModelStubDevice; + testUser.os = userModelStubOs; + testUser.custom = nil; + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + testUser.privateAttributes = @[kUserAttributeCustom]; + testDictionary = [testUser dictionaryValueWithFlagConfig:NO includePrivateAttributes:YES config:config]; + XCTAssertTrue([testUser matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:YES privateAttributes:@[kUserAttributeCustom]]); + + testUser = [[LDUserModel alloc] init]; + testUser.key = [[NSUUID UUID] UUIDString]; + testUser.custom = [LDUserModel customStub]; + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + testUser.privateAttributes = @[kUserAttributeCustom]; + testDictionary = [testUser dictionaryValueWithFlagConfig:NO includePrivateAttributes:YES config:config]; + XCTAssertTrue([testUser matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:YES privateAttributes:@[kUserAttributeCustom]]); } --(void)testConvertToJson { +-(void)testDictionaryValueWithFlags_No_AndPrivateProperties_No { + LDUserModel *userStub = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + NSMutableArray *allAttributes = [NSMutableArray arrayWithArray:[LDUserModel allUserAttributes]]; + [allAttributes addObjectsFromArray:userStub.custom.allKeys]; + LDConfig *config = [[LDConfig alloc] initWithMobileKey:@"customMobileKey"]; + NSDictionary *testDictionary; + + for (NSString *attribute in allAttributes) { + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = @[attribute]; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:NO privateAttributes:@[attribute]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = @[attribute]; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:NO privateAttributes:@[attribute]]); + + } + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = allAttributes; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:NO privateAttributes:[LDUserModel allUserAttributes]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = allAttributes; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:NO privateAttributes:[LDUserModel allUserAttributes]]); + + config.allUserAttributesPrivate = YES; + config.privateUserAttributes = nil; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:NO privateAttributes:[LDUserModel allUserAttributes]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:NO privateAttributes:nil]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = @[]; + userStub.privateAttributes = nil; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:NO privateAttributes:@[]]); + + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + userStub.privateAttributes = @[]; + testDictionary = [userStub dictionaryValueWithFlagConfig:NO includePrivateAttributes:NO config:config]; + XCTAssertTrue([userStub matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:NO privateAttributes:@[]]); + + LDUserModel *emptyUser = [[LDUserModel alloc] init]; + emptyUser.key = [[NSUUID UUID] UUIDString]; + emptyUser.privateAttributes = [LDUserModel allUserAttributes]; + for (NSString *attribute in [LDUserModel allUserAttributes]) { + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + emptyUser.privateAttributes = @[attribute]; + testDictionary = [emptyUser dictionaryValueWithFlagConfig:NO includePrivateAttributes:NO config:config]; + XCTAssertTrue([emptyUser matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:NO privateAttributes:@[attribute]]); + } + + //Custom dictionary test cases + LDUserModel *testUser = [[LDUserModel alloc] init]; + testUser.key = [[NSUUID UUID] UUIDString]; + testUser.device = userModelStubDevice; + testUser.os = userModelStubOs; + testUser.custom = nil; + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + testUser.privateAttributes = @[kUserAttributeCustom]; + testDictionary = [testUser dictionaryValueWithFlagConfig:NO includePrivateAttributes:NO config:config]; + XCTAssertTrue([testUser matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:NO privateAttributes:@[kUserAttributeCustom]]); + + testUser = [[LDUserModel alloc] init]; + testUser.key = [[NSUUID UUID] UUIDString]; + testUser.custom = [LDUserModel customStub]; + config.allUserAttributesPrivate = NO; + config.privateUserAttributes = nil; + testUser.privateAttributes = @[kUserAttributeCustom]; + testDictionary = [testUser dictionaryValueWithFlagConfig:NO includePrivateAttributes:NO config:config]; + XCTAssertTrue([testUser matchesDictionary:testDictionary includeFlags:NO includePrivateAttributes:NO privateAttributes:@[kUserAttributeCustom]]); +} + +-(void)testEncodeAndDecode { + LDUserModel *userStub = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + NSMutableArray *allAttributes = [NSMutableArray arrayWithArray:[LDUserModel allUserAttributes]]; + [allAttributes addObjectsFromArray:userStub.custom.allKeys]; + userStub.privateAttributes = allAttributes; + + NSData *encodedUserData = [NSKeyedArchiver archivedDataWithRootObject:userStub]; + XCTAssertNotNil(encodedUserData); + + LDUserModel *decodedUser = [NSKeyedUnarchiver unarchiveObjectWithData:encodedUserData]; + XCTAssertTrue([userStub isEqual:decodedUser ignoringAttributes:@[kUserAttributeUpdatedAt]]); +} + +-(void)testInitWithDictionary { + LDUserModel *userStub = [LDUserModel stubWithKey:[[NSUUID UUID] UUIDString]]; + NSMutableArray *allAttributes = [NSMutableArray arrayWithArray:[LDUserModel allUserAttributes]]; + [allAttributes addObjectsFromArray:userStub.custom.allKeys]; + userStub.privateAttributes = allAttributes; + + NSDictionary *userDictionary = [userStub dictionaryValueWithFlags:YES includePrivateAttributes:YES config:nil includePrivateAttributeList:YES]; + XCTAssertTrue(userDictionary && [userDictionary count]); + + LDUserModel *reinflatedUser = [[LDUserModel alloc] initWithDictionary:userDictionary]; + XCTAssertTrue([userStub isEqual:reinflatedUser ignoringAttributes:nil]); +} + +-(void)testUserJsonContainsNoWhitespace { NSMutableDictionary *userDict = [self userDictionaryWithUserKey:@"aKey" userName:@"John_Doe" customDictionary:@{@"foo": @"Foo"}]; //Keep whitespace out of strings!! LDUserModel *user = [[LDUserModel alloc] initWithDictionary:userDict]; [self validateUserModelIsEqualBehaviorUsingUserDictionary:userDict]; - NSString *jsonUser = [user convertToJson]; + NSString *jsonUser = [[user dictionaryValueWithPrivateAttributesAndFlagConfig:NO] jsonString]; //jsonUser contains no whitespace NSString *strippedJsonUser = [jsonUser stringByRemovingWhitespace]; @@ -77,7 +436,7 @@ -(void)testConvertToJson { //jsonUser converts to the same user minus config NSArray *ignoredProperties = @[@"config", @"updatedAt"]; - XCTAssertTrue([user isEqual:[LDUserModel userFrom:jsonUser] ignoringProperties:ignoredProperties]); + XCTAssertTrue([user isEqual:[LDUserModel userFrom:jsonUser] ignoringAttributes:ignoredProperties]); } - (void)testUserSave { @@ -89,7 +448,7 @@ - (void)testUserSave { [[LDDataManager sharedManager] saveUser:user]; LDUserModel *retrievedUser = [[LDDataManager sharedManager] findUserWithkey:userKey]; - XCTAssertTrue([user isEqual:retrievedUser ignoringProperties:@[@"updatedAt"]]); + XCTAssertTrue([user isEqual:retrievedUser ignoringAttributes:@[@"updatedAt"]]); } -(void)testUserBackwardsCompatibility { @@ -104,7 +463,7 @@ -(void)testUserBackwardsCompatibility { #pragma clang diagnostic pop LDUserModel *retrievedUser = [[LDDataManager sharedManager] findUserWithkey:userKey]; - XCTAssertTrue([user isEqual:retrievedUser ignoringProperties:@[@"updatedAt"]]); + XCTAssertTrue([user isEqual:retrievedUser ignoringAttributes:@[@"updatedAt"]]); } #pragma mark - Helpers @@ -127,7 +486,7 @@ -(void)validateUserModelIsEqualBehaviorUsingUserDictionary:(NSMutableDictionary* customDictionary[@"os"] = @"ios 10.3"; userDictionary[@"custom"] = [customDictionary copy]; LDUserModel *changedUser = [[LDUserModel alloc] initWithDictionary:userDictionary]; - XCTAssertFalse([user isEqual:changedUser ignoringProperties:@[@"updatedAt"]]); + XCTAssertFalse([user isEqual:changedUser ignoringAttributes:@[@"updatedAt"]]); } -(NSDictionary*)serverJson { @@ -161,5 +520,4 @@ -(NSMutableDictionary*)userDictionaryWithUserKey:(NSString*)userKey userName:(NS @"anonymous": @1 }]; } - @end diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index 27cbdf12..2a92cddc 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "LaunchDarkly" - s.version = "2.9.1" + s.version = "2.10.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.9.1" } + s.source = { :git => "https://github.com/launchdarkly/ios-client.git", :tag => "2.10.0" } s.source_files = "Darkly/*.{h,m}"