From 0296ab4a1d5f448f2778d039005c0cf461523d7b Mon Sep 17 00:00:00 2001 From: Arun Bhalla Date: Fri, 13 Oct 2017 14:19:11 -0700 Subject: [PATCH] Version 2.8.0 (#117) * add REPORT flag request --- CHANGELOG.md | 4 + Darkly.xcodeproj/project.pbxproj | 6 + Darkly/DarklyConstants.h | 3 + Darkly/DarklyConstants.m | 5 +- Darkly/LDClientManager.m | 26 +- Darkly/LDConfig.h | 8 + Darkly/LDConfig.m | 10 + Darkly/LDRequestManager.h | 9 +- Darkly/LDRequestManager.m | 173 +++++++--- DarklyTests/Categories/LDConfig+Testable.h | 13 + DarklyTests/Categories/LDConfig+Testable.m | 13 + DarklyTests/LDClientManagerTest.m | 52 ++- DarklyTests/LDConfigTest.m | 63 ++-- DarklyTests/LDRequestManagerTest.m | 358 ++++++++++++++++++--- LaunchDarkly.podspec | 4 +- 15 files changed, 586 insertions(+), 161 deletions(-) create mode 100644 DarklyTests/Categories/LDConfig+Testable.h create mode 100644 DarklyTests/Categories/LDConfig+Testable.m diff --git a/CHANGELOG.md b/CHANGELOG.md index 0aa463fc..a8be5afb 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.8.0] - 2017-10-13 +### Added +- `useReport` property on `LDConfig` to allow switching the request verb from `GET` to `REPORT`. Do not use unless advised by LaunchDarkly. + ## [2.7.0] - 2017-09-25 ### Changed - Updated for Xcode 9 support diff --git a/Darkly.xcodeproj/project.pbxproj b/Darkly.xcodeproj/project.pbxproj index d90d6f37..871fb821 100644 --- a/Darkly.xcodeproj/project.pbxproj +++ b/Darkly.xcodeproj/project.pbxproj @@ -161,6 +161,7 @@ 833D08D21F3B97EB00BEED83 /* NSThread+MainExecutable.m in Sources */ = {isa = PBXBuildFile; fileRef = 833D08CA1F3B97EB00BEED83 /* NSThread+MainExecutable.m */; }; 8349F51E1F19352300B1F3DB /* NSDictionary+StringKey_Matchable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8349F51D1F19352300B1F3DB /* NSDictionary+StringKey_Matchable.m */; }; 8349F5211F195BCF00B1F3DB /* LDUserModel+Equatable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8349F5201F195BCF00B1F3DB /* LDUserModel+Equatable.m */; }; + 8358F25A1F4202A300ECE1AF /* LDConfig+Testable.m in Sources */ = {isa = PBXBuildFile; fileRef = 8358F2591F4202A300ECE1AF /* LDConfig+Testable.m */; }; 8369475C1F1FED400047697C /* boolConfigIsABool-false.json in Resources */ = {isa = PBXBuildFile; fileRef = 836947591F1FED400047697C /* boolConfigIsABool-false.json */; }; 8369475D1F1FED400047697C /* boolConfigIsABool-true.json in Resources */ = {isa = PBXBuildFile; fileRef = 8369475A1F1FED400047697C /* boolConfigIsABool-true.json */; }; 8369475E1F1FED400047697C /* boolConfigIsABool2-true.json in Resources */ = {isa = PBXBuildFile; fileRef = 8369475B1F1FED400047697C /* boolConfigIsABool2-true.json */; }; @@ -273,6 +274,8 @@ 8349F51D1F19352300B1F3DB /* NSDictionary+StringKey_Matchable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+StringKey_Matchable.m"; sourceTree = ""; }; 8349F51F1F195BCF00B1F3DB /* LDUserModel+Equatable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDUserModel+Equatable.h"; sourceTree = ""; }; 8349F5201F195BCF00B1F3DB /* LDUserModel+Equatable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "LDUserModel+Equatable.m"; sourceTree = ""; }; + 8358F2581F4202A300ECE1AF /* LDConfig+Testable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LDConfig+Testable.h"; sourceTree = ""; }; + 8358F2591F4202A300ECE1AF /* LDConfig+Testable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "LDConfig+Testable.m"; sourceTree = ""; }; 836947591F1FED400047697C /* boolConfigIsABool-false.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "boolConfigIsABool-false.json"; sourceTree = ""; }; 8369475A1F1FED400047697C /* boolConfigIsABool-true.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "boolConfigIsABool-true.json"; sourceTree = ""; }; 8369475B1F1FED400047697C /* boolConfigIsABool2-true.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "boolConfigIsABool2-true.json"; sourceTree = ""; }; @@ -501,6 +504,8 @@ 83258A3C1F323049008C2133 /* LDClientManager+EventSource.m */, 83258A3E1F3244D0008C2133 /* LDUserBuilder+Testable.h */, 83258A3F1F3244D0008C2133 /* LDUserBuilder+Testable.m */, + 8358F2581F4202A300ECE1AF /* LDConfig+Testable.h */, + 8358F2591F4202A300ECE1AF /* LDConfig+Testable.m */, ); path = Categories; sourceTree = ""; @@ -1082,6 +1087,7 @@ 690347331E689B9F00E45133 /* NSArray+UnitTests.m in Sources */, 6903472C1E689B9F00E45133 /* LDClientTest.m in Sources */, 832C78901F2A8DF600E334A2 /* LDUserModel+JsonDecodeable.m in Sources */, + 8358F25A1F4202A300ECE1AF /* LDConfig+Testable.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Darkly/DarklyConstants.h b/Darkly/DarklyConstants.h index 37cba963..47a24c78 100644 --- a/Darkly/DarklyConstants.h +++ b/Darkly/DarklyConstants.h @@ -49,3 +49,6 @@ extern int const kMinimumPollingInterval; extern int const kDefaultBackgroundFetchInterval; extern int const kMinimumBackgroundFetchInterval; extern int const kMillisInSecs; +extern NSInteger const kHTTPStatusCodeBadRequest; +extern NSInteger const kHTTPStatusCodeMethodNotAllowed; +extern NSInteger const kHTTPStatusCodeNotImplemented; diff --git a/Darkly/DarklyConstants.m b/Darkly/DarklyConstants.m index f1035436..55bfd917 100644 --- a/Darkly/DarklyConstants.m +++ b/Darkly/DarklyConstants.m @@ -4,7 +4,7 @@ #import "DarklyConstants.h" -NSString * const kClientVersion = @"2.7.0"; +NSString * const kClientVersion = @"2.8.0"; NSString * const kBaseUrl = @"https://app.launchdarkly.com"; NSString * const kEventsUrl = @"https://mobile.launchdarkly.com"; NSString * const kStreamUrl = @"https://clientstream.launchdarkly.com/mping"; @@ -36,3 +36,6 @@ int const kDefaultBackgroundFetchInterval = 3600; int const kMinimumBackgroundFetchInterval = 900; int const kMillisInSecs = 1000; +NSInteger const kHTTPStatusCodeBadRequest = 400; +NSInteger const kHTTPStatusCodeMethodNotAllowed = 405; +NSInteger const kHTTPStatusCodeNotImplemented = 501; diff --git a/Darkly/LDClientManager.m b/Darkly/LDClientManager.m index c4891468..112dba49 100644 --- a/Darkly/LDClientManager.m +++ b/Darkly/LDClientManager.m @@ -158,25 +158,17 @@ -(void)syncWithServerForEvents { } -(void)syncWithServerForConfig { - if (!offlineEnabled) { - DEBUG_LOGX(@"ClientManager syncing config with server"); - LDClient *client = [LDClient sharedInstance]; - LDUserModel *currentUser = client.ldUser; - - if (currentUser) { - NSString *jsonString = [currentUser convertToJson]; - if (jsonString) { - NSString *encodedUser = [LDUtil base64UrlEncodeString:jsonString]; - [[LDRequestManager sharedInstance] performFeatureFlagRequest:encodedUser]; - } else { - DEBUG_LOGX(@"ClientManager is not able to convert user to json"); - } - } else { - DEBUG_LOGX(@"ClientManager has no user so won't sync config with server"); - } - } else { + if (offlineEnabled) { DEBUG_LOGX(@"ClientManager is in offline mode so won't sync config with server"); + return; } + + if (![LDClient sharedInstance].ldUser) { + DEBUG_LOGX(@"ClientManager has no user so won't sync config with server"); + return; + } + + [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; } - (void)flushEvents { diff --git a/Darkly/LDConfig.h b/Darkly/LDConfig.h index 26333e46..56268edf 100644 --- a/Darkly/LDConfig.h +++ b/Darkly/LDConfig.h @@ -69,6 +69,13 @@ */ @property (nonatomic) BOOL streaming; +/** + 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. + Do not use unless advised by LaunchDarkly. + */ +@property (nonatomic, assign) BOOL useReport; + /** Flag that enables debug mode to allow things such as logging. */ @@ -80,6 +87,7 @@ @return An instance of LDConfig object. */ - (instancetype _Nonnull)initWithMobileKey:(nonnull NSString *)mobileKey NS_DESIGNATED_INITIALIZER; +- (BOOL)isFlagRetryStatusCode:(NSInteger)statusCode; - (instancetype _Nonnull )init NS_UNAVAILABLE; diff --git a/Darkly/LDConfig.m b/Darkly/LDConfig.m index 4b575c4c..d8749429 100644 --- a/Darkly/LDConfig.m +++ b/Darkly/LDConfig.m @@ -7,6 +7,7 @@ @interface LDConfig() @property (nonatomic, copy, nonnull) NSString* mobileKey; +@property (nonatomic, strong, nonnull) NSArray *flagRetryStatusCodes; @end @implementation LDConfig @@ -26,6 +27,11 @@ - (instancetype)initWithMobileKey:(NSString *)mobileKey { self.baseUrl = kBaseUrl; self.eventsUrl = kEventsUrl; self.streamUrl = kStreamUrl; +// self.flagRetryStatusCodes = @[@(kHTTPStatusCodeMethodNotAllowed), +// @(kHTTPStatusCodeBadRequest), +// @(kHTTPStatusCodeNotImplemented)]; + self.flagRetryStatusCodes = @[]; //Temporarily, leave these codes empty to disable the REPORT fallback using GET capability + self.useReport = NO; return self; } @@ -115,6 +121,10 @@ - (void)setDebugEnabled:(BOOL)debugEnabled { DEBUG_LOG(@"Set LDConfig debug enabled: %d", debugEnabled); } +- (BOOL)isFlagRetryStatusCode:(NSInteger)statusCode { + return [self.flagRetryStatusCodes containsObject:@(statusCode)]; +} + @end diff --git a/Darkly/LDRequestManager.h b/Darkly/LDRequestManager.h index 66b3f348..8196be2e 100644 --- a/Darkly/LDRequestManager.h +++ b/Darkly/LDRequestManager.h @@ -2,8 +2,7 @@ // Copyright © 2015 Catamorphic Co. All rights reserved. // - -#import "LDFlagConfigModel.h" +#import "LDUserModel.h" @protocol RequestManagerDelegate @@ -12,9 +11,7 @@ @end -@interface LDRequestManager : NSObject { - -} +@interface LDRequestManager : NSObject extern NSString * const kHeaderMobileKey; @property (nonatomic) NSString* mobileKey; @@ -25,7 +22,7 @@ extern NSString * const kHeaderMobileKey; +(LDRequestManager *)sharedInstance; --(void)performFeatureFlagRequest:(NSString *)encodedUser; +-(void)performFeatureFlagRequest:(LDUserModel *)user; -(void)performEventRequest:(NSArray *)jsonEventArray; diff --git a/Darkly/LDRequestManager.m b/Darkly/LDRequestManager.m index 4eb738c3..86ed41a7 100644 --- a/Darkly/LDRequestManager.m +++ b/Darkly/LDRequestManager.m @@ -7,7 +7,8 @@ #import "LDClientManager.h" #import "LDConfig.h" -static NSString * const kFeatureFlagUrl = @"/msdk/eval/users/"; +static NSString * const kFeatureFlagGetUrl = @"/msdk/eval/users/"; +static NSString * const kFeatureFlagReportUrl = @"/msdk/eval/user"; static NSString * const kEventUrl = @"/mobile/events/bulk"; NSString * const kHeaderMobileKey = @"api_key "; static NSString * const kConfigRequestCompletedNotification = @"config_request_completed_notification"; @@ -17,8 +18,7 @@ @implementation LDRequestManager @synthesize mobileKey, baseUrl, eventsUrl, connectionTimeout, delegate; -+(LDRequestManager *)sharedInstance -{ ++(LDRequestManager *)sharedInstance { static LDRequestManager *sharedApiManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -34,63 +34,91 @@ +(LDRequestManager *)sharedInstance return sharedApiManager; } -- (void)dealloc -{ +- (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } --(void)performFeatureFlagRequest:(NSString *)encodedUser +-(void)performFeatureFlagRequest:(LDUserModel *)user { - DEBUG_LOGX(@"RequestManager syncing config to server"); + if (!mobileKey) { + DEBUG_LOGX(@"RequestManager unable to sync config to server since no mobileKey"); + return; + } - dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + if (!user) { + DEBUG_LOGX(@"RequestManager unable to sync config to server since no user"); + return; + } - if (mobileKey) { - if (encodedUser) { - NSURLSession *defaultSession = [NSURLSession sharedSession]; - NSString *requestUrl = [baseUrl stringByAppendingString:kFeatureFlagUrl]; - - requestUrl = [requestUrl stringByAppendingString:encodedUser]; + if ([LDClient sharedInstance].ldConfig.useReport) { + DEBUG_LOGX(@"RequestManager syncing config to server via REPORT"); + NSURLRequest *flagRequestUsingReportMethod = [self flagRequestUsingReportMethodForUser:user]; + [self performFlagRequest:flagRequestUsingReportMethod completionHandler:^(NSData * _Nullable originalData, NSURLResponse * _Nullable originalResponse, NSError * _Nullable originalError) { - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:requestUrl]]; - [request setTimeoutInterval:self.connectionTimeout]; - - [self addFeatureRequestHeaders:request]; - - NSURLSessionDataTask *dataTask = [defaultSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { - // This comes in on a background thread, there's no reason to do any additional processing of - // the data on the main thread -- just the notification of the delegate on the main thread. - BOOL configProcessed = NO; - NSDictionary *responseDict = nil; - if (!error) { - NSError *jsonError; - NSDictionary * responseObject = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError]; - if (responseObject) { - configProcessed = YES; - responseDict = responseObject; - } + if ([self shouldTryFlagGetRequestForFlagResponse:originalResponse]) { + NSURLRequest *flagRequestUsingGetMethod = [self flagRequestUsingGetMethodForUser:user]; + if (flagRequestUsingGetMethod) { + DEBUG_LOGX(@"RequestManager syncing config to server via GET"); + + [self performFlagRequest:flagRequestUsingGetMethod completionHandler:^(NSData * _Nullable retriedData, NSURLResponse * _Nullable retriedResponse, NSError * _Nullable retriedError) { + [self processFlagResponseWithData:retriedData error:retriedError]; + }]; + return; } - dispatch_semaphore_signal(semaphore); - dispatch_async(dispatch_get_main_queue(), ^{ - [delegate processedConfig:configProcessed jsonConfigDictionary:responseDict]; - }); - - }]; - - [dataTask resume]; - - dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); - - } else { - DEBUG_LOGX(@"RequestManager unable to sync config to server since no encodedUser"); - } + } + [self processFlagResponseWithData:originalData error:originalError]; + }]; } else { - DEBUG_LOGX(@"RequestManager unable to sync config to server since no mobileKey"); + DEBUG_LOGX(@"RequestManager syncing config to server via GET"); + + NSURLRequest *flagRequestUsingGetMethod = [self flagRequestUsingGetMethodForUser:user]; + [self performFlagRequest:flagRequestUsingGetMethod completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + [self processFlagResponseWithData:data error:error]; + }]; } } --(void)performEventRequest:(NSArray *)jsonEventArray -{ +-(BOOL)shouldTryFlagGetRequestForFlagResponse:(NSURLResponse*)flagResponse { + if (!flagResponse) { return NO; } + if (![flagResponse isKindOfClass:[NSHTTPURLResponse class]]) { return NO; } + NSHTTPURLResponse *httpFlagResponse = (NSHTTPURLResponse*)flagResponse; + return [LDClient sharedInstance].ldConfig.useReport && [[LDClient sharedInstance].ldConfig isFlagRetryStatusCode:httpFlagResponse.statusCode]; +} + +-(void)processFlagResponseWithData:(NSData*)data error:(NSError*)error { + BOOL configProcessed = NO; + NSDictionary *featureFlags; + if (!error) { + NSError *jsonError; + featureFlags = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError]; + configProcessed = featureFlags != nil; + } + dispatch_async(dispatch_get_main_queue(), ^{ + [self.delegate processedConfig:configProcessed jsonConfigDictionary:featureFlags]; + }); +} + +-(void)performFlagRequest:(NSURLRequest*)request completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler { + if (!request) { return; } + + NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; + NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration:configuration delegate:nil delegateQueue:nil]; + + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + NSURLSessionDataTask *dataTask = [defaultSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + if (completionHandler) { + completionHandler(data, response, error); + } + dispatch_semaphore_signal(semaphore); + }]; + + [dataTask resume]; + + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); +} + +-(void)performEventRequest:(NSArray *)jsonEventArray { DEBUG_LOGX(@"RequestManager syncing events to server"); dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); @@ -131,14 +159,59 @@ -(void)performEventRequest:(NSArray *)jsonEventArray } #pragma mark - requests --(void)addFeatureRequestHeaders:(NSMutableURLRequest *)request{ +-(NSURLRequest*)flagRequestUsingReportMethodForUser:(LDUserModel*)user { + if (!user) { + DEBUG_LOGX(@"RequestManager unable to sync config to server since no user"); + return nil; + } + NSString *userJson = [user convertToJson]; + if (!userJson) { + DEBUG_LOGX(@"RequestManager could not convert user to json, aborting sync config to server"); + return nil; + } + + NSString *requestUrl = [baseUrl stringByAppendingString:kFeatureFlagReportUrl]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:requestUrl]]; + request.HTTPMethod = @"REPORT"; + request.HTTPBody = [userJson dataUsingEncoding:NSUTF8StringEncoding]; + [request setTimeoutInterval:self.connectionTimeout]; + [self addFeatureRequestHeaders:request]; + + return request; +} + +-(NSURLRequest*)flagRequestUsingGetMethodForUser:(LDUserModel*)user { + if (!user) { + DEBUG_LOGX(@"RequestManager unable to sync config to server since no user"); + return nil; + } + NSString *userJson = [user convertToJson]; + if (!userJson) { + DEBUG_LOGX(@"RequestManager could not convert user to json, aborting sync config to server"); + return nil; + } + NSString *encodedUser = [LDUtil base64UrlEncodeString:userJson]; + if (!encodedUser) { + DEBUG_LOGX(@"RequestManager could not base64Url encode user, aborting sync config to server"); + return nil; + } + NSString *requestUrl = [baseUrl stringByAppendingString:kFeatureFlagGetUrl]; + requestUrl = [requestUrl stringByAppendingString:encodedUser]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:requestUrl]]; + [request setTimeoutInterval:self.connectionTimeout]; + [self addFeatureRequestHeaders:request]; + + return request; +} + +-(void)addFeatureRequestHeaders:(NSMutableURLRequest *)request { NSString *authKey = [kHeaderMobileKey stringByAppendingString:mobileKey]; [request addValue:authKey forHTTPHeaderField:@"Authorization"]; [request addValue:[@"iOS/" stringByAppendingString:kClientVersion] forHTTPHeaderField:@"User-Agent"]; } --(void)addEventRequestHeaders: (NSMutableURLRequest *)request { +-(void)addEventRequestHeaders: (NSMutableURLRequest *)request { NSString *authKey = [kHeaderMobileKey stringByAppendingString:mobileKey]; [request addValue:authKey forHTTPHeaderField:@"Authorization"]; diff --git a/DarklyTests/Categories/LDConfig+Testable.h b/DarklyTests/Categories/LDConfig+Testable.h new file mode 100644 index 00000000..7f5f115d --- /dev/null +++ b/DarklyTests/Categories/LDConfig+Testable.h @@ -0,0 +1,13 @@ +// +// LDConfig+Testable.h +// Darkly +// +// Created by Mark Pokorny on 8/14/17. +JMJ +// Copyright © 2017 LaunchDarkly. All rights reserved. +// + +#import + +@interface LDConfig (Testable) +@property (nonatomic, strong, nonnull, readonly) NSArray *flagRetryStatusCodes; +@end diff --git a/DarklyTests/Categories/LDConfig+Testable.m b/DarklyTests/Categories/LDConfig+Testable.m new file mode 100644 index 00000000..d8a30a61 --- /dev/null +++ b/DarklyTests/Categories/LDConfig+Testable.m @@ -0,0 +1,13 @@ +// +// LDConfig+Testable.m +// Darkly +// +// Created by Mark Pokorny on 8/14/17. +JMJ +// Copyright © 2017 LaunchDarkly. All rights reserved. +// + +#import "LDConfig+Testable.h" + +@implementation LDConfig (Testable) +@dynamic flagRetryStatusCodes; +@end diff --git a/DarklyTests/LDClientManagerTest.m b/DarklyTests/LDClientManagerTest.m index d3597d37..f4606ed5 100644 --- a/DarklyTests/LDClientManagerTest.m +++ b/DarklyTests/LDClientManagerTest.m @@ -29,16 +29,12 @@ @implementation LDClientManagerTest - (void)setUp { [super setUp]; + LDUserBuilder *userBuilder = [[LDUserBuilder alloc] init]; - userBuilder.key = @"jeff@test.com"; - LDUserModel *user = [userBuilder build]; - LDConfig *config = [[LDConfig alloc] initWithMobileKey:@""]; - config.flushInterval = [NSNumber numberWithInt:30]; + userBuilder.key = [[NSUUID UUID] UUIDString]; + userBuilder.email = @"jeff@test.com"; - ldClientMock = OCMClassMock([LDClient class]); - OCMStub(ClassMethod([ldClientMock sharedInstance])).andReturn(ldClientMock); - OCMStub([ldClientMock ldUser]).andReturn(user); - OCMStub([ldClientMock ldConfig]).andReturn(config); + ldClientMock = [self mockClientWithUser:[userBuilder build]]; requestManagerMock = OCMClassMock([LDRequestManager class]); OCMStub(ClassMethod([requestManagerMock sharedInstance])).andReturn(requestManagerMock); @@ -102,25 +98,36 @@ - (void)testEventSourceRemainsConstantAcrossWillEnterForegroundCalls { } } -- (void)testSyncWithServerForConfigWhenUserExists { +- (void)testSyncWithServerForConfigWhenUserExistsAndOnline { + [[requestManagerMock expect] performFeatureFlagRequest:[ldClientMock ldUser]]; + LDClientManager *clientManager = [LDClientManager sharedInstance]; [clientManager setOfflineEnabled:NO]; [clientManager syncWithServerForConfig]; - OCMVerify([requestManagerMock performFeatureFlagRequest:[OCMArg isKindOfClass:[NSString class]]]); + [requestManagerMock verify]; } -- (void)testDoNotSyncWithServerForConfigWhenUserDoesNotExists { - NSData *testData = nil; +- (void)testSyncWithServerForConfigWhenUserDoesNotExist { + [self mockClientWithUser:nil]; + XCTAssertNil([LDClient sharedInstance].ldUser); + + [[requestManagerMock reject] performFeatureFlagRequest:[OCMArg any]]; + LDClientManager *clientManager = [LDClientManager sharedInstance]; [clientManager setOfflineEnabled:NO]; - [[requestManagerMock reject] performEventRequest:[OCMArg isEqual:testData]]; - [clientManager syncWithServerForConfig]; +} + +- (void)testSyncWithServerForConfigWhenOffline { + [[requestManagerMock reject] performFeatureFlagRequest:[OCMArg any]]; + + LDClientManager *clientManager = [LDClientManager sharedInstance]; + [clientManager setOfflineEnabled:YES]; - [requestManagerMock verify]; + [clientManager syncWithServerForConfig]; } - (void)testSyncWithServerForEventsWhenEventsExist { @@ -275,6 +282,8 @@ - (void)testProcessedConfigSuccessWithUserDifferentUserConfig { XCTAssertTrue([clientUser.config isEqualToConfig:endingConfig]); } +#pragma mark - Helpers + - (NSDictionary*)dictionaryFromJsonFileNamed:(NSString *)fileName { NSString *filepath = [[NSBundle bundleForClass:[self class]] pathForResource:fileName ofType:@"json"]; @@ -285,5 +294,18 @@ - (NSDictionary*)dictionaryFromJsonFileNamed:(NSString *)fileName { error:&error]; } +- (id)mockClientWithUser:(LDUserModel*)user { + id mockClient = OCMClassMock([LDClient class]); + OCMStub(ClassMethod([mockClient sharedInstance])).andReturn(mockClient); + OCMStub([mockClient ldUser]).andReturn(user); + XCTAssertEqual([LDClient sharedInstance].ldUser, user); + + LDConfig *config = [[LDConfig alloc] initWithMobileKey:@"testMobileKey"]; + config.flushInterval = [NSNumber numberWithInt:30]; + OCMStub([mockClient ldConfig]).andReturn(config); + XCTAssertEqual([LDClient sharedInstance].ldConfig, config); + + return mockClient; +} @end diff --git a/DarklyTests/LDConfigTest.m b/DarklyTests/LDConfigTest.m index f25f1bf3..aca77d5c 100644 --- a/DarklyTests/LDConfigTest.m +++ b/DarklyTests/LDConfigTest.m @@ -3,6 +3,7 @@ // #import "LDConfig.h" +#import "LDConfig+Testable.h" #import "DarklyXCTestCase.h" #import "DarklyConstants.h" @@ -10,6 +11,8 @@ @interface LDConfigTest : DarklyXCTestCase @end +NSString * const LDConfigTestMobileKey = @"testMobileKey"; + @implementation LDConfigTest - (void)setUp { @@ -23,9 +26,8 @@ - (void)tearDown { } - (void)testConfigDefaultValues { - NSString *testMobileKey = @"testMobileKey"; - LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; - XCTAssertEqualObjects([config mobileKey], testMobileKey); + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; + XCTAssertEqualObjects([config mobileKey], LDConfigTestMobileKey); XCTAssertEqualObjects([config capacity], [NSNumber numberWithInt:kCapacity]); XCTAssertEqualObjects([config connectionTimeout], [NSNumber numberWithInt:kConnectionTimeout]); XCTAssertEqualObjects([config flushInterval], [NSNumber numberWithInt:kDefaultFlushInterval]); @@ -33,14 +35,13 @@ - (void)testConfigDefaultValues { } - (void)testConfigBuilderDefaultValues { - NSString *testMobileKey = @"testMobileKey"; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" LDConfigBuilder *builder = [[LDConfigBuilder alloc] init]; - [builder withMobileKey:testMobileKey]; + [builder withMobileKey:LDConfigTestMobileKey]; #pragma clang diagnostic pop LDConfig *config = [builder build]; - XCTAssertEqualObjects([config mobileKey], testMobileKey); + XCTAssertEqualObjects([config mobileKey], LDConfigTestMobileKey); XCTAssertEqualObjects([config capacity], [NSNumber numberWithInt:kCapacity]); XCTAssertEqualObjects([config connectionTimeout], [NSNumber numberWithInt:kConnectionTimeout]); XCTAssertEqualObjects([config flushInterval], [NSNumber numberWithInt:kDefaultFlushInterval]); @@ -48,11 +49,10 @@ - (void)testConfigBuilderDefaultValues { } - (void)testConfigOverrideBaseUrl { - NSString *testMobileKey = @"testMobileKey"; NSString *testBaseUrl = @"testBaseUrl"; - LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; config.baseUrl = testBaseUrl; - XCTAssertEqualObjects([config mobileKey], testMobileKey); + XCTAssertEqualObjects([config mobileKey], LDConfigTestMobileKey); XCTAssertEqualObjects([config capacity], [NSNumber numberWithInt:kCapacity]); XCTAssertEqualObjects([config connectionTimeout], [NSNumber numberWithInt:kConnectionTimeout]); XCTAssertEqualObjects([config flushInterval], [NSNumber numberWithInt:kDefaultFlushInterval]); @@ -68,11 +68,10 @@ - (void)testConfigOverrideStreamUrl { } - (void)testConfigOverrideCapacity { - NSString *testMobileKey = @"testMobileKey"; int testCapacity = 20; - LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; config.capacity = [NSNumber numberWithInt:testCapacity]; - XCTAssertEqualObjects([config mobileKey], testMobileKey); + XCTAssertEqualObjects([config mobileKey], LDConfigTestMobileKey); XCTAssertEqualObjects([config capacity], [NSNumber numberWithInt:testCapacity]); XCTAssertEqualObjects([config connectionTimeout], [NSNumber numberWithInt:kConnectionTimeout]); XCTAssertEqualObjects([config flushInterval], [NSNumber numberWithInt:kDefaultFlushInterval]); @@ -80,11 +79,10 @@ - (void)testConfigOverrideCapacity { } - (void)testConfigOverrideConnectionTimeout { - NSString *testMobileKey = @"testMobileKey"; int testConnectionTimeout = 15; - LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; config.connectionTimeout = [NSNumber numberWithInt:testConnectionTimeout]; - XCTAssertEqualObjects([config mobileKey], testMobileKey); + XCTAssertEqualObjects([config mobileKey], LDConfigTestMobileKey); XCTAssertEqualObjects([config capacity], [NSNumber numberWithInt:kCapacity]); XCTAssertEqualObjects([config connectionTimeout], [NSNumber numberWithInt:testConnectionTimeout]); XCTAssertEqualObjects([config flushInterval], [NSNumber numberWithInt:kDefaultFlushInterval]); @@ -92,11 +90,10 @@ - (void)testConfigOverrideConnectionTimeout { } - (void)testConfigOverrideFlushInterval { - NSString *testMobileKey = @"testMobileKey"; int testFlushInterval = 5; - LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; config.flushInterval = [NSNumber numberWithInt:testFlushInterval]; - XCTAssertEqualObjects([config mobileKey], testMobileKey); + XCTAssertEqualObjects([config mobileKey], LDConfigTestMobileKey); XCTAssertEqualObjects([config capacity], [NSNumber numberWithInt:kCapacity]); XCTAssertEqualObjects([config connectionTimeout], [NSNumber numberWithInt:kConnectionTimeout]); XCTAssertEqualObjects([config flushInterval], [NSNumber numberWithInt:testFlushInterval]); @@ -104,9 +101,8 @@ - (void)testConfigOverrideFlushInterval { } - (void)testConfigOverridePollingInterval { - NSString *testMobileKey = @"testMobileKey"; - LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; - XCTAssertEqualObjects([config mobileKey], testMobileKey); + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; + XCTAssertEqualObjects([config mobileKey], LDConfigTestMobileKey); XCTAssertEqualObjects([config pollingInterval], [NSNumber numberWithInt:kDefaultPollingInterval]); XCTAssertFalse([config debugEnabled]); @@ -120,9 +116,8 @@ - (void)testConfigOverridePollingInterval { } - (void)testConfigOverrideStreaming { - NSString *testMobileKey = @"testMobileKey"; - LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; - XCTAssertEqualObjects(config.mobileKey, testMobileKey); + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; + XCTAssertEqualObjects(config.mobileKey, LDConfigTestMobileKey); XCTAssertTrue(config.streaming); config.streaming = NO; @@ -130,14 +125,28 @@ - (void)testConfigOverrideStreaming { } - (void)testConfigOverrideDebug { - NSString *testMobileKey = @"testMobileKey"; - LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; config.debugEnabled = YES; - XCTAssertEqualObjects([config mobileKey], testMobileKey); + XCTAssertEqualObjects([config mobileKey], LDConfigTestMobileKey); XCTAssertEqualObjects([config capacity], [NSNumber numberWithInt:kCapacity]); XCTAssertEqualObjects([config connectionTimeout], [NSNumber numberWithInt:kConnectionTimeout]); XCTAssertEqualObjects([config flushInterval], [NSNumber numberWithInt:kDefaultFlushInterval]); XCTAssertTrue([config debugEnabled]); } +- (void)testIsFlagRetryStatusCode { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:LDConfigTestMobileKey]; + + NSMutableSet *selectedStatusCodes = [NSMutableSet setWithArray:@[@405, @400, @501, @200, @304, @307, @401, @404, @412, @500]]; + [selectedStatusCodes unionSet:[NSSet setWithArray:config.flagRetryStatusCodes]]; //allow flagRetryStatusCodes to change without changing the test + NSMutableDictionary *statusCodeResults = [NSMutableDictionary dictionaryWithCapacity:selectedStatusCodes.count]; + [selectedStatusCodes enumerateObjectsUsingBlock:^(NSNumber * _Nonnull statusCode, BOOL * _Nonnull stop) { + statusCodeResults[statusCode] = @([config.flagRetryStatusCodes containsObject:statusCode]); + }]; + + for (NSNumber *statusCode in [statusCodeResults allKeys]) { + XCTAssertTrue([config isFlagRetryStatusCode:[statusCode integerValue]] == [statusCodeResults[statusCode] boolValue]); + } +} + @end diff --git a/DarklyTests/LDRequestManagerTest.m b/DarklyTests/LDRequestManagerTest.m index 6d913e31..9a7c1e0e 100644 --- a/DarklyTests/LDRequestManagerTest.m +++ b/DarklyTests/LDRequestManagerTest.m @@ -2,41 +2,39 @@ // Copyright © 2015 Catamorphic Co. All rights reserved. // +#import +#import + #import "DarklyXCTestCase.h" #import "LDRequestManager.h" #import "LDDataManager.h" #import "LDClientManager.h" #import "LDUserBuilder.h" #import "LDConfig.h" +#import "LDConfig+Testable.h" #import "LDClient.h" -#import -#import + +static NSString *const httpMethodReport = @"REPORT"; +static NSString *const httpMethodGet = @"GET"; +static NSString *const testMobileKey = @"testMobileKey"; +static NSString *const emptyJson = @"{ }"; +static NSString *const flagRequestHost = @"app.launchdarkly.com"; +static NSString *const eventRequestHost = @"mobile.launchdarkly.com"; +static const int httpStatusCodeOk = 200; @interface LDRequestManagerTest : DarklyXCTestCase @property (nonatomic) id clientManagerMock; @property (nonatomic) id ldClientMock; -@property (nonatomic) int tempFlushInterval; @end @implementation LDRequestManagerTest @synthesize clientManagerMock; @synthesize ldClientMock; -@synthesize tempFlushInterval; - (void)setUp { [super setUp]; - // Put setup code here. This method is called before the invocation of each test method in the class. - LDUserBuilder *userBuilder = [[LDUserBuilder alloc] init]; - userBuilder.key = @"jeff@test.com"; - LDUserModel *user = [userBuilder build]; - LDConfig *config = [[LDConfig alloc] initWithMobileKey:@"testMobileKey"]; - tempFlushInterval = 30; - config.flushInterval = [NSNumber numberWithInt:tempFlushInterval]; - - ldClientMock = OCMClassMock([LDClient class]); - OCMStub(ClassMethod([ldClientMock sharedInstance])).andReturn(ldClientMock); - OCMStub([ldClientMock ldUser]).andReturn(user); - OCMStub([ldClientMock ldConfig]).andReturn(config); + + ldClientMock = [self mockClientWithUser:[self mockUser] config:[self testConfig]]; clientManagerMock = OCMClassMock([LDClientManager class]); OCMStub(ClassMethod([clientManagerMock sharedInstance])).andReturn(clientManagerMock); @@ -45,37 +43,254 @@ - (void)setUp { - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. [super tearDown]; + [LDRequestManager sharedInstance].delegate = nil; + [OHHTTPStubs removeAllStubs]; } -- (void)testFeatureFlagRequestMakesHttpRequestWithMobileKey { +- (void)testPerformFeatureFlagRequestMakesGetRequestAndCallsDelegateOnSuccess { + XCTestExpectation *getRequestMade = [self expectationWithDescription:@"feature flag GET request made"]; - XCTestExpectation* responseArrived = [self expectationWithDescription:@"response of async request has arrived"]; - __block BOOL httpRequestAttempted = NO; - NSData *data = [[NSData alloc] initWithBase64EncodedString:@"" options: 0] ; + __block id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; + [[requestManagerDelegateMock expect] processedConfig:YES jsonConfigDictionary:[OCMArg isKindOfClass:[NSDictionary class]]]; + [LDRequestManager sharedInstance].delegate = requestManagerDelegateMock; + + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:httpMethodGet]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + [getRequestMade fulfill]; + return [OHHTTPStubsResponse responseWithData: [self successJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; + }]; + + [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; + + [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) { + [requestManagerDelegateMock verifyWithDelay:1]; + }]; +} + +- (void)testPerformFeatureFlagRequestDoesNotMakeFallbackRequestWhenUsingGet { + NSMutableArray *selectedStatusCodes = [NSMutableArray arrayWithArray:[LDClient sharedInstance].ldConfig.flagRetryStatusCodes]; + [selectedStatusCodes addObjectsFromArray:[self selectedNoFallbackStatusCodes]]; + + for (NSNumber *statusCode in selectedStatusCodes) { + NSString *getStubName = @"get stub"; + + __block XCTestExpectation *getRequestMade = [self expectationWithDescription:@"feature flag GET request made"]; + __block XCTestExpectation *errorResponseArrived = [self expectationWithDescription:@"feature flag error response arrived"]; + + __block id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; + [[requestManagerDelegateMock expect] processedConfig:NO jsonConfigDictionary:[OCMArg isNil]]; + [LDRequestManager sharedInstance].delegate = requestManagerDelegateMock; + + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:httpMethodGet]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + [getRequestMade fulfill]; + return [OHHTTPStubsResponse responseWithData: [NSData data] statusCode:[statusCode intValue] headers:[self headerForStatusCode:[statusCode intValue]]]; + }].name = getStubName; + + [OHHTTPStubs onStubActivation:^(NSURLRequest * _Nonnull request, id _Nonnull stub) { + if ([stub.name isEqualToString:getStubName]) { + [errorResponseArrived fulfill]; + } + }]; + + [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; + + [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) { + [requestManagerDelegateMock verifyWithDelay:1]; //By checking the delegate, we are sure a fallback GET isn't called + + [LDRequestManager sharedInstance].delegate = nil; + requestManagerDelegateMock = nil; + + getRequestMade = nil; + errorResponseArrived = nil; + + [OHHTTPStubs removeAllStubs]; + }]; + } +} + +- (void)testPerformFeatureFlagRequestMakesReportRequestAndCallsDelegateOnSuccess { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; + config.useReport = YES; + + [self mockClientWithUser:[self mockUser] config:config]; + + __block id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; + [[requestManagerDelegateMock expect] processedConfig:YES jsonConfigDictionary:[OCMArg isKindOfClass:[NSDictionary class]]]; + [LDRequestManager sharedInstance].delegate = requestManagerDelegateMock; + + XCTestExpectation *reportRequestMade = [self expectationWithDescription:@"feature flag REPORT request made"]; + XCTestExpectation *responseArrived = [self expectationWithDescription:@"feature flag response arrived"]; [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { - return [request.URL.host isEqualToString:@"app.launchdarkly.com"]; + return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:httpMethodReport]; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { - httpRequestAttempted = YES; + [reportRequestMade fulfill]; + return [OHHTTPStubsResponse responseWithData: [self successJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; + }]; + + [OHHTTPStubs onStubActivation:^(NSURLRequest * _Nonnull request, id _Nonnull stub) { + XCTAssertTrue([request.HTTPMethod isEqualToString:httpMethodReport]); [responseArrived fulfill]; - return [OHHTTPStubsResponse responseWithData: data statusCode:200 headers:@{@"Content-Type":@"application/json"}]; }]; - NSString *mobileKey = @"YOUR_MOBILE_KEY"; - NSString *encodedUserString = @"eyJrZXkiOiAiamVmZkB0ZXN0LmNvbSJ9"; - LDRequestManager *requestManager = [[LDRequestManager alloc] init]; - [requestManager setMobileKey:mobileKey]; - [requestManager setBaseUrl:kBaseUrl]; - [requestManager setConnectionTimeout:10]; + [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; + + [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) { + [requestManagerDelegateMock verifyWithDelay:1]; + }]; +} - [requestManager performFeatureFlagRequest:encodedUserString]; +- (void)testPerformFeatureFlagRequestMakesFallbackGetRequest { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; + config.useReport = YES; - [self waitForExpectationsWithTimeout:10 handler:^(NSError *error){ - // By the time we reach this code, the while loop has exited - // so the response has arrived or the test has timed out - XCTAssertTrue(httpRequestAttempted); - [OHHTTPStubs removeAllStubs]; + [self mockClientWithUser:[self mockUser] config:config]; + + NSArray *fallbackStatusCodes = [config flagRetryStatusCodes]; + XCTAssertNotNil(fallbackStatusCodes); + + for (NSNumber *fallbackStatusCode in fallbackStatusCodes) { + NSString *reportStubName = @"report stub"; + NSString *getStubName = @"get stub"; + + __block XCTestExpectation *reportRequestMade = [self expectationWithDescription:@"feature flag REPORT request made"]; + __block XCTestExpectation *getRequestMade = [self expectationWithDescription:@"feature flag GET request made"]; + __block XCTestExpectation *errorResponseArrived = [self expectationWithDescription:@"feature flag error response arrived"]; + __block XCTestExpectation *flagResponseArrived = [self expectationWithDescription:@"feature flag response arrived"]; + + __block id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; + [[requestManagerDelegateMock expect] processedConfig:NO jsonConfigDictionary:[OCMArg isNil]]; + [LDRequestManager sharedInstance].delegate = requestManagerDelegateMock; + + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:httpMethodReport]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + [reportRequestMade fulfill]; + return [OHHTTPStubsResponse responseWithData: [NSData data] statusCode:[fallbackStatusCode intValue] headers:[self headerForStatusCode:[fallbackStatusCode intValue]]]; + }].name = reportStubName; + + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:httpMethodGet]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + [getRequestMade fulfill]; + return [OHHTTPStubsResponse responseWithData: [self emptyJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; + }].name = getStubName; + + [OHHTTPStubs onStubActivation:^(NSURLRequest * _Nonnull request, id _Nonnull stub) { + if ([stub.name isEqualToString:reportStubName]) { + [errorResponseArrived fulfill]; + } + if ([stub.name isEqualToString:getStubName]) { + [flagResponseArrived fulfill]; + } + }]; + + [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; + + [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) { + [requestManagerDelegateMock verifyWithDelay:1]; + + [LDRequestManager sharedInstance].delegate = nil; + requestManagerDelegateMock = nil; + + reportRequestMade = nil; + getRequestMade = nil; + errorResponseArrived = nil; + flagResponseArrived = nil; + + [OHHTTPStubs removeAllStubs]; + }]; + } +} + +- (void)testPerformFeatureFlagRequestDoesNotMakeFallbackGetRequest { + NSArray *noFallbackStatusCodes = [self selectedNoFallbackStatusCodes]; + + for (NSNumber *fallbackStatusCode in noFallbackStatusCodes) { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; + config.useReport = YES; + + [self mockClientWithUser:[self mockUser] config:config]; + + NSString *reportStubName = @"report stub"; + NSString *getStubName = @"get stub"; + + __block XCTestExpectation *reportRequestMade = [self expectationWithDescription:@"feature flag REPORT request made"]; + __block XCTestExpectation *errorResponseArrived = [self expectationWithDescription:@"feature flag error response arrived"]; + + __block id requestManagerDelegateMock = [OCMockObject niceMockForProtocol:@protocol(RequestManagerDelegate)]; + [[requestManagerDelegateMock expect] processedConfig:NO jsonConfigDictionary:[OCMArg isNil]]; + [LDRequestManager sharedInstance].delegate = requestManagerDelegateMock; + + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:httpMethodReport]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + [reportRequestMade fulfill]; + return [OHHTTPStubsResponse responseWithData: [NSData data] statusCode:[fallbackStatusCode intValue] headers:[self headerForStatusCode:[fallbackStatusCode intValue]]]; + }].name = reportStubName; + + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.host isEqualToString:flagRequestHost] && [request.HTTPMethod isEqualToString:httpMethodGet]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + XCTFail(@"Request Manager made GET flag request in response to a non-fallback status code"); + return [OHHTTPStubsResponse responseWithData: [self emptyJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; + }].name = getStubName; + + [OHHTTPStubs onStubActivation:^(NSURLRequest * _Nonnull request, id _Nonnull stub) { + if ([stub.name isEqualToString:reportStubName]) { + [errorResponseArrived fulfill]; + } + }]; + + [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; + + [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) { + [requestManagerDelegateMock verifyWithDelay:1]; + + [LDRequestManager sharedInstance].delegate = nil; + requestManagerDelegateMock = nil; + + reportRequestMade = nil; + errorResponseArrived = nil; + + [OHHTTPStubs removeAllStubs]; + }]; + } +} + +- (void)testPerformFeatureFlagRequestWithoutMobileKey { + //This test is a little unfair because the LDConfig can't be created without a mobile key, and it's not settable. + //However the LDRequestManager mobileKey IS settable, and so a client could remove a key after the LDRequestManager is instantiated. + //The client would probably be trying to break the sdk in that case, so testing it seems appropriate + [LDRequestManager sharedInstance].mobileKey = nil; + XCTAssertNil([LDRequestManager sharedInstance].mobileKey); + + [self mockFlagResponse]; + + [OHHTTPStubs onStubActivation:^(NSURLRequest * _Nonnull request, id _Nonnull stub) { + if ([request.URL.host isEqualToString:kBaseUrl]) { + XCTFail(@"performFeatureFlagRequest should not make a flag request without a mobile key"); + } }]; + + [[LDRequestManager sharedInstance] performFeatureFlagRequest:[LDClient sharedInstance].ldUser]; +} + +- (void)testPerformFeatureFlagRequestWithoutUser { + [self mockClientWithUser:nil config:[self testConfig]]; + + [self mockFlagResponse]; + + [OHHTTPStubs onStubActivation:^(NSURLRequest * _Nonnull request, id _Nonnull stub) { + if ([request.URL.host isEqualToString:kBaseUrl]) { + XCTFail(@"performFeatureFlagRequest should not make a flag request without a user"); + } + }]; + + [[LDRequestManager sharedInstance] performFeatureFlagRequest:nil]; } - (void)testEventRequestMakesHttpRequestWithMobileKey { @@ -89,21 +304,15 @@ - (void)testEventRequestMakesHttpRequestWithMobileKey { } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { httpRequestAttempted = YES; [responseArrived fulfill]; - return [OHHTTPStubsResponse responseWithData: data statusCode:200 headers:@{@"Content-Type":@"application/json"}]; + return [OHHTTPStubsResponse responseWithData: data statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; }]; - NSString *mobileKey = @"YOUR_MOBILE_KEY"; NSString *jsonEventString = @"[{\"kind\": \"feature\", \"user\": {\"key\" : \"jeff@test.com\", \"custom\" : {\"groups\" : [\"microsoft\", \"google\"]}}, \"creationDate\": 1438468068, \"key\": \"isConnected\", \"value\": true, \"default\": false}]"; NSData* eventData = [jsonEventString dataUsingEncoding:NSUTF8StringEncoding]; NSArray *eventsArray = [NSJSONSerialization JSONObjectWithData:eventData options:kNilOptions error:nil]; - LDRequestManager *requestManager = [[LDRequestManager alloc] init]; - [requestManager setMobileKey:mobileKey]; - [requestManager setEventsUrl:kEventsUrl]; - [requestManager setConnectionTimeout:10]; - - [requestManager performEventRequest:eventsArray]; + [[LDRequestManager sharedInstance] performEventRequest:eventsArray]; [self waitForExpectationsWithTimeout:10 handler:^(NSError *error){ // By the time we reach this code, the while loop has exited @@ -113,4 +322,67 @@ - (void)testEventRequestMakesHttpRequestWithMobileKey { }]; } +#pragma mark - Helpers + +- (id)mockClientWithUser:(LDUserModel*)user config:(LDConfig*)config { + id mockClient = OCMClassMock([LDClient class]); + OCMStub(ClassMethod([mockClient sharedInstance])).andReturn(mockClient); + OCMStub([mockClient ldUser]).andReturn(user); + XCTAssertEqual([LDClient sharedInstance].ldUser, user); + + if (!config) { + config = [self testConfig]; + } + + OCMStub([mockClient ldConfig]).andReturn(config); + XCTAssertEqual([LDClient sharedInstance].ldConfig, config); + + return mockClient; +} + +- (void)mockFlagResponse { + [OHHTTPStubs removeAllStubs]; + + [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { + return [request.URL.host isEqualToString:flagRequestHost]; + } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { + return [OHHTTPStubsResponse responseWithData:[self emptyJsonData] statusCode:httpStatusCodeOk headers:[self headerForStatusCode:httpStatusCodeOk]]; + }]; + + XCTAssertEqual([OHHTTPStubs allStubs].count, 1); +} + +- (LDUserModel*)mockUser { + LDUserBuilder *userBuilder = [[LDUserBuilder alloc] init]; + userBuilder.key = [[NSUUID UUID] UUIDString]; + userBuilder.email = @"jeff@test.com"; + + return [userBuilder build]; +} + +- (LDConfig*)testConfig { + LDConfig *config = [[LDConfig alloc] initWithMobileKey:testMobileKey]; + + return config; +} + +- (NSDictionary*)headerForStatusCode:(int)statusCode { + if (statusCode == httpStatusCodeOk) { + return @{@"Content-Type":@"application/json"}; + } + return @{@"Content-Type":@"text"}; +} + +- (NSData*)emptyJsonData { + return [[NSData alloc] initWithBase64EncodedString:[LDUtil base64EncodeString:emptyJson] options: 0]; +} + +- (NSData*)successJsonData { + return [[NSData alloc] initWithBase64EncodedString:[LDUtil base64EncodeString:@"{\"test-flag\":true}"] options: 0]; +} + +- (NSArray*)selectedNoFallbackStatusCodes { + return @[@304, @307, @401, @404, @412, @500]; +} + @end diff --git a/LaunchDarkly.podspec b/LaunchDarkly.podspec index 1e738a7e..cf5c886e 100644 --- a/LaunchDarkly.podspec +++ b/LaunchDarkly.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "LaunchDarkly" - s.version = "2.7.0" + s.version = "2.8.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.7.0" } + s.source = { :git => "https://github.com/launchdarkly/ios-client.git", :tag => "2.8.0" } s.source_files = "Darkly/*.{h,m}"