diff --git a/Classes/BITAuthenticator.m b/Classes/BITAuthenticator.m index dc952a8535..21cdc1fdf8 100644 --- a/Classes/BITAuthenticator.m +++ b/Classes/BITAuthenticator.m @@ -10,6 +10,8 @@ #import "HockeySDK.h" #import "HockeySDKPrivate.h" #import "BITAuthenticator_Private.h" +#import "BITHTTPOperation.h" + static NSString* const kBITAuthenticatorAuthTokenKey = @"BITAuthenticatorAuthTokenKey"; static NSString* const kBITAuthenticatorLastAuthenticatedVersionKey = @"BITAuthenticatorLastAuthenticatedVersionKey"; @@ -21,6 +23,7 @@ static NSString* const kBITAuthenticatorLastAuthenticatedVersionKey = @"BITAuthe - (void)dealloc { [self unregisterObservers]; + [self cancelOperationsWithPath:nil method:nil]; } - (instancetype) initWithAppIdentifier:(NSString *)appIdentifier isAppStoreEnvironemt:(BOOL)isAppStoreEnvironment { @@ -251,4 +254,73 @@ static NSString* const kBITAuthenticatorLastAuthenticatedVersionKey = @"BITAuthe [self validateInstallationWithCompletion:nil]; } } + +#pragma mark - Networking +- (NSMutableURLRequest *) requestWithMethod:(NSString*) method + path:(NSString *) path { + NSParameterAssert(self.serverURL); + NSParameterAssert(method); + path = path ? : @""; + + NSURL *endpoint = [[NSURL URLWithString:self.serverURL] URLByAppendingPathComponent:path]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:endpoint]; + request.HTTPMethod = method; + + return request; +} + +- (BITHTTPOperation*) operationWithURLRequest:(NSURLRequest*) request + completion:(BITNetworkCompletionBlock) completion { + BITHTTPOperation *operation = [BITHTTPOperation operationWithRequest:request + ]; + [operation setCompletion:completion]; + + return operation; +} + +- (void)getPath:(NSString *)path completion:(BITNetworkCompletionBlock)completion { + NSURLRequest *request = [self requestWithMethod:@"GET" path:path]; + BITHTTPOperation *op = [self operationWithURLRequest:request + completion:completion]; + [self enqeueHTTPOperation:op]; +} + +- (void) enqeueHTTPOperation:(BITHTTPOperation *) operation { + [self.operationQueue addOperation:operation]; +} + +- (NSUInteger) cancelOperationsWithPath:(NSString*) path + method:(NSString*) method { + NSUInteger cancelledOperations = 0; + for(BITHTTPOperation *operation in self.operationQueue.operations) { + NSURLRequest *request = operation.URLRequest; + + BOOL matchedMethod = YES; + if(method && ![request.HTTPMethod isEqualToString:method]) { + matchedMethod = NO; + } + + BOOL matchedPath = YES; + if(path) { + //method is not interesting here, we' just creating it to get the URL + NSURL *url = [self requestWithMethod:@"GET" path:path].URL; + matchedPath = [request.URL isEqual:url]; + } + + if(matchedPath && matchedMethod) { + ++cancelledOperations; + [operation cancel]; + } + } + return cancelledOperations; +} + +- (NSOperationQueue *)operationQueue { + if(nil == _operationQueue) { + _operationQueue = [[NSOperationQueue alloc] init]; + _operationQueue.maxConcurrentOperationCount = 1; + } + return _operationQueue; +} + @end diff --git a/Classes/BITAuthenticator_Private.h b/Classes/BITAuthenticator_Private.h index ddd24d61a8..8be28d7fe9 100644 --- a/Classes/BITAuthenticator_Private.h +++ b/Classes/BITAuthenticator_Private.h @@ -9,6 +9,7 @@ #import "BITAuthenticator.h" #import "BITHockeyBaseManagerPrivate.h" #import "BITAuthenticationViewController.h" +#import "BITHTTPOperation.h" //needed for typedef @interface BITAuthenticator () @@ -46,4 +47,57 @@ - (void) validationSucceeded; - (void) validationFailedWithError:(NSError *) validationError; + +#pragma mark - Networking helpers (TODO: move to base-class / networking component) +@property (nonatomic, strong) NSOperationQueue *operationQueue; + +/** + * creates an NRURLRequest for the given method and path by using + * the internally stored baseURL. + * + * @param method the HTTPMethod to check, must not be nil + * @param path path to append to baseURL. can be nil in which case "/" is appended + * + * @return an NSMutableURLRequest for further configuration + */ +- (NSMutableURLRequest *) requestWithMethod:(NSString*) method + path:(NSString *) path; +/** + * Creates an operation for the given NSURLRequest + * + * @param request the request that should be handled + * @param completion completionBlock that is called once the operation finished + * + * @return operation, which can be queued via enqueueHTTPOperation: + */ +- (BITHTTPOperation*) operationWithURLRequest:(NSURLRequest*) request + completion:(BITNetworkCompletionBlock) completion; + +/** + * Creates an operation for the given path, and enqueues it + * + * @param path the request path to check + * @param completion completionBlock that is called once the operation finished + * + */ +- (void) getPath:(NSString*) path + completion:(BITNetworkCompletionBlock) completion; + +/** + * adds the given operation to the internal queue + * + * @param operation operation to add + */ +- (void) enqeueHTTPOperation:(BITHTTPOperation *) operation; + +/** + * cancels the specified operations + * + * @param path the path which operation should be cancelled. Can be nil to match all + * @param method the method which operations to cancel. Can be nil to match all + * @return number of operations cancelled + */ +- (NSUInteger) cancelOperationsWithPath:(NSString*) path + method:(NSString*) method; + @end diff --git a/Classes/BITHTTPOperation.h b/Classes/BITHTTPOperation.h new file mode 100644 index 0000000000..278a335ce1 --- /dev/null +++ b/Classes/BITHTTPOperation.h @@ -0,0 +1,25 @@ +// +// BITHTTPOperation.h +// HockeySDK +// +// Created by Stephan Diederich on 10.08.13. +// +// + +#import + +@class BITHTTPOperation; +typedef void (^BITNetworkCompletionBlock)(BITHTTPOperation* operation, id response, NSError* error); + +@interface BITHTTPOperation : NSOperation + ++ (instancetype) operationWithRequest:(NSURLRequest *) urlRequest; + +@property (nonatomic, readonly) NSURLRequest *URLRequest; + +- (void) setCompletion:(BITNetworkCompletionBlock) completionBlock; + +@property (nonatomic, readonly) NSData *data; +@property (nonatomic, readonly) NSError *error; + +@end diff --git a/Classes/BITHTTPOperation.m b/Classes/BITHTTPOperation.m new file mode 100644 index 0000000000..52185d2bf3 --- /dev/null +++ b/Classes/BITHTTPOperation.m @@ -0,0 +1,118 @@ +// +// BITHTTPOperation.m +// HockeySDK +// +// Created by Stephan Diederich on 10.08.13. +// +// + +#import "BITHTTPOperation.h" + +@interface BITHTTPOperation() +@end + +@implementation BITHTTPOperation { + NSURLRequest *_URLRequest; + NSURLConnection *_connection; + NSMutableData *_data; + + BOOL _isExecuting; + BOOL _isFinished; +} + + ++ (instancetype)operationWithRequest:(NSURLRequest *)urlRequest { + BITHTTPOperation *op = [[self class] new]; + op->_URLRequest = urlRequest; + return op; +} + +#pragma mark - NSOperation overrides +- (BOOL)isConcurrent { + return YES; +} + +- (void)cancel { + [_connection cancel]; + [super cancel]; +} + +- (void) start { + if (![[NSThread currentThread] isMainThread]) { + [self performSelector:@selector(start) onThread:NSThread.mainThread withObject:nil waitUntilDone:NO]; + } + + [self willChangeValueForKey:@"isExecuting"]; + _isExecuting = YES; + [self didChangeValueForKey:@"isExecuting"]; + + _connection = [[NSURLConnection alloc] initWithRequest:_URLRequest + delegate:self + startImmediately:NO]; + + [_connection scheduleInRunLoop:[NSRunLoop currentRunLoop] + forMode:NSDefaultRunLoopMode]; + + [_connection start]; +} + +- (void) finish { + [self willChangeValueForKey:@"isExecuting"]; + [self willChangeValueForKey:@"isFinished"]; + _isExecuting = NO; + _isFinished = YES; + [self didChangeValueForKey:@"isExecuting"]; + [self didChangeValueForKey:@"isFinished"]; +} + +#pragma mark - NSURLConnectionDelegate +-(void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response { + _data = [[NSMutableData alloc] init]; +} + +-(void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data { + [_data appendData:data]; +} + +-(void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error { + //FINISHED and failed + _error = error; + _data = nil; + + [self finish]; +} + +-(void)connectionDidFinishLoading:(NSURLConnection*)connection { + [self finish]; +} + +#pragma mark - Public interface +- (NSData *)data { + return _data; +} + +- (void)setCompletion:(BITNetworkCompletionBlock)completion { + __weak typeof(self) weakSelf = self; + if(nil == completion) { + [super setCompletionBlock:nil]; + } else { + [super setCompletionBlock:^{ + typeof(self) strongSelf = weakSelf; + if(strongSelf) { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(strongSelf, strongSelf->_data, strongSelf->_error); + }); + } + }]; + } +} + +- (BOOL)isFinished { + return _isFinished; +} + +- (BOOL)isExecuting { + return _isExecuting; +} + +@end diff --git a/Support/HockeySDK.xcodeproj/project.pbxproj b/Support/HockeySDK.xcodeproj/project.pbxproj index 45066e4464..4f85e3cd3a 100644 --- a/Support/HockeySDK.xcodeproj/project.pbxproj +++ b/Support/HockeySDK.xcodeproj/project.pbxproj @@ -133,6 +133,8 @@ E48A3DEC17B3ED1C00924C3D /* BITAuthenticator.h in Headers */ = {isa = PBXBuildFile; fileRef = E48A3DEA17B3ED1C00924C3D /* BITAuthenticator.h */; settings = {ATTRIBUTES = (Public, ); }; }; E48A3DED17B3ED1C00924C3D /* BITAuthenticator.m in Sources */ = {isa = PBXBuildFile; fileRef = E48A3DEB17B3ED1C00924C3D /* BITAuthenticator.m */; }; E48A3DEF17B3EFF100924C3D /* BITAuthenticatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E48A3DEE17B3EFF100924C3D /* BITAuthenticatorTests.m */; }; + E4933E8017B66CDA00B11ACC /* BITHTTPOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = E4933E7E17B66CDA00B11ACC /* BITHTTPOperation.h */; }; + E4933E8117B66CDA00B11ACC /* BITHTTPOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = E4933E7F17B66CDA00B11ACC /* BITHTTPOperation.m */; }; E4B4DB7D17B435550099C67F /* BITAuthenticationViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = E4B4DB7B17B435550099C67F /* BITAuthenticationViewController.h */; }; E4B4DB7E17B435550099C67F /* BITAuthenticationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = E4B4DB7C17B435550099C67F /* BITAuthenticationViewController.m */; }; /* End PBXBuildFile section */ @@ -287,6 +289,8 @@ E48A3DEB17B3ED1C00924C3D /* BITAuthenticator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITAuthenticator.m; sourceTree = ""; }; E48A3DEE17B3EFF100924C3D /* BITAuthenticatorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITAuthenticatorTests.m; sourceTree = ""; }; E48A3DF117B408F400924C3D /* BITAuthenticator_Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BITAuthenticator_Private.h; sourceTree = ""; }; + E4933E7E17B66CDA00B11ACC /* BITHTTPOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITHTTPOperation.h; sourceTree = ""; }; + E4933E7F17B66CDA00B11ACC /* BITHTTPOperation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITHTTPOperation.m; sourceTree = ""; }; E4B4DB7B17B435550099C67F /* BITAuthenticationViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BITAuthenticationViewController.h; sourceTree = ""; }; E4B4DB7C17B435550099C67F /* BITAuthenticationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BITAuthenticationViewController.m; sourceTree = ""; }; /* End PBXFileReference section */ @@ -573,6 +577,8 @@ E48A3DF117B408F400924C3D /* BITAuthenticator_Private.h */, E4B4DB7B17B435550099C67F /* BITAuthenticationViewController.h */, E4B4DB7C17B435550099C67F /* BITAuthenticationViewController.m */, + E4933E7E17B66CDA00B11ACC /* BITHTTPOperation.h */, + E4933E7F17B66CDA00B11ACC /* BITHTTPOperation.m */, ); name = private; sourceTree = ""; @@ -608,6 +614,7 @@ 1E49A47C1612226D00463151 /* BITUpdateManagerPrivate.h in Headers */, 1E49A4851612226D00463151 /* BITUpdateViewControllerPrivate.h in Headers */, 1E49A4B5161222B900463151 /* BITHockeyBaseManagerPrivate.h in Headers */, + E4933E8017B66CDA00B11ACC /* BITHTTPOperation.h in Headers */, 1E49A4BE161222B900463151 /* BITHockeyHelper.h in Headers */, 1E49A4C4161222B900463151 /* BITAppStoreHeader.h in Headers */, 1E49A4CA161222B900463151 /* BITStoreButton.h in Headers */, @@ -825,6 +832,7 @@ 1E49A4451612223B00463151 /* BITFeedbackListViewCell.m in Sources */, 1E49A44B1612223B00463151 /* BITFeedbackListViewController.m in Sources */, 1E49A4511612223B00463151 /* BITFeedbackManager.m in Sources */, + E4933E8117B66CDA00B11ACC /* BITHTTPOperation.m in Sources */, 1E49A45A1612223B00463151 /* BITFeedbackMessage.m in Sources */, 1E49A4601612223B00463151 /* BITFeedbackUserDataViewController.m in Sources */, 1E49A4701612226D00463151 /* BITAppVersionMetaInfo.m in Sources */, diff --git a/Support/HockeySDKTests/BITAuthenticatorTests.m b/Support/HockeySDKTests/BITAuthenticatorTests.m index 6b517c7d14..f1a2499a95 100644 --- a/Support/HockeySDKTests/BITAuthenticatorTests.m +++ b/Support/HockeySDKTests/BITAuthenticatorTests.m @@ -17,7 +17,7 @@ #import "HockeySDK.h" #import "BITAuthenticator.h" #import "BITAuthenticator_Private.h" - +#import "BITHTTPOperation.h" #import "BITTestHelper.h" @interface MyDevice : NSObject @@ -287,4 +287,138 @@ [verifyCount(delegateMock, never()) authenticator:_sut failedToValidateInstallationWithError:(id)anything()]; [verifyCount(delegateMock, times(1)) authenticatorDidValidateInstallation:_sut]; } + +#pragma mark - Networking base tests +- (void) testThatURLRequestHasBaseURLSet { + _sut.serverURL = @"http://myserver.com"; + NSMutableURLRequest *request = [_sut requestWithMethod:@"GET" path:nil]; + assertThat(request.URL, equalTo([NSURL URLWithString:@"http://myserver.com/"])); +} + +- (void) testThatURLRequestHasPathAppended { + _sut.serverURL = @"http://myserver.com"; + NSMutableURLRequest *request = [_sut requestWithMethod:@"GET" path:@"projects"]; + assertThat(request.URL, equalTo([NSURL URLWithString:@"http://myserver.com/projects"])); +} + +- (void) testThatURLRequestHasMethodSet { + NSMutableURLRequest *request = [_sut requestWithMethod:@"POST" path:nil]; + + assertThat(request.HTTPMethod, equalTo(@"POST")); +} + +- (void) testThatOperationHasURLRequestSet { + _sut.serverURL = @"http://myserver.com"; + NSURLRequest *r = [_sut requestWithMethod:@"PUT" path:@"x"]; + BITHTTPOperation *op = [_sut operationWithURLRequest:r + completion:nil]; + assertThat(op.URLRequest, equalTo(r)); +} + +#pragma mark - Convenience methods +- (void) testThatGetPathCreatesAndEnquesAnOperation { + assertThatUnsignedInt(_sut.operationQueue.operationCount, equalToUnsignedInt(0)); + [given([_sut operationWithURLRequest:(id)anything() + completion:(id)anything()]) willReturn:[NSOperation new]]; + + [_sut getPath:@"endpoint" + completion:nil]; + assertThatUnsignedInt(_sut.operationQueue.operationCount, equalToUnsignedInt(1)); +} + +#pragma mark - Completion Tests +- (void) testThatCompletionIsCalled { + //TODO +} + +#pragma mark - HTTPOperation enqueuing / cancellation +- (void) testThatOperationIsQueued { + assertThatUnsignedInt(_sut.operationQueue.operationCount, equalToUnsignedInt(0)); + [_sut.operationQueue setSuspended:YES]; + BITHTTPOperation *op = [BITHTTPOperation new]; + [_sut enqeueHTTPOperation:op]; + + assertThatUnsignedInt(_sut.operationQueue.operationCount, equalToUnsignedInt(1)); +} + +- (void) testThatOperationCancellingMatchesAllOperationsWithNilMethod { + [_sut.operationQueue setSuspended:YES]; + NSURLRequest *requestGet = [_sut requestWithMethod:@"GET" path:nil]; + NSURLRequest *requestPut = [_sut requestWithMethod:@"PUT" path:nil]; + NSURLRequest *requestPost = [_sut requestWithMethod:@"POST" path:nil]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestGet + completion:nil]]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestPut + completion:nil]]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestPost + completion:nil]]; + assertThatUnsignedInt(_sut.operationQueue.operationCount, equalToUnsignedInt(3)); + NSUInteger numCancelled = [_sut cancelOperationsWithPath:nil method:nil]; + assertThatUnsignedInt(numCancelled, equalToUnsignedInt(3)); +} + +- (void) testThatOperationCancellingMatchesAllOperationsWithNilPath { + [_sut.operationQueue setSuspended:YES]; + NSURLRequest *requestGet = [_sut requestWithMethod:@"GET" path:@"test"]; + NSURLRequest *requestPut = [_sut requestWithMethod:@"PUT" path:@"Another/acas"]; + NSURLRequest *requestPost = [_sut requestWithMethod:@"POST" path:nil]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestGet + completion:nil]]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestPut + completion:nil]]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestPost + completion:nil]]; + assertThatUnsignedInt(_sut.operationQueue.operationCount, equalToUnsignedInt(3)); + NSUInteger numCancelled = [_sut cancelOperationsWithPath:nil method:nil]; + assertThatUnsignedInt(numCancelled, equalToUnsignedInt(3)); +} + + +- (void) testThatOperationCancellingMatchesAllOperationsWithSetPath { + NSURLRequest *requestGet = [_sut requestWithMethod:@"GET" path:@"test"]; + NSURLRequest *requestPut = [_sut requestWithMethod:@"PUT" path:@"Another/acas"]; + NSURLRequest *requestPost = [_sut requestWithMethod:@"POST" path:nil]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestGet + completion:nil]]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestPut + completion:nil]]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestPost + completion:nil]]; + assertThatUnsignedInt(_sut.operationQueue.operationCount, equalToUnsignedInt(3)); + [_sut cancelOperationsWithPath:@"Another/acas" method:nil]; + assertThatUnsignedInt(_sut.operationQueue.operationCount, equalToUnsignedInt(2)); +} + +- (void) testThatOperationCancellingMatchesAllOperationsWithSetMethod { + NSURLRequest *requestGet = [_sut requestWithMethod:@"GET" path:@"test"]; + NSURLRequest *requestPut = [_sut requestWithMethod:@"PUT" path:@"Another/acas"]; + NSURLRequest *requestPost = [_sut requestWithMethod:@"POST" path:nil]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestGet + completion:nil]]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestPut + completion:nil]]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestPost + completion:nil]]; + assertThatUnsignedInt(_sut.operationQueue.operationCount, equalToUnsignedInt(3)); + NSUInteger numCancelled = [_sut cancelOperationsWithPath:nil method:@"POST"]; + assertThatUnsignedInt(numCancelled, equalToUnsignedInt(1)); +} + +- (void) testThatOperationCancellingMatchesAllOperationsWithSetMethodAndPath { + NSURLRequest *requestGet = [_sut requestWithMethod:@"GET" path:@"test"]; + NSURLRequest *requestPut = [_sut requestWithMethod:@"PUT" path:@"Another/acas"]; + NSURLRequest *requestPost = [_sut requestWithMethod:@"POST" path:nil]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestGet + completion:nil]]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestPut + completion:nil]]; + [_sut enqeueHTTPOperation:[_sut operationWithURLRequest:requestPost + completion:nil]]; + assertThatUnsignedInt(_sut.operationQueue.operationCount, equalToUnsignedInt(3)); + NSUInteger numCancelled = [_sut cancelOperationsWithPath:@"Another/acas" method:@"PUT"]; + assertThatUnsignedInt(numCancelled, equalToUnsignedInt(1)); +} + +#pragma mark - Operation Testing + @end