TwitterNetworkLayerTests/TNLRequestOperationTest.m (1,054 lines of code) (raw):

// // TNLRequestOperationTest.m // TwitterNetworkLayer // // Created on 11/12/14. // Copyright © 2020 Twitter. All rights reserved. // #include <pthread.h> #import "NSDictionary+TNLAdditions.h" #import "TNL_Project.h" #import "TNLGlobalConfiguration_Project.h" #import "TNLHTTPRequest.h" #import "TNLPseudoURLProtocol.h" #import "TNLRequestDelegate.h" #import "TNLRequestOperation_Project.h" #import "TNLRequestOperationCancelSource.h" #import "TNLRequestOperationQueue_Project.h" #import "TNLRequestRetryPolicyProvider.h" #import "TNLTemporaryFile_Project.h" @import ObjectiveC.runtime; #if TARGET_OS_IPHONE // == IOS + WATCH + TV @import UIKit.UIApplication; // for notification names #endif @import XCTest; // unit tests should never hit the network for CI. can turn this to 0 to hit the network for local testing if desired (be careful!) #define RUN_TESTS_WITH_CANNED_RESPONSES 1 // background requests cannot be run from unit tests, so this will stay 0 #define RUN_BACKGROUND_REQUESTS 0 #define kBODY_DICTIONARY @{@"body":@"this is the body"} static NSError *CoersedOperationError(NSError *error); static NSError *CoersedOperationError(NSError *error) { if ([error.domain isEqualToString:TNLErrorDomain] && error.code == TNLErrorCodeRequestOperationInvalidHydratedRequest) { error = error.userInfo[NSUnderlyingErrorKey]; } return error; } #if TARGET_OS_IPHONE // == IOS + WATCH + TV @interface FakeApplication : NSObject @property (nonatomic) UIApplicationState applicationState; - (instancetype)init; @end static FakeApplication *sFakeApplication = nil; static void FireFakeApplicationNotification(NSString *notificationName, NSTimeInterval delay); static void FireFakeApplicationNotification(NSString *notificationName, NSTimeInterval delay) { UIApplicationState newState = sFakeApplication.applicationState; BOOL updateStateAfterNotification = NO; if ([notificationName isEqualToString:UIApplicationWillResignActiveNotification]) { updateStateAfterNotification = YES; newState = UIApplicationStateInactive; } else if ([notificationName isEqualToString:UIApplicationWillEnterForegroundNotification]) { updateStateAfterNotification = YES; newState = UIApplicationStateInactive; } else if ([notificationName isEqualToString:UIApplicationDidBecomeActiveNotification]) { newState = UIApplicationStateActive; } else if ([notificationName isEqualToString:UIApplicationDidEnterBackgroundNotification]) { newState = UIApplicationStateBackground; } UIApplication *application = (id)sFakeApplication; dispatch_block_t block = ^{ if (!updateStateAfterNotification) { sFakeApplication.applicationState = newState; } [[NSNotificationCenter defaultCenter] postNotificationName:notificationName object:application]; if (updateStateAfterNotification) { sFakeApplication.applicationState = newState; } }; if (delay > 0) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), block); } else { block(); } } @implementation FakeApplication - (instancetype)init { if (self = [super init]) { _applicationState = UIApplicationStateBackground; } return self; } @end #endif // TARGET_OS_IPHONE @interface TestJSONResponse : TNLResponse @property (nonatomic, readonly) NSDictionary *result; @property (nonatomic, readonly) NSError *jsonParseError; @property (nonatomic, readonly) BOOL responseBodyWasInFile; @end @interface TestRetryOnceOn503Response : NSObject <TNLRequestRetryPolicyProvider> @end @interface TestTNLRequestDelegate : NSObject <TNLRequestDelegate> @property (atomic, readonly) NSArray<NSString *> *observedCallbacks; @end typedef void(^TestCallbackBlock)(TestJSONResponse *response); @interface TNLRequestOperationTest : XCTestCase <TNLRequestDelegate> { NSMutableArray *_registeredEndpoints; dispatch_queue_t _delegateQueue; } @property (atomic) BOOL responseWasReceived; @property (atomic) BOOL attemptDidComplete; @property (atomic) NSNumber *requestClogDuration; - (void)cleanupPseudoProtocol; - (void)unregisterRequest:(TNLHTTPRequest *)request; - (void)registerRequest:(TNLHTTPRequest *)request args:(NSDictionary *)args; - (TNLMutableRequestConfiguration *)config; - (TNLMutableHTTPRequest *)httpBinRequest:(NSDictionary *)args; - (NSData *)uploadData; - (NSString *)uploadFile; - (NSInputStream *)uploadStream; - (TNLRequestOperation *)executeRequest:(TNLHTTPRequest *)request config:(TNLRequestConfiguration *)config; - (void)performRequest:(TNLHTTPRequest *)request config:(TNLRequestConfiguration *)config args:(NSDictionary *)args callback:(TestCallbackBlock)callback; @end @implementation TNLRequestOperationTest + (void)setUp { [super setUp]; #if TARGET_OS_IPHONE // == IOS + WATCH + TV (void)[TNLGlobalConfiguration sharedInstance]; sFakeApplication = [[FakeApplication alloc] init]; sFakeApplication.applicationState = UIApplicationStateActive; FireFakeApplicationNotification(UIApplicationDidFinishLaunchingNotification, 0); #endif } + (void)tearDown { #if TARGET_OS_IPHONE // == IOS + WATCH + TV sFakeApplication.applicationState = UIApplicationStateActive; FireFakeApplicationNotification(UIApplicationDidFinishLaunchingNotification, 0); sFakeApplication = nil; #endif [super tearDown]; } - (void)setUp { [super setUp]; _registeredEndpoints = [NSMutableArray array]; _delegateQueue = dispatch_queue_create("TNLRequestOperationTest.delegate.queue", DISPATCH_QUEUE_SERIAL); } - (void)tearDown { _delegateQueue = nil; [self cleanupPseudoProtocol]; self.attemptDidComplete = NO; self.responseWasReceived = NO; [super tearDown]; } - (void)cleanupPseudoProtocol { for (NSURL *url in _registeredEndpoints) { [TNLPseudoURLProtocol unregisterEndpoint:url]; } [_registeredEndpoints removeAllObjects]; } - (void)unregisterRequest:(TNLHTTPRequest *)request { [TNLPseudoURLProtocol unregisterEndpoint:request.URL]; [_registeredEndpoints removeObject:request.URL]; } - (void)registerRequest:(TNLHTTPRequest *)request args:(NSDictionary *)args { [self registerRequest:request statusCode:200 args:args]; } - (void)registerRequest:(TNLHTTPRequest *)request statusCode:(NSInteger)statusCode args:(NSDictionary *)args { #if RUN_TESTS_WITH_CANNED_RESPONSES NSURL *endpoint = request.URL; const BOOL hasBody = request.HTTPBody || request.HTTPBodyFilePath || request.HTTPBodyStream; NSMutableDictionary *headers = [request.allHTTPHeaderFields mutableCopy]; headers[@"Host"] = endpoint.host; if (hasBody) { if (!request.HTTPBody && !request.HTTPBodyFilePath) { // streamed requests are "chunked" headers[@"Transfer-Encoding"] = @"Chunked"; } else { headers[@"Content-Length"] = @(self.uploadData.length).stringValue; } headers[@"Content-Type"] = TNLHTTPContentTypeJSON; } NSDictionary *responseBodyJSON = @{ @"args" : args ?: @{}, @"headers" : headers, @"url" : endpoint.absoluteString, @"json" : (hasBody) ? kBODY_DICTIONARY : [NSNull null] }; NSData *body = [NSJSONSerialization dataWithJSONObject:responseBodyJSON options:0 error:NULL]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:endpoint statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:@{ @"Content-Type" : @"application/json" }]; [TNLPseudoURLProtocol registerURLResponse:response body:body withEndpoint:endpoint]; [_registeredEndpoints addObject:endpoint]; #endif // RUN_TESTS_WITH_CANNED_RESPONSES } - (TNLMutableRequestConfiguration *)config { TNLMutableRequestConfiguration *config = [TNLMutableRequestConfiguration defaultConfiguration]; config.URLCache = nil; config.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; #if RUN_TESTS_WITH_CANNED_RESPONSES config.protocolOptions = TNLRequestProtocolOptionPseudo; #endif // RUN_TESTS_WITH_CANNED_RESPONSES return config; } - (TNLMutableHTTPRequest *)httpBinRequest:(NSDictionary *)args { NSString *params = [[[TNLParameterCollection alloc] initWithDictionary:args] URLEncodedStringValueWithOptions:TNLURLEncodingOptionsNone]; NSString *urlString = [NSString stringWithFormat:@"http://httpbin.org/%@", args[@"method"] ?: @"get"]; if (params.length) { urlString = [urlString stringByAppendingFormat:@"?%@", params]; } NSURL *endpoint = [NSURL URLWithString:urlString]; TNLMutableHTTPRequest *request = [[TNLMutableHTTPRequest alloc] initWithURL:endpoint HTTPMethodValue:TNLHTTPMethodUnknown HTTPHeaderFields:@{ @"TNL-Version" : TNLVersion() } HTTPBody:nil HTTPBodyStream:nil HTTPBodyFilePath:nil]; return request; } - (NSData *)uploadData { static NSData *sData = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sData = [NSJSONSerialization dataWithJSONObject:kBODY_DICTIONARY options:0 error:NULL]; }); return sData; } - (NSString *)uploadFile { static NSString *sFile; static dispatch_once_t sOnce; dispatch_once(&sOnce, ^{ NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"com.twitter.tnl.request.operation.test.json"]; [[NSFileManager defaultManager] removeItemAtPath:filePath error:NULL]; if ([self.uploadData writeToFile:filePath atomically:YES]) { sFile = filePath; } }); return sFile; } - (NSInputStream *)uploadStream { NSInputStream *inputStream = nil; NSString *path = self.uploadFile; if (path) { inputStream = [NSInputStream inputStreamWithFileAtPath:self.uploadFile]; } else { NSData *data = self.uploadData; if (data) { inputStream = [NSInputStream inputStreamWithData:data]; } } return inputStream; } - (TNLRequestOperation *)executeRequest:(TNLHTTPRequest *)request config:(TNLRequestConfiguration *)config { TNLRequestOperation *op = [TNLRequestOperation operationWithRequest:request responseClass:[TestJSONResponse class] configuration:config delegate:self]; [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:op]; return op; } - (void)performRequest:(TNLHTTPRequest *)request config:(TNLRequestConfiguration *)config args:(NSDictionary *)args callback:(TestCallbackBlock)callback { [self registerRequest:request args:args]; TNLRequestOperation *op = [self executeRequest:request config:config]; [op waitUntilFinishedWithoutBlockingRunLoop]; XCTAssertTrue(self.responseWasReceived); XCTAssertTrue(self.attemptDidComplete); TestJSONResponse *response = (id)op.response; if (callback) { callback(response); } [self unregisterRequest:request]; self.responseWasReceived = NO; self.attemptDidComplete = NO; } #pragma mark - TNLRequestDelegate - (void)tnl_requestOperation:(TNLRequestOperation *)op hydrateRequest:(id<TNLRequest>)request completion:(TNLRequestHydrateCompletionBlock)complete { NSNumber *sleepDuration = self.requestClogDuration; if (sleepDuration) { [NSThread sleepForTimeInterval:[sleepDuration doubleValue]]; } complete(request, nil); } - (void)tnl_requestOperation:(TNLRequestOperation *)op didCompleteWithResponse:(TNLResponse *)response { self.responseWasReceived = YES; XCTAssertTrue(self.attemptDidComplete, @"%@", response); } - (void)tnl_requestOperation:(TNLRequestOperation *)op didCompleteAttemptWithResponse:(TNLResponse *)response disposition:(TNLAttemptCompleteDisposition)disposition { self.attemptDidComplete = YES; XCTAssertFalse(self.responseWasReceived, @"%@", response); } - (dispatch_queue_t)tnl_delegateQueueForRequestOperation:(TNLRequestOperation *)op { return _delegateQueue; } - (dispatch_queue_t)tnl_completionQueueForRequestOperation:(TNLRequestOperation *)op { return _delegateQueue; } #pragma mark - Tests - (void)testGET { NSDictionary *args = @{@"method":@"get"}; TNLMutableHTTPRequest *request = [self httpBinRequest:args]; request.HTTPMethodValue = TNLHTTPMethodGET; TNLMutableRequestConfiguration *config = self.config; // Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)0); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertFalse(response.responseBodyWasInFile); }]; // Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)0); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertTrue(response.responseBodyWasInFile); }]; #if !RUN_TESTS_WITH_CANNED_RESPONSES // Background - Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeBackground; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestInvalidBackgroundRequest); }]; #if RUN_BACKGROUND_REQUESTS // Background - Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)0); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertTrue(response.responseBodyWasInFile); }]; #endif // RUN_BACKGROUND_REQUESTS #endif // !RUN_TESTS_WITH_CANNED_RESPONSES } - (void)testPOSTWithoutBody { NSDictionary *args = @{@"method":@"post"}; TNLMutableHTTPRequest *request = [self httpBinRequest:args]; request.HTTPMethodValue = TNLHTTPMethodPOST; TNLMutableRequestConfiguration *config = self.config; // Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)0); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertEqualObjects(response.result[@"json"], [NSNull null]); XCTAssertFalse(response.responseBodyWasInFile); }]; // Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)0); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertEqualObjects(response.result[@"json"], [NSNull null]); XCTAssertTrue(response.responseBodyWasInFile); }]; #if !RUN_TESTS_WITH_CANNED_RESPONSES // Background - Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeBackground; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestInvalidBackgroundRequest); }]; #if RUN_BACKGROUND_REQUESTS // Background - Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)0); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertEqualObjects(response.result[@"json"], [NSNull null]); XCTAssertTrue(response.responseBodyWasInFile); }]; #endif // RUN_BACKGROUND_REQUESTS #endif // !RUN_TESTS_WITH_CANNED_RESPONSES } - (void)testPOSTWithData { NSDictionary *args = @{@"method":@"post"}; TNLMutableHTTPRequest *request = [self httpBinRequest:args]; request.HTTPMethodValue = TNLHTTPMethodPOST; request.HTTPBody = self.uploadData; [request setValue:TNLHTTPContentTypeJSON forHTTPHeaderField:@"Content-Type"]; TNLMutableRequestConfiguration *config = self.config; // Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)self.uploadData.length); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertEqualObjects(response.result[@"json"], kBODY_DICTIONARY); XCTAssertFalse(response.responseBodyWasInFile); }]; // Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload); }]; #if !RUN_TESTS_WITH_CANNED_RESPONSES // Background - Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeBackground; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestInvalidBackgroundRequest); }]; // Background - Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload); }]; #endif // !RUN_TESTS_WITH_CANNED_RESPONSES } - (void)testPOSTWithStream { NSDictionary *args = @{@"method":@"post"}; TNLMutableHTTPRequest *request = [self httpBinRequest:args]; request.HTTPMethodValue = TNLHTTPMethodPOST; request.HTTPBodyStream = self.uploadStream; [request setValue:TNLHTTPContentTypeJSON forHTTPHeaderField:@"Content-Type"]; TNLMutableRequestConfiguration *config = self.config; // Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqualObjects([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Transfer-Encoding"] lowercaseString], @"chunked"); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertEqualObjects(response.result[@"json"], kBODY_DICTIONARY); XCTAssertFalse(response.responseBodyWasInFile); }]; // Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload); }]; #if !RUN_TESTS_WITH_CANNED_RESPONSES // Background - Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeBackground; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestInvalidBackgroundRequest); }]; // Background - Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload); }]; #endif // !RUN_TESTS_WITH_CANNED_RESPONSES } - (void)testPOSTWithFile { NSDictionary *args = @{@"method":@"post"}; TNLMutableHTTPRequest *request = [self httpBinRequest:args]; request.HTTPMethodValue = TNLHTTPMethodPOST; request.HTTPBodyFilePath = self.uploadFile; [request setValue:TNLHTTPContentTypeJSON forHTTPHeaderField:@"Content-Type"]; TNLMutableRequestConfiguration *config = self.config; // Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectsForCaseInsensitiveKey:@"TNL-Version"].firstObject, TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectsForCaseInsensitiveKey:@"Content-Length"].firstObject integerValue], (NSInteger)self.uploadData.length); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertEqualObjects(response.result[@"json"], kBODY_DICTIONARY); XCTAssertFalse(response.responseBodyWasInFile); }]; // Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload); }]; #if !RUN_TESTS_WITH_CANNED_RESPONSES #if RUN_BACKGROUND_REQUESTS // Background - Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeBackground; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)self.uploadData.length); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertEqualObjects(response.result[@"json"], kBODY_DICTIONARY); XCTAssertFalse(response.responseBodyWasInFile); }]; #endif // RUN_BACKGROUND_REQUESTS // Background - Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload); }]; #endif // !RUN_TESTS_WITH_CANNED_RESPONSES } - (void)testPUTWithoutBody { NSDictionary *args = @{@"method":@"put"}; TNLMutableHTTPRequest *request = [self httpBinRequest:args]; request.HTTPMethodValue = TNLHTTPMethodPUT; TNLMutableRequestConfiguration *config = self.config; // Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)0); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertEqualObjects(response.result[@"json"], [NSNull null]); XCTAssertFalse(response.responseBodyWasInFile); }]; // Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)0); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertEqualObjects(response.result[@"json"], [NSNull null]); XCTAssertTrue(response.responseBodyWasInFile); }]; #if !RUN_TESTS_WITH_CANNED_RESPONSES // Background - Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeBackground; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestInvalidBackgroundRequest); }]; #if RUN_BACKGROUND_REQUESTS // Background - Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)0); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertEqualObjects(response.result[@"json"], [NSNull null]); XCTAssertTrue(response.responseBodyWasInFile); }]; #endif // RUN_BACKGROUND_REQUESTS #endif // !RUN_TESTS_WITH_CANNED_RESPONSES } - (void)testPUTWithData { NSDictionary *args = @{@"method":@"put"}; TNLMutableHTTPRequest *request = [self httpBinRequest:args]; request.HTTPMethodValue = TNLHTTPMethodPUT; request.HTTPBody = self.uploadData; [request setValue:TNLHTTPContentTypeJSON forHTTPHeaderField:@"Content-Type"]; TNLMutableRequestConfiguration *config = self.config; // Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)self.uploadData.length); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertEqualObjects(response.result[@"json"], kBODY_DICTIONARY); XCTAssertFalse(response.responseBodyWasInFile); }]; // Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload); }]; #if !RUN_TESTS_WITH_CANNED_RESPONSES // Background - Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeBackground; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestInvalidBackgroundRequest); }]; // Background - Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload); }]; #endif // !RUN_TESTS_WITH_CANNED_RESPONSES } - (void)testPUTWithStream { NSDictionary *args = @{@"method":@"put"}; TNLMutableHTTPRequest *request = [self httpBinRequest:args]; request.HTTPMethodValue = TNLHTTPMethodPUT; request.HTTPBodyStream = self.uploadStream; [request setValue:TNLHTTPContentTypeJSON forHTTPHeaderField:@"Content-Type"]; TNLMutableRequestConfiguration *config = self.config; // Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqualObjects([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Transfer-Encoding"] lowercaseString], @"chunked"); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertEqualObjects(response.result[@"json"], kBODY_DICTIONARY); XCTAssertFalse(response.responseBodyWasInFile); }]; // Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload); }]; #if !RUN_TESTS_WITH_CANNED_RESPONSES // Background - Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeBackground; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestInvalidBackgroundRequest); }]; // Background - Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload); }]; #endif // !RUN_TESTS_WITH_CANNED_RESPONSES } - (void)testPUTWithFile { NSDictionary *args = @{@"method":@"put"}; TNLMutableHTTPRequest *request = [self httpBinRequest:args]; request.HTTPMethodValue = TNLHTTPMethodPUT; request.HTTPBodyFilePath = self.uploadFile; [request setValue:TNLHTTPContentTypeJSON forHTTPHeaderField:@"Content-Type"]; TNLMutableRequestConfiguration *config = self.config; // Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)self.uploadData.length); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertEqualObjects(response.result[@"json"], kBODY_DICTIONARY); XCTAssertFalse(response.responseBodyWasInFile); }]; // Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload); }]; #if !RUN_TESTS_WITH_CANNED_RESPONSES #if RUN_BACKGROUND_REQUESTS // Background - Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeBackground; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)self.uploadData.length); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertEqualObjects(response.result[@"json"], kBODY_DICTIONARY); XCTAssertFalse(response.responseBodyWasInFile); }]; #endif // RUN_BACKGROUND_REQUESTS // Background - Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestHTTPBodyCannotBeSetForDownload); }]; #endif // !RUN_TESTS_WITH_CANNED_RESPONSES } - (void)testDELETE { NSDictionary *args = @{@"method":@"delete"}; TNLMutableHTTPRequest *request = [self httpBinRequest:args]; request.HTTPMethodValue = TNLHTTPMethodDELETE; TNLMutableRequestConfiguration *config = self.config; // Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)0); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertFalse(response.responseBodyWasInFile); }]; // Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result, @"Something happened: %@", response.operationError); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)0); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertTrue(response.responseBodyWasInFile); }]; #if !RUN_TESTS_WITH_CANNED_RESPONSES // Background - Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeBackground; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { NSError *error = CoersedOperationError(response.operationError); XCTAssertEqualObjects(error.domain, TNLErrorDomain); XCTAssertEqual(error.code, TNLErrorCodeRequestInvalidBackgroundRequest); }]; #if RUN_BACKGROUND_REQUESTS // Background - Save to Disk config.responseDataConsumptionMode = TNLResponseDataConsumptionModeSaveToDisk; config.executionMode = TNLRequestExecutionModeInApp; [self performRequest:request config:config args:args callback:^(TestJSONResponse *response) { XCTAssertEqual(response.info.statusCode, 200); XCTAssertNotNil(response.result); XCTAssertEqualObjects([response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"TNL-Version"], TNLVersion()); XCTAssertEqual([[response.result[@"headers"] tnl_objectForCaseInsensitiveKey:@"Content-Length"] integerValue], (NSInteger)0); XCTAssertEqualObjects(response.result[@"args"], args); XCTAssertTrue(response.responseBodyWasInFile); }]; #endif // RUN_BACKGROUND_REQUESTS #endif // !RUN_TESTS_WITH_CANNED_RESPONSES } - (void)testCancelBeforeStart { NSDictionary *args = @{@"method":@"get"}; TNLMutableHTTPRequest *request = [self httpBinRequest:args]; request.HTTPMethodValue = TNLHTTPMethodGET; TNLMutableRequestConfiguration *config = self.config; // Normal config.responseDataConsumptionMode = TNLResponseDataConsumptionModeStoreInMemory; config.executionMode = TNLRequestExecutionModeInApp; __block TNLResponse *completedResponse; TNLRequestOperation *operation; completedResponse = nil; operation = [TNLRequestOperation operationWithRequest:request completion:^(TNLRequestOperation *op, TNLResponse *response) { completedResponse = response; }]; [operation cancelWithSource:@"Early Cancel"]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3.0]]; XCTAssertFalse(operation.isFinished); [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:operation]; [operation waitUntilFinishedWithoutBlockingRunLoop]; XCTAssertEqualObjects(TNLErrorDomain, completedResponse.operationError.domain); XCTAssertEqual(TNLErrorCodeRequestOperationCancelled, completedResponse.operationError.code); completedResponse = nil; operation = [TNLRequestOperation operationWithRequest:request completion:^(TNLRequestOperation *op, TNLResponse *response) { completedResponse = response; }]; [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:operation]; [operation cancelWithSource:@"Early Cancel"]; [operation waitUntilFinishedWithoutBlockingRunLoop]; XCTAssertEqualObjects(TNLErrorDomain, completedResponse.operationError.domain); XCTAssertEqual(TNLErrorCodeRequestOperationCancelled, completedResponse.operationError.code); } - (TNLResponse *)_runCloggedCallback:(NSTimeInterval)lockedDuration { BOOL oldShouldForceCrashOnCloggedCallback = [TNLGlobalConfiguration sharedInstance].shouldForceCrashOnCloggedCallback; NSTimeInterval oldCallbackTimeout = [TNLGlobalConfiguration sharedInstance].requestOperationCallbackTimeout; [TNLGlobalConfiguration sharedInstance].shouldForceCrashOnCloggedCallback = NO; [TNLGlobalConfiguration sharedInstance].requestOperationCallbackTimeout = 3.0; NSDictionary *args = @{@"method":@"get"}; TNLMutableHTTPRequest *request = [self httpBinRequest:args]; request.HTTPMethodValue = TNLHTTPMethodGET; TNLMutableRequestConfiguration *config = self.config; TNLRequestOperation *op; // Register request op = [TNLRequestOperation operationWithRequest:request configuration:config delegate:self]; [self registerRequest:request args:args]; // Clog w/ sleep self.requestClogDuration = @(lockedDuration); // Run [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:op]; [op waitUntilFinishedWithoutBlockingRunLoop]; [self unregisterRequest:request]; TNLResponse *response = op.response; // Reset global config [TNLGlobalConfiguration sharedInstance].shouldForceCrashOnCloggedCallback = oldShouldForceCrashOnCloggedCallback; [TNLGlobalConfiguration sharedInstance].requestOperationCallbackTimeout = oldCallbackTimeout; return response; } - (void)testCallbackClog1_NormalFailure { TNLResponse *response = [self _runCloggedCallback:5.0]; XCTAssertEqualObjects(response.operationError.domain, TNLErrorDomain); XCTAssertEqual(response.operationError.code, TNLErrorCodeRequestOperationCallbackTimedOut); XCTAssertTrue([[response.operationError.userInfo[@"timeoutTags"] firstObject] hasPrefix:NSStringFromClass([self class])]); } #if TARGET_OS_IPHONE // == IOS + WATCH + TV - (void)testCallbackClog2_ClogBackgroundForegroundUnclogSuccess { FireFakeApplicationNotification(UIApplicationWillResignActiveNotification, 0.5); FireFakeApplicationNotification(UIApplicationDidBecomeActiveNotification, 5.0); TNLResponse *response = [self _runCloggedCallback:6.0]; XCTAssertNil(response.operationError); XCTAssertNotNil(response); } - (void)testCallbackClog3_ClogBackgroundForegroundStillCloggedFailure { FireFakeApplicationNotification(UIApplicationWillResignActiveNotification, 0.5); FireFakeApplicationNotification(UIApplicationDidBecomeActiveNotification, 5.0); TNLResponse *response = [self _runCloggedCallback:9.0]; XCTAssertEqualObjects(response.operationError.domain, TNLErrorDomain); XCTAssertEqual(response.operationError.code, TNLErrorCodeRequestOperationCallbackTimedOut); XCTAssertTrue([[response.operationError.userInfo[@"timeoutTags"] firstObject] hasPrefix:NSStringFromClass([self class])]); } #endif // TARGET_OS_IPHONE - (void)testIdleTimeoutModes { NSURL *URL = [NSURL URLWithString:@"https://www.idle.timeouts.com/dummy"]; const NSTimeInterval latency = 3.0; const NSTimeInterval idleTimeout = 1.0; XCTAssert(latency > idleTimeout); TNLMutableHTTPRequest *request = [TNLMutableHTTPRequest GETRequestWithURL:URL HTTPHeaderFields:nil]; request.HTTPMethodValue = TNLHTTPMethodGET; TNLMutableRequestConfiguration *config = self.config; config.idleTimeout = idleTimeout; config.protocolOptions = TNLRequestProtocolOptionPseudo; @autoreleasepool { NSData *binaryData = [NSData dataWithContentsOfFile:[NSBundle bundleForClass:[self class]].executablePath]; NSMutableData *responseData = [binaryData mutableCopy]; const NSUInteger responseLength = 1000000; while (responseData.length < responseLength) { [responseData appendData:binaryData]; } [responseData setLength:responseLength]; NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:@{ @"Content-Type" : @"application/octet-stream" }]; TNLPseudoURLResponseConfig *responseConfig = [[TNLPseudoURLResponseConfig alloc] init]; responseConfig.bps = 2000000; responseConfig.latency = (uint64_t)(latency * 1000.0); [TNLPseudoURLProtocol registerURLResponse:response body:responseData config:responseConfig withEndpoint:URL]; } __block TNLResponse *completedResponse; TNLRequestOperation *operation; // Idle Timeout Includes Initial Connection [TNLGlobalConfiguration sharedInstance].idleTimeoutMode = TNLGlobalConfigurationIdleTimeoutModeEnabledIncludingInitialConnection; completedResponse = nil; operation = [TNLRequestOperation operationWithRequest:request configuration:config completion:^(TNLRequestOperation *op, TNLResponse *response) { completedResponse = response; }]; [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:operation]; [operation waitUntilFinishedWithoutBlockingRunLoop]; XCTAssertEqualObjects(completedResponse.operationError.domain, TNLErrorDomain); XCTAssertEqual(completedResponse.operationError.code, TNLErrorCodeRequestOperationIdleTimedOut); XCTAssertGreaterThan(completedResponse.metrics.totalDuration, idleTimeout); XCTAssertLessThan(completedResponse.metrics.totalDuration, idleTimeout + latency); NSLog(@"Idle Timeout Includes Initial Connection: %.3fs", completedResponse.metrics.totalDuration); // Idle Timeout Excludes Initial Connection [TNLGlobalConfiguration sharedInstance].idleTimeoutMode = TNLGlobalConfigurationIdleTimeoutModeEnabledExcludingInitialConnection; completedResponse = nil; operation = [TNLRequestOperation operationWithRequest:request configuration:config completion:^(TNLRequestOperation *op, TNLResponse *response) { completedResponse = response; }]; [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:operation]; [operation waitUntilFinishedWithoutBlockingRunLoop]; XCTAssertEqualObjects(completedResponse.operationError.domain, TNLErrorDomain); XCTAssertEqual(completedResponse.operationError.code, TNLErrorCodeRequestOperationIdleTimedOut); XCTAssertGreaterThan(completedResponse.metrics.totalDuration, latency); XCTAssertGreaterThan(completedResponse.metrics.totalDuration, idleTimeout + latency); NSLog(@"Idle Timeout Excludes Initial Connection: %.3fs", completedResponse.metrics.totalDuration); // No Idle Timeout [TNLGlobalConfiguration sharedInstance].idleTimeoutMode = TNLGlobalConfigurationIdleTimeoutModeDisabled; completedResponse = nil; operation = [TNLRequestOperation operationWithRequest:request configuration:config completion:^(TNLRequestOperation *op, TNLResponse *response) { completedResponse = response; }]; [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:operation]; [operation waitUntilFinishedWithoutBlockingRunLoop]; XCTAssertNil(completedResponse.operationError); XCTAssertGreaterThan(completedResponse.metrics.totalDuration, latency); XCTAssertGreaterThan(completedResponse.metrics.totalDuration, latency * 2); NSLog(@"Idle Timeout Disabled: %.3fs", completedResponse.metrics.totalDuration); [TNLGlobalConfiguration sharedInstance].idleTimeoutMode = TNLGlobalConfigurationIdleTimeoutModeDefault; [TNLPseudoURLProtocol unregisterEndpoint:URL]; } - (void)testOrderOfCallbacks { NSDictionary *args = @{@"method":@"get"}; TNLMutableHTTPRequest *request = [self httpBinRequest:args]; request.HTTPMethodValue = TNLHTTPMethodGET; TNLMutableRequestConfiguration *config = self.config; TestTNLRequestDelegate *delegate = [[TestTNLRequestDelegate alloc] init]; [self registerRequest:request args:args]; TNLRequestOperation *op = [TNLRequestOperation operationWithRequest:request responseClass:[TestJSONResponse class] configuration:config delegate:delegate]; op.context = delegate; [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:op]; [op waitUntilFinishedWithoutBlockingRunLoop]; [self unregisterRequest:request]; NSArray<NSString *> *expectedCallbacks = @[ NSStringFromSelector(@selector(tnl_requestOperation:didReceiveURLResponse:)), NSStringFromSelector(@selector(tnl_requestOperation:didCompleteAttemptWithResponse:disposition:)), NSStringFromSelector(@selector(tnl_requestOperation:didCompleteWithResponse:)) ]; NSArray<NSString *> *observedCallbacks = delegate.observedCallbacks; XCTAssertEqualObjects(expectedCallbacks, observedCallbacks); } - (void)testOrderOfCallbacksWithRetry { NSDictionary *args = @{@"method":@"status/503"}; TNLMutableHTTPRequest *request = [self httpBinRequest:args]; request.HTTPMethodValue = TNLHTTPMethodGET; [self registerRequest:request statusCode:503 args:args]; TNLMutableRequestConfiguration *config = self.config; config.retryPolicyProvider = [[TestRetryOnceOn503Response alloc] init]; TestTNLRequestDelegate *delegate = [[TestTNLRequestDelegate alloc] init]; TNLRequestOperation *op = [TNLRequestOperation operationWithRequest:request responseClass:[TestJSONResponse class] configuration:config delegate:delegate]; op.context = delegate; [[TNLRequestOperationQueue defaultOperationQueue] enqueueRequestOperation:op]; [op waitUntilFinishedWithoutBlockingRunLoop]; [self unregisterRequest:request]; NSArray<NSString *> *expectedCallbacks = @[ NSStringFromSelector(@selector(tnl_requestOperation:didReceiveURLResponse:)), NSStringFromSelector(@selector(tnl_requestOperation:didCompleteAttemptWithResponse:disposition:)), NSStringFromSelector(@selector(tnl_requestOperation:willStartRetryFromResponse:policyProvider:afterDelay:)), NSStringFromSelector(@selector(tnl_requestOperation:didStartRetryFromResponse:policyProvider:)), NSStringFromSelector(@selector(tnl_requestOperation:didReceiveURLResponse:)), NSStringFromSelector(@selector(tnl_requestOperation:didCompleteAttemptWithResponse:disposition:)), NSStringFromSelector(@selector(tnl_requestOperation:didCompleteWithResponse:)) ]; NSArray<NSString *> *observedCallbacks = delegate.observedCallbacks; XCTAssertEqualObjects(expectedCallbacks, observedCallbacks); } @end @implementation TestJSONResponse - (void)prepare { [super prepare]; NSError *error = nil; if (!_operationError) { NSData *data = nil; if (_info.data) { data = _info.data; } else if (_info.temporarySavedFile) { _responseBodyWasInFile = YES; NSString *newFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:[NSUUID UUID].UUIDString]; if ([_info.temporarySavedFile moveToPath:newFilePath error:&error]) { data = [NSData dataWithContentsOfFile:newFilePath options:0 error:&error]; [[NSFileManager defaultManager] removeItemAtPath:newFilePath error:NULL]; } } @try { _result = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; } @catch (NSException *e) { _jsonParseError = [NSError errorWithDomain:NSPOSIXErrorDomain code:EINTR userInfo:nil]; } } } @end @implementation TestRetryOnceOn503Response - (BOOL)tnl_shouldRetryRequestOperation:(TNLRequestOperation *)op withResponse:(TNLResponse *)response { return op.retryCount <= 0 && response.info.statusCode == 503; } @end @implementation TestTNLRequestDelegate { dispatch_queue_t _slowQueue; dispatch_queue_t _fastQueue; NSMutableArray<NSString *> *_cmds; } - (instancetype)init { self = [super init]; if (self) { _fastQueue = dispatch_queue_create("test.queue.fast", DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(_fastQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); _slowQueue = dispatch_queue_create("test.queue.slow", DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(_slowQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)); _cmds = [[NSMutableArray alloc] init]; } return self; } - (dispatch_queue_t)tnl_delegateQueueForRequestOperation:(TNLRequestOperation *)op { return _slowQueue; } - (dispatch_queue_t)tnl_completionQueueForRequestOperation:(TNLRequestOperation *)op { return _fastQueue; } - (void)tnl_requestOperation:(TNLRequestOperation *)op didReceiveURLResponse:(NSURLResponse *)response { SEL cmd = _cmd; sleep(2); dispatch_sync(_fastQueue, ^{ [self trackSelector:cmd]; }); } - (void)tnl_requestOperation:(TNLRequestOperation *)op didCompleteAttemptWithResponse:(TNLResponse *)response disposition:(TNLAttemptCompleteDisposition)disposition { SEL cmd = _cmd; sleep(1); dispatch_sync(_fastQueue, ^{ [self trackSelector:cmd]; }); } - (void)tnl_requestOperation:(TNLRequestOperation *)op willStartRetryFromResponse:(TNLResponse *)responseBeforeRetry policyProvider:(id<TNLRequestRetryPolicyProvider>)policyProvider afterDelay:(NSTimeInterval)delay { SEL cmd = _cmd; dispatch_sync(_fastQueue, ^{ [self trackSelector:cmd]; }); } - (void)tnl_requestOperation:(TNLRequestOperation *)op didStartRetryFromResponse:(TNLResponse *)responseBeforeRetry policyProvider:(id<TNLRequestRetryPolicyProvider>)policyProvider { SEL cmd = _cmd; dispatch_sync(_fastQueue, ^{ [self trackSelector:cmd]; }); } - (void)tnl_requestOperation:(TNLRequestOperation *)op didCompleteWithResponse:(TNLResponse *)response { [self trackSelector:_cmd]; } - (void)trackSelector:(SEL)cmd { NSString *cmdString = NSStringFromSelector(cmd); [_cmds addObject:cmdString]; } - (NSArray<NSString *> *)observedCallbacks { __block NSArray<NSString *> *cmds; __block NSUInteger dummy; dispatch_sync(_slowQueue, ^{ dummy = (NSUInteger)self; }); dummy++; dispatch_sync(_fastQueue, ^{ cmds = [self->_cmds copy]; }); return cmds; } @end