| /* Copyright (c) 2009 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #import <XCTest/XCTest.h> |
| |
| #import "GTMHTTPFetcherTestServer.h" |
| #import "GTMHTTPFetcher.h" |
| #import "GTMHTTPFetchHistory.h" |
| #import "GTMHTTPFetcherLogging.h" |
| #import "GTMHTTPUploadFetcher.h" |
| |
| @interface GTMHTTPFetcherFetchingTest : XCTestCase { |
| |
| // these ivars are checked after fetches, and are reset by resetFetchResponse |
| NSData *fetchedData_; |
| NSError *fetcherError_; |
| int fetchedStatus_; |
| NSURLResponse *fetchedResponse_; |
| NSMutableURLRequest *fetchedRequest_; |
| int retryCounter_; |
| |
| int fetchStartedNotificationCount_; |
| int fetchStoppedNotificationCount_; |
| int retryDelayStartedNotificationCount_; |
| int retryDelayStoppedNotificationCount_; |
| |
| // setup/teardown ivars |
| GTMHTTPFetchHistory *fetchHistory_; |
| GTMHTTPFetcherTestServer *testServer_; |
| BOOL isServerRunning_; |
| } |
| |
| - (void)testFetcher:(GTMHTTPFetcher *)fetcher |
| finishedWithData:(NSData *)data |
| error:(NSError *)error; |
| |
| - (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString |
| cachingDatedData:(BOOL)doCaching; |
| |
| - (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString |
| cachingDatedData:(BOOL)doCaching |
| retrySelector:(SEL)retrySel |
| maxRetryInterval:(NSTimeInterval)maxRetryInterval |
| credential:(NSURLCredential *)credential |
| userData:(id)userData; |
| |
| - (NSString *)localURLStringToTestFileName:(NSString *)name; |
| - (NSString *)localPathForFileName:(NSString *)name; |
| @end |
| |
| // Authorization testing |
| @interface TestAuthorizer : NSObject <GTMFetcherAuthorizationProtocol> { |
| BOOL hasExpired_; |
| } |
| @property (assign) BOOL expired; |
| |
| + (TestAuthorizer *)authorizer; |
| + (TestAuthorizer *)expiredAuthorizer; |
| @end |
| |
| static NSString *const kGoodBearerValue = @"Bearer good"; |
| static NSString *const kExpiredBearerValue = @"Bearer expired"; |
| |
| @implementation GTMHTTPFetcherFetchingTest |
| |
| // The wrong-fetch test can take >10s to pass. |
| static const NSTimeInterval kGiveUpInterval = 30.0; |
| |
| // file available in Tests folder |
| static NSString *const kValidFileName = @"gettysburgaddress.txt"; |
| |
| - (NSString *)docRootPath { |
| NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; |
| XCTAssertNotNil(testBundle); |
| |
| NSString *docFolder = [testBundle resourcePath]; |
| return docFolder; |
| } |
| |
| - (void)setUp { |
| fetchHistory_ = [[GTMHTTPFetchHistory alloc] init]; |
| |
| NSString *docRoot = [self docRootPath]; |
| |
| testServer_ = [[GTMHTTPFetcherTestServer alloc] initWithDocRoot:docRoot]; |
| isServerRunning_ = (testServer_ != nil); |
| |
| XCTAssertTrue(isServerRunning_, |
| @">>> http test server failed to launch; skipping" |
| " fetcher tests\n"); |
| |
| // install observers for fetcher notifications |
| NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; |
| [nc addObserver:self selector:@selector(fetchStateChanged:) name:kGTMHTTPFetcherStartedNotification object:nil]; |
| [nc addObserver:self selector:@selector(fetchStateChanged:) name:kGTMHTTPFetcherStoppedNotification object:nil]; |
| [nc addObserver:self selector:@selector(retryDelayStateChanged:) name:kGTMHTTPFetcherRetryDelayStartedNotification object:nil]; |
| [nc addObserver:self selector:@selector(retryDelayStateChanged:) name:kGTMHTTPFetcherRetryDelayStoppedNotification object:nil]; |
| } |
| |
| - (void)resetFetchResponse { |
| [fetchedData_ release]; |
| fetchedData_ = nil; |
| |
| [fetcherError_ release]; |
| fetcherError_ = nil; |
| |
| [fetchedRequest_ release]; |
| fetchedRequest_ = nil; |
| |
| [fetchedResponse_ release]; |
| fetchedResponse_ = nil; |
| |
| fetchedStatus_ = 0; |
| |
| retryCounter_ = 0; |
| } |
| |
| - (void)tearDown { |
| [testServer_ release]; |
| testServer_ = nil; |
| |
| isServerRunning_ = NO; |
| |
| [self resetFetchResponse]; |
| |
| [fetchHistory_ release]; |
| fetchHistory_ = nil; |
| } |
| |
| - (NSData *)gettysburgAddress { |
| NSString *gettysburgPath = [testServer_ localPathForFile:kValidFileName]; |
| NSData *gettysburgAddress = [NSData dataWithContentsOfFile:gettysburgPath]; |
| return gettysburgAddress; |
| } |
| |
| #pragma mark Notification callbacks |
| |
| - (void)fetchStateChanged:(NSNotification *)note { |
| if ([[note name] isEqual:kGTMHTTPFetcherStartedNotification]) { |
| ++fetchStartedNotificationCount_; |
| } else { |
| ++fetchStoppedNotificationCount_; |
| } |
| |
| XCTAssertTrue(fetchStoppedNotificationCount_ <= fetchStartedNotificationCount_, |
| @"fetch notification imbalance: starts=%d stops=%d", |
| fetchStartedNotificationCount_, |
| fetchStoppedNotificationCount_); |
| } |
| |
| - (void)retryDelayStateChanged:(NSNotification *)note { |
| if ([[note name] isEqual:kGTMHTTPFetcherRetryDelayStartedNotification]) { |
| ++retryDelayStartedNotificationCount_; |
| } else { |
| ++retryDelayStoppedNotificationCount_; |
| } |
| |
| XCTAssertTrue(retryDelayStoppedNotificationCount_ <= retryDelayStartedNotificationCount_, |
| @"retry delay notification imbalance: starts=%d stops=%d", |
| retryDelayStartedNotificationCount_, |
| retryDelayStoppedNotificationCount_); |
| } |
| |
| - (void)resetNotificationCounts { |
| fetchStartedNotificationCount_ = 0; |
| fetchStoppedNotificationCount_ = 0; |
| retryDelayStartedNotificationCount_ = 0; |
| retryDelayStoppedNotificationCount_ = 0; |
| } |
| |
| #pragma mark Tests |
| |
| - (void)testFetch { |
| if (!isServerRunning_) return; |
| |
| [self resetNotificationCounts]; |
| [self resetFetchResponse]; |
| |
| NSString *urlString = [self localURLStringToTestFileName:kValidFileName]; |
| [self doFetchWithURLString:urlString cachingDatedData:YES]; |
| |
| XCTAssertNotNil(fetchedData_, |
| @"failed to fetch data, status:%d error:%@, URL:%@", |
| fetchedStatus_, fetcherError_, urlString); |
| |
| // we'll verify we fetched from the server the actual data on disk |
| NSData *gettysburgAddress = [self gettysburgAddress]; |
| XCTAssertEqualObjects(fetchedData_, gettysburgAddress, |
| @"Lincoln disappointed"); |
| |
| XCTAssertNotNil(fetchedResponse_, |
| @"failed to get fetch response, status:%d error:%@", |
| fetchedStatus_, fetcherError_); |
| XCTAssertNotNil(fetchedRequest_, |
| @"failed to get fetch request, URL %@", urlString); |
| XCTAssertNil(fetcherError_, @"fetching data gave error: %@", fetcherError_); |
| XCTAssertEqual(fetchedStatus_, 200, |
| @"unexpected status for URL %@", urlString); |
| |
| // no cookies should be sent with our first request |
| NSDictionary *headers = [fetchedRequest_ allHTTPHeaderFields]; |
| NSString *cookiesSent = [headers objectForKey:@"Cookie"]; |
| XCTAssertNil(cookiesSent, @"Cookies sent unexpectedly: %@", cookiesSent); |
| |
| // cookies should have been set by the response; specifically, TestCookie |
| // should be set to the name of the file requested |
| NSDictionary *responseHeaders; |
| |
| responseHeaders = [(NSHTTPURLResponse *)fetchedResponse_ allHeaderFields]; |
| NSString *cookiesSetString = [responseHeaders objectForKey:@"Set-Cookie"]; |
| NSString *cookieExpected = [NSString stringWithFormat:@"TestCookie=%@", |
| kValidFileName]; |
| XCTAssertEqualObjects(cookiesSetString, cookieExpected, @"Unexpected cookie"); |
| |
| // make a copy of the fetched data to compare with our next fetch from the |
| // cache |
| NSData *originalFetchedData = [[fetchedData_ copy] autorelease]; |
| |
| |
| // Now fetch again so the "If-None-Match" header will be set (because |
| // we're calling setFetchHistory: below) and caching ON, and verify that we |
| // got a good data from the cache and a nil error, along with a |
| // "Not Modified" status in the fetcher |
| |
| [self resetFetchResponse]; |
| |
| [self doFetchWithURLString:urlString cachingDatedData:YES]; |
| |
| XCTAssertEqualObjects(fetchedData_, originalFetchedData, |
| @"cache data mismatch"); |
| |
| XCTAssertNotNil(fetchedData_, |
| @"failed to fetch data, status:%d error:%@, URL:%@", |
| fetchedStatus_, fetcherError_, urlString); |
| XCTAssertNotNil(fetchedResponse_, |
| @"failed to get fetch response, status:%d error:%@", |
| fetchedStatus_, fetcherError_); |
| XCTAssertNotNil(fetchedRequest_, |
| @"failed to get fetch request, URL %@", |
| urlString); |
| XCTAssertNil(fetcherError_, @"fetching data gave error: %@", fetcherError_); |
| |
| XCTAssertEqual(fetchedStatus_, kGTMHTTPFetcherStatusNotModified, // 304 |
| @"fetch status unexpected for URL %@", urlString); |
| |
| // the TestCookie set previously should be sent with this request |
| cookiesSent = [[fetchedRequest_ allHTTPHeaderFields] objectForKey:@"Cookie"]; |
| XCTAssertEqualObjects(cookiesSent, cookieExpected, @"Cookie not sent"); |
| |
| |
| // Now fetch twice without caching enabled, and verify that we got a |
| // "Precondition failed" status, along with a non-nil but empty NSData (which |
| // is normal for that status code) from the second fetch |
| |
| [self resetFetchResponse]; |
| |
| [fetchHistory_ clearHistory]; |
| |
| [self doFetchWithURLString:urlString cachingDatedData:NO]; |
| |
| XCTAssertEqualObjects(fetchedData_, originalFetchedData, |
| @"cache data mismatch"); |
| XCTAssertNil(fetcherError_, @"unexpected error: %@", fetcherError_); |
| |
| [self resetFetchResponse]; |
| [self doFetchWithURLString:urlString cachingDatedData:NO]; |
| |
| XCTAssertNotNil(fetchedData_, @""); |
| XCTAssertEqual([fetchedData_ length], (NSUInteger) 0, @"unexpected data"); |
| XCTAssertEqual(fetchedStatus_, kGTMHTTPFetcherStatusNotModified, |
| @"fetching data expected status 304, instead got %d", fetchedStatus_); |
| XCTAssertNotNil(fetcherError_, @"missing 304 error"); |
| |
| // check the notifications |
| XCTAssertEqual(fetchStartedNotificationCount_, 4, @"fetches started"); |
| XCTAssertEqual(fetchStoppedNotificationCount_, 4, @"fetches stopped"); |
| XCTAssertEqual(retryDelayStartedNotificationCount_, 0, @"retries started"); |
| XCTAssertEqual(retryDelayStoppedNotificationCount_, 0, @"retries started"); |
| } |
| |
| - (void)testFailToBeginFetch { |
| if (!isServerRunning_) return; |
| [self resetNotificationCounts]; |
| [self resetFetchResponse]; |
| |
| // |
| // Test with delegate/selector callback. |
| // |
| NSString *urlString = [self localURLStringToTestFileName:kValidFileName]; |
| NSURL *url = [NSURL URLWithString:urlString]; |
| NSURLRequest *req = [NSURLRequest requestWithURL:url |
| cachePolicy:NSURLRequestReloadIgnoringCacheData |
| timeoutInterval:kGiveUpInterval]; |
| GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:req]; |
| XCTAssertNotNil(fetcher); |
| |
| fetcher.allowLocalhostRequest = YES; |
| fetcher.delegateQueue = [NSOperationQueue mainQueue]; |
| [fetcher setProperty:@YES forKey:@"_CannotBeginFetch"]; |
| |
| BOOL isFetching = [fetcher beginFetchWithDelegate:self |
| didFinishSelector:@selector(testFetcher:finishedWithData:error:)]; |
| XCTAssertFalse(isFetching); |
| |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| |
| XCTAssertNil(fetchedData_); |
| XCTAssertNotNil(fetcherError_); |
| XCTAssertEqual([fetcherError_ code], -1); |
| |
| XCTAssertEqual(fetchStartedNotificationCount_, 0, @"fetches started"); |
| XCTAssertEqual(fetchStoppedNotificationCount_, 0, @"fetches stopped"); |
| XCTAssertEqual(retryDelayStartedNotificationCount_, 0, @"retries started"); |
| XCTAssertEqual(retryDelayStoppedNotificationCount_, 0, @"retries started"); |
| |
| [self resetNotificationCounts]; |
| [self resetFetchResponse]; |
| |
| // |
| // Test with block callback. |
| // |
| fetcher = [GTMHTTPFetcher fetcherWithRequest:req]; |
| XCTAssertNotNil(fetcher); |
| |
| fetcher.allowLocalhostRequest = YES; |
| fetcher.delegateQueue = [NSOperationQueue mainQueue]; |
| [fetcher setProperty:@YES forKey:@"_CannotBeginFetch"]; |
| |
| __block BOOL hasFinishedFetching = NO; |
| isFetching = [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { |
| XCTAssertNil(data); |
| XCTAssertEqual([error code], -1); |
| hasFinishedFetching = YES; |
| }]; |
| XCTAssertFalse(isFetching); |
| |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| |
| XCTAssertTrue(hasFinishedFetching); |
| |
| XCTAssertEqual(fetchStartedNotificationCount_, 0, @"fetches started"); |
| XCTAssertEqual(fetchStoppedNotificationCount_, 0, @"fetches stopped"); |
| XCTAssertEqual(retryDelayStartedNotificationCount_, 0, @"retries started"); |
| XCTAssertEqual(retryDelayStoppedNotificationCount_, 0, @"retries started"); |
| } |
| |
| - (void)testAuthorizorFetch { |
| if (!isServerRunning_) return; |
| [self resetNotificationCounts]; |
| |
| // |
| // fetch a live, authorized URL |
| // |
| NSString *authName = [kValidFileName stringByAppendingFormat:@"?oauth2=good"]; |
| NSString *authedURL = [self localURLStringToTestFileName:authName]; |
| |
| NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:authedURL] |
| cachePolicy:NSURLRequestReloadIgnoringCacheData |
| timeoutInterval:kGiveUpInterval]; |
| |
| __block BOOL hasFinishedFetching = NO; |
| __block GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:req]; |
| XCTAssertNotNil(fetcher, @"Failed to allocate fetcher"); |
| |
| fetcher.allowLocalhostRequest = YES; |
| fetcher.authorizer = [TestAuthorizer authorizer]; |
| |
| void (^completionBlock)(NSData *, NSError *) = ^(NSData *data, NSError *error) { |
| XCTAssertEqualObjects(data, [self gettysburgAddress], @"wrong data"); |
| XCTAssertNil(error, @"unexpected status error"); |
| hasFinishedFetching = YES; |
| }; |
| |
| [fetcher beginFetchWithCompletionHandler:completionBlock]; |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| XCTAssertTrue(hasFinishedFetching, @"auth 1 fetch failed"); |
| |
| // |
| // fetch with an expired authorizer, no retry allowed |
| // |
| authName = [kValidFileName stringByAppendingFormat:@"?oauth2=good"]; |
| authedURL = [self localURLStringToTestFileName:authName]; |
| |
| req = [NSURLRequest requestWithURL:[NSURL URLWithString:authedURL] |
| cachePolicy:NSURLRequestReloadIgnoringCacheData |
| timeoutInterval:kGiveUpInterval]; |
| |
| hasFinishedFetching = NO; |
| fetcher = [GTMHTTPFetcher fetcherWithRequest:req]; |
| fetcher.retryBlock = ^(BOOL suggestedWillRetry, NSError *error) { |
| return NO; |
| }; |
| XCTAssertNotNil(fetcher, @"Failed to allocate fetcher"); |
| |
| fetcher.allowLocalhostRequest = YES; |
| fetcher.authorizer = [TestAuthorizer expiredAuthorizer]; |
| |
| completionBlock = ^(NSData *data, NSError *error) { |
| XCTAssertEqual([error code], (NSInteger) 401, |
| @"unexpected status, error=%@", error); |
| hasFinishedFetching = YES; |
| }; |
| |
| [fetcher beginFetchWithCompletionHandler:completionBlock]; |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| XCTAssertTrue(hasFinishedFetching, @"auth 2 fetch failed"); |
| |
| // |
| // fetch with an expired authorizer, with automatic refresh |
| // |
| authName = [kValidFileName stringByAppendingFormat:@"?oauth2=good"]; |
| authedURL = [self localURLStringToTestFileName:authName]; |
| |
| req = [NSURLRequest requestWithURL:[NSURL URLWithString:authedURL] |
| cachePolicy:NSURLRequestReloadIgnoringCacheData |
| timeoutInterval:kGiveUpInterval]; |
| |
| hasFinishedFetching = NO; |
| fetcher = [GTMHTTPFetcher fetcherWithRequest:req]; |
| XCTAssertNotNil(fetcher, @"Failed to allocate fetcher"); |
| |
| fetcher.allowLocalhostRequest = YES; |
| fetcher.authorizer = [TestAuthorizer expiredAuthorizer]; |
| |
| completionBlock = ^(NSData *data, NSError *error) { |
| XCTAssertEqualObjects(data, [self gettysburgAddress], @"wrong data"); |
| XCTAssertNil(error, @"unexpected error"); |
| hasFinishedFetching = YES; |
| }; |
| |
| [fetcher beginFetchWithCompletionHandler:completionBlock]; |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| XCTAssertTrue(hasFinishedFetching, @"auth 3 fetch failed"); |
| } |
| |
| - (void)testWrongFetch { |
| |
| if (!isServerRunning_) return; |
| [self resetNotificationCounts]; |
| |
| // fetch a live, invalid URL |
| NSString *badURLString = @"http://localhost:86/"; |
| [self doFetchWithURLString:badURLString cachingDatedData:NO]; |
| |
| if (fetchedData_) { |
| NSString *str = [[[NSString alloc] initWithData:fetchedData_ |
| encoding:NSUTF8StringEncoding] autorelease]; |
| XCTAssertNil(fetchedData_, @"fetched unexpected data: %@", str); |
| } |
| |
| XCTAssertNotNil(fetcherError_, @"failed to receive fetching error"); |
| XCTAssertEqual(fetchedStatus_, 0, |
| @"unexpected status from no response"); |
| |
| // fetch with a specific status code from our http server |
| [self resetFetchResponse]; |
| |
| NSString *invalidWebPageFile = [kValidFileName stringByAppendingString:@"?status=400"]; |
| NSString *statusUrlString = [self localURLStringToTestFileName:invalidWebPageFile]; |
| |
| [self doFetchWithURLString:statusUrlString cachingDatedData:NO]; |
| |
| XCTAssertNotNil(fetchedData_, @"fetch lacked data with error info"); |
| XCTAssertNotNil(fetcherError_, @"expected status error"); |
| NSData *statusData = [[fetcherError_ userInfo] objectForKey:kGTMHTTPFetcherStatusDataKey]; |
| NSString *dataStr = [[[NSString alloc] initWithData:statusData |
| encoding:NSUTF8StringEncoding] autorelease]; |
| NSString *expectedStr = @"{ \"error\" : { \"message\" : \"Server Status 400\", \"code\" : 400 } }"; |
| XCTAssertEqualObjects(dataStr, expectedStr, @"expected status data"); |
| |
| XCTAssertEqual(fetchedStatus_, 400, |
| @"unexpected status, error=%@", fetcherError_); |
| |
| // check the notifications |
| XCTAssertEqual(fetchStartedNotificationCount_, 2, @"fetches started"); |
| XCTAssertEqual(fetchStoppedNotificationCount_, 2, @"fetches stopped"); |
| XCTAssertEqual(retryDelayStartedNotificationCount_, 0, @"retries started"); |
| XCTAssertEqual(retryDelayStoppedNotificationCount_, 0, @"retries started"); |
| } |
| |
| - (void)testFetchToFile { |
| if (!isServerRunning_) return; |
| |
| // create an empty file from which we can make an NSFileHandle |
| NSString *path = [NSTemporaryDirectory() stringByAppendingFormat:@"fhTest_%@", |
| [NSDate date]]; |
| [[NSData data] writeToFile:path atomically:YES]; |
| |
| NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:path]; |
| XCTAssertNotNil(fileHandle, @"missing filehandle for %@", path); |
| |
| // make the http request to our test server |
| __block NSString *testName = @"Download to file handle"; |
| |
| NSString *urlString = [self localURLStringToTestFileName:kValidFileName]; |
| NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString] |
| cachePolicy:NSURLRequestReloadIgnoringCacheData |
| timeoutInterval:kGiveUpInterval]; |
| |
| // we'll put fetcher in a __block variable so we can refer to the |
| // latest instance of it in the callbacks |
| __block GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:req]; |
| XCTAssertNotNil(fetcher, @"Failed to allocate fetcher"); |
| |
| // received-data block |
| // |
| // the final received-data block invocation should show the length of the |
| // file actually downloaded |
| __block NSUInteger receivedDataLen = 0; |
| |
| void (^receivedBlock)(NSData *) = ^(NSData *dataReceivedSoFar){ |
| // a nil data argument is expected when the downloaded data is written |
| // to a file handle |
| XCTAssertNil(dataReceivedSoFar, @"%@: unexpected dataReceivedSoFar", |
| testName); |
| |
| receivedDataLen = [fetcher downloadedLength]; |
| }; |
| |
| fetcher.allowLocalhostRequest = YES; |
| |
| // fetch & completion block |
| __block BOOL hasFinishedFetching = NO; |
| |
| void (^completionBlock)(NSData *, NSError *) = ^(NSData *data, NSError *error) { |
| XCTAssertNil(data, @"%@: unexpected data", testName); |
| XCTAssertNil(error, @"%@: unexpected error: %@", testName, error); |
| |
| NSString *fetchedContents = [NSString stringWithContentsOfFile:path |
| encoding:NSUTF8StringEncoding |
| error:NULL]; |
| XCTAssertEqual(receivedDataLen, [fetchedContents length], |
| @"%@: length issue", testName); |
| |
| NSString *origPath = [self localPathForFileName:kValidFileName]; |
| NSString *origContents = [NSString stringWithContentsOfFile:origPath |
| encoding:NSUTF8StringEncoding |
| error:NULL]; |
| XCTAssertEqualObjects(fetchedContents, origContents, |
| @"%@: fetch to FH error", testName); |
| |
| hasFinishedFetching = YES; |
| }; |
| |
| [fetcher setDownloadFileHandle:fileHandle]; |
| [fetcher setReceivedDataBlock:receivedBlock]; |
| [fetcher beginFetchWithCompletionHandler:completionBlock]; |
| |
| // spin until the fetch completes |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| XCTAssertTrue(hasFinishedFetching, @"file handle fetch timed out"); |
| |
| [[NSFileManager defaultManager] removeItemAtPath:path |
| error:NULL]; |
| |
| // |
| // repeat the test with a new fetcher, writing directly to the path |
| // instead of explicitly creating a file handle |
| // |
| hasFinishedFetching = NO; |
| |
| testName = @"Download to file path"; |
| fetcher = [GTMHTTPFetcher fetcherWithRequest:req]; |
| [fetcher setDownloadPath:path]; |
| [fetcher setReceivedDataBlock:receivedBlock]; |
| [fetcher setAllowLocalhostRequest:YES]; |
| |
| [fetcher beginFetchWithCompletionHandler:completionBlock]; |
| |
| // grab a copy of the temporary file path |
| NSString *tempPath = [[[fetcher performSelector:@selector(temporaryDownloadPath)] copy] autorelease]; |
| |
| // spin until the fetch completes |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| XCTAssertTrue(hasFinishedFetching, @"path fetch timed out"); |
| |
| // verify that the temp file has been deleted |
| BOOL doesExist = [[NSFileManager defaultManager] fileExistsAtPath:tempPath]; |
| XCTAssertFalse(doesExist, @"%@: temp file should not exist", testName); |
| |
| [[NSFileManager defaultManager] removeItemAtPath:path |
| error:NULL]; |
| |
| // |
| // repeat the test with a new fetcher, writing directly to a path, |
| // but with a fetch that will fail |
| // |
| hasFinishedFetching = NO; |
| |
| testName = @"Invalid download to file path"; |
| NSString *invalidFile = [kValidFileName stringByAppendingString:@"?status=400"]; |
| urlString = [self localURLStringToTestFileName:invalidFile]; |
| req = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString] |
| cachePolicy:NSURLRequestReloadIgnoringCacheData |
| timeoutInterval:kGiveUpInterval]; |
| fetcher = [GTMHTTPFetcher fetcherWithRequest:req]; |
| [fetcher setDownloadPath:path]; |
| [fetcher setReceivedDataBlock:receivedBlock]; |
| [fetcher setAllowLocalhostRequest:YES]; |
| |
| [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { |
| XCTAssertNil(data, @"%@: unexpected data", testName); |
| XCTAssertEqual([error code], (NSInteger) 400, |
| @"%@: unexpected error: %@", testName, error); |
| hasFinishedFetching = YES; |
| }]; |
| |
| // grab a copy of the temporary file path |
| tempPath = [[[fetcher performSelector:@selector(temporaryDownloadPath)] copy] autorelease]; |
| |
| // spin until the fetch completes |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| XCTAssertTrue(hasFinishedFetching, @"path fetch timed out"); |
| |
| // the file at the temporary path should be gone, and none should be at |
| // the final path |
| // |
| // we test it here rather than in the callback since it's not deleted |
| // until the fetcher does its stopFetching cleanup |
| doesExist = [[NSFileManager defaultManager] fileExistsAtPath:tempPath]; |
| XCTAssertFalse(doesExist, @"%@: temp file should not exist", testName); |
| |
| doesExist = [[NSFileManager defaultManager] fileExistsAtPath:path]; |
| XCTAssertFalse(doesExist, @"%@: file should not exist", testName); |
| } |
| |
| - (void)testRetryFetches { |
| |
| if (!isServerRunning_) return; |
| [self resetNotificationCounts]; |
| |
| GTMHTTPFetcher *fetcher; |
| |
| NSString *invalidFile = [kValidFileName stringByAppendingString:@"?status=503"]; |
| NSString *invalidFileURLString = [self localURLStringToTestFileName:invalidFile]; |
| |
| SEL countRetriesSel = @selector(countRetriesFetcher:willRetry:forError:); |
| SEL fixRequestSel = @selector(fixRequestFetcher:willRetry:forError:); |
| |
| // |
| // test: retry until timeout, then expect failure with status message |
| // |
| |
| fetcher = [self doFetchWithURLString:invalidFileURLString |
| cachingDatedData:NO |
| retrySelector:countRetriesSel |
| maxRetryInterval:5.0 // retry intervals of 1, 2, 4 |
| credential:nil |
| userData:@1000]; |
| |
| XCTAssertNotNil(fetchedData_, @"error data is expected"); |
| XCTAssertEqual(fetchedStatus_, 503, |
| @"fetchedStatus_ should be 503, was %d", fetchedStatus_); |
| XCTAssertEqual([fetcher retryCount], (NSUInteger) 3, @"retry count unexpected"); |
| |
| // |
| // test: retry with server sleep to force timeout, then expect failure with status 408 |
| // after first retry |
| // |
| [self resetFetchResponse]; |
| |
| NSString *timeoutFile = [kValidFileName stringByAppendingString:@"?sleep=10"]; |
| NSString *timeoutFileURLString = [self localURLStringToTestFileName:timeoutFile]; |
| |
| fetcher = [self doFetchWithURLString:timeoutFileURLString |
| cachingDatedData:NO |
| retrySelector:countRetriesSel |
| maxRetryInterval:5.0 // retry interval of 1, then exceed 3*max timout |
| credential:nil |
| userData:@1000]; |
| |
| XCTAssertNotNil(fetchedData_, @"error data is expected"); |
| XCTAssertEqual(fetchedStatus_, 408, |
| @"fetchedStatus_ should be 408, was %d", fetchedStatus_); |
| XCTAssertEqual([fetcher retryCount], (NSUInteger)1, @"retry count unexpected"); |
| |
| // |
| // test: retry twice, then give up |
| // |
| [self resetFetchResponse]; |
| |
| fetcher = [self doFetchWithURLString:invalidFileURLString |
| cachingDatedData:NO |
| retrySelector:countRetriesSel |
| maxRetryInterval:10.0 // retry intervals of 1, 2, 4, 8 |
| credential:nil |
| userData:@2]; |
| |
| XCTAssertNotNil(fetchedData_, @"error data is expected"); |
| XCTAssertEqual(fetchedStatus_, 503, |
| @"fetchedStatus_ should be 503, was %d", fetchedStatus_); |
| XCTAssertEqual([fetcher retryCount], (NSUInteger) 2, @"retry count unexpected"); |
| |
| |
| // |
| // test: retry, making the request succeed on the first retry |
| // by fixing the URL |
| // |
| [self resetFetchResponse]; |
| |
| fetcher = [self doFetchWithURLString:invalidFileURLString |
| cachingDatedData:NO |
| retrySelector:fixRequestSel |
| maxRetryInterval:30.0 // should only retry once due to selector |
| credential:nil |
| userData:@1000]; |
| |
| XCTAssertNotNil(fetchedData_, @"data is expected"); |
| XCTAssertEqual(fetchedStatus_, 200, |
| @"fetchedStatus_ should be 200, was %d", fetchedStatus_); |
| XCTAssertEqual([fetcher retryCount], (NSUInteger) 1, @"retry count unexpected"); |
| |
| // check the notifications |
| XCTAssertEqual(fetchStartedNotificationCount_, 11, @"fetches started"); |
| XCTAssertEqual(fetchStoppedNotificationCount_, 11, @"fetches stopped"); |
| XCTAssertEqual(retryDelayStartedNotificationCount_, 7, @"retries started"); |
| XCTAssertEqual(retryDelayStoppedNotificationCount_, 7, @"retries started"); |
| } |
| |
| #pragma mark Upload fetches |
| |
| - (NSData *)generatedUploadDataWithLength:(NSUInteger)length { |
| // fill a data block with data |
| NSMutableData *data = [NSMutableData dataWithLength:length]; |
| |
| unsigned char *bytes = [data mutableBytes]; |
| for (NSUInteger idx = 0; idx < length; idx++) { |
| bytes[idx] = ((idx + 1) % 256); |
| } |
| |
| return data; |
| } |
| |
| static NSString* const kPauseAtKey = @"pauseAt"; |
| static NSString* const kRetryAtKey = @"retryAt"; |
| static NSString* const kOriginalURLKey = @"origURL"; |
| |
| - (void)uploadFetcher:(GTMHTTPUploadFetcher *)fetcher |
| didSendBytes:(NSInteger)bytesSent |
| totalBytesSent:(NSInteger)totalBytesSent |
| totalBytesExpectedToSend:(NSInteger)totalBytesExpectedToSend { |
| |
| NSNumber *pauseAtNum = [fetcher propertyForKey:kPauseAtKey]; |
| if (pauseAtNum) { |
| int pauseAt = [pauseAtNum intValue]; |
| if (pauseAt < totalBytesSent) { |
| // we won't be paused again |
| [fetcher setProperty:nil forKey:kPauseAtKey]; |
| |
| // we've reached the point where we should pause |
| // |
| // use perform selector to avoid pausing immediately, as that would nuke |
| // the chunk upload fetcher that is calling us back now |
| [fetcher performSelector:@selector(pauseFetching) |
| withObject:nil |
| afterDelay:0.0]; |
| |
| [fetcher performSelector:@selector(resumeFetching) |
| withObject:nil |
| afterDelay:1.0]; |
| } |
| } |
| |
| NSNumber *retryAtNum = [fetcher propertyForKey:kRetryAtKey]; |
| if (retryAtNum) { |
| int retryAt = [retryAtNum intValue]; |
| if (retryAt < totalBytesSent) { |
| // we won't be retrying again |
| [fetcher setProperty:nil forKey:kRetryAtKey]; |
| |
| // save the current locationURL before appending &status=503 |
| NSURL *origURL = fetcher.locationURL; |
| [fetcher setProperty:origURL forKey:kOriginalURLKey]; |
| |
| NSString *newURLStr = [[origURL absoluteString] stringByAppendingString:@"?status=503"]; |
| fetcher.locationURL = [NSURL URLWithString:newURLStr]; |
| } |
| } |
| } |
| |
| |
| -(BOOL)uploadRetryFetcher:(GTMHTTPUploadFetcher *)fetcher willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error { |
| // change this fetch's request (and future requests) to have the original URL, |
| // not the one with status=503 appended |
| NSURL *origURL = [fetcher propertyForKey:kOriginalURLKey]; |
| |
| [fetcher.activeFetcher.mutableRequest setURL:origURL]; |
| fetcher.locationURL = origURL; |
| |
| [fetcher setProperty:nil forKey:kOriginalURLKey]; |
| |
| return suggestedWillRetry; // do the retry fetch; it should succeed now |
| } |
| |
| - (void)testChunkedUploadFetch { |
| if (!isServerRunning_) return; |
| |
| NSData *bigData = [self generatedUploadDataWithLength:199000]; |
| NSData *smallData = [self generatedUploadDataWithLength:13]; |
| |
| NSData *gettysburgAddress = [self gettysburgAddress]; |
| |
| // write the big data into a temp file |
| NSString *tempDir = NSTemporaryDirectory(); |
| NSString *bigFileName = @"GTMFetchingTest_BigFile"; |
| NSString *bigFilePath = [tempDir stringByAppendingPathComponent:bigFileName]; |
| [bigData writeToFile:bigFilePath atomically:NO]; |
| |
| NSFileHandle *bigFileHandle = [NSFileHandle fileHandleForReadingAtPath:bigFilePath]; |
| |
| SEL progressSel = @selector(uploadFetcher:didSendBytes:totalBytesSent:totalBytesExpectedToSend:); |
| SEL retrySel = @selector(uploadRetryFetcher:willRetry:forError:); |
| SEL finishedSel = @selector(testFetcher:finishedWithData:error:); |
| |
| NSString *urlString = [self localURLStringToTestFileName:kValidFileName]; |
| urlString = [urlString stringByAppendingPathExtension:@"location"]; |
| |
| [self resetNotificationCounts]; |
| |
| // |
| // test uploading a big file handle |
| // |
| [self resetFetchResponse]; |
| |
| NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString] |
| cachePolicy:NSURLRequestReloadIgnoringCacheData |
| timeoutInterval:kGiveUpInterval]; |
| |
| GTMHTTPUploadFetcher *fetcher = [GTMHTTPUploadFetcher uploadFetcherWithRequest:request |
| uploadFileHandle:bigFileHandle |
| uploadMIMEType:@"text/plain" |
| chunkSize:75000 |
| fetcherService:nil]; |
| [fetcher setAllowLocalhostRequest:YES]; |
| |
| [fetcher beginFetchWithDelegate:self |
| didFinishSelector:finishedSel]; |
| |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| |
| |
| XCTAssertNotNil(fetchedData_, |
| @"failed to fetch data, status:%d error:%@, URL:%@", |
| fetchedStatus_, fetcherError_, urlString); |
| |
| // check that we fetched the expected data |
| XCTAssertEqualObjects(fetchedData_, gettysburgAddress, |
| @"Lincoln disappointed"); |
| XCTAssertNotNil(fetchedResponse_, |
| @"failed to get fetch response, status:%d error:%@", |
| fetchedStatus_, fetcherError_); |
| XCTAssertNotNil(fetchedRequest_, |
| @"failed to get fetch request, URL %@", urlString); |
| XCTAssertNil(fetcherError_, @"fetching data gave error: %@", fetcherError_); |
| XCTAssertEqual(fetchedStatus_, 200, |
| @"unexpected status for URL %@", urlString); |
| |
| // check the request of the final chunk fetcher to be sure we were uploading |
| // chunks as expected. Chunk requests replace the original request in the |
| // fetcher. |
| NSDictionary *reqHdrs = [fetcher.mutableRequest allHTTPHeaderFields]; |
| |
| NSString *uploadReqURLPath = @"gettysburgaddress.txt.location"; |
| NSString *contentLength = [reqHdrs objectForKey:@"Content-Length"]; |
| NSString *contentRange = [reqHdrs objectForKey:@"Content-Range"]; |
| |
| XCTAssertTrue([[[request URL] absoluteString] hasSuffix:uploadReqURLPath], |
| @"upload request wrong"); |
| XCTAssertEqualObjects(contentLength, @"49000", @"content length"); |
| XCTAssertEqualObjects(contentRange, @"bytes 150000-198999/199000", @"range"); |
| |
| // |
| // repeat the big upload using NSData |
| // |
| [self resetFetchResponse]; |
| |
| request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString] |
| cachePolicy:NSURLRequestReloadIgnoringCacheData |
| timeoutInterval:kGiveUpInterval]; |
| |
| fetcher = [GTMHTTPUploadFetcher uploadFetcherWithRequest:request |
| uploadData:bigData |
| uploadMIMEType:@"text/plain" |
| chunkSize:75000 |
| fetcherService:nil]; |
| [fetcher setAllowLocalhostRequest:YES]; |
| |
| [fetcher beginFetchWithDelegate:self |
| didFinishSelector:finishedSel]; |
| |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| |
| // check that we fetched the expected data |
| XCTAssertEqualObjects(fetchedData_, gettysburgAddress, |
| @"Lincoln disappointed"); |
| XCTAssertNotNil(fetchedResponse_, |
| @"failed to get fetch response, status:%d error:%@", |
| fetchedStatus_, fetcherError_); |
| XCTAssertNotNil(fetchedRequest_, |
| @"failed to get fetch request, URL %@", urlString); |
| XCTAssertNil(fetcherError_, @"fetching data gave error: %@", fetcherError_); |
| XCTAssertEqual(fetchedStatus_, 200, |
| @"unexpected status for URL %@", urlString); |
| |
| // check the request of the final chunk fetcher |
| reqHdrs = [fetcher.mutableRequest allHTTPHeaderFields]; |
| |
| uploadReqURLPath = @"gettysburgaddress.txt.location"; |
| contentLength = [reqHdrs objectForKey:@"Content-Length"]; |
| contentRange = [reqHdrs objectForKey:@"Content-Range"]; |
| |
| XCTAssertTrue([[[request URL] absoluteString] hasSuffix:uploadReqURLPath], |
| @"upload request wrong"); |
| XCTAssertEqualObjects(contentLength, @"49000", @"content length"); |
| XCTAssertEqualObjects(contentRange, @"bytes 150000-198999/199000", @"range"); |
| |
| |
| // |
| // repeat the big upload, pausing after 20000 bytes |
| // |
| [self resetFetchResponse]; |
| |
| fetcher = [GTMHTTPUploadFetcher uploadFetcherWithRequest:request |
| uploadData:bigData |
| uploadMIMEType:@"text/plain" |
| chunkSize:75000 |
| fetcherService:nil]; |
| [fetcher setAllowLocalhostRequest:YES]; |
| |
| // add a property to the fetcher that our progress callback will look for to |
| // know when to pause and resume the upload |
| fetcher.sentDataSelector = progressSel; |
| [fetcher setProperty:@20000 |
| forKey:kPauseAtKey]; |
| |
| [fetcher beginFetchWithDelegate:self |
| didFinishSelector:finishedSel]; |
| |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| |
| // check the request of the final chunk fetcher to be sure we were uploading |
| // chunks as expected. |
| reqHdrs = [fetcher.mutableRequest allHTTPHeaderFields]; |
| |
| uploadReqURLPath = @"gettysburgaddress.txt.location"; |
| contentLength = [reqHdrs objectForKey:@"Content-Length"]; |
| contentRange = [reqHdrs objectForKey:@"Content-Range"]; |
| |
| XCTAssertTrue([[[request URL] absoluteString] hasSuffix:uploadReqURLPath], |
| @"upload request wrong"); |
| XCTAssertEqualObjects(contentLength, @"24499", @"content length"); |
| XCTAssertEqualObjects(contentRange, @"bytes 174501-198999/199000", @"range"); |
| |
| |
| // |
| // repeat the big upload using blocks instead of a delegate, |
| // pausing after 20000 bytes |
| // |
| // for the blocks test, the body of the blocks will just invoke the non-block |
| // callback methods |
| // |
| [self resetFetchResponse]; |
| |
| fetcher = [GTMHTTPUploadFetcher uploadFetcherWithRequest:request |
| uploadData:bigData |
| uploadMIMEType:@"text/plain" |
| chunkSize:75000 |
| fetcherService:nil]; |
| [fetcher setAllowLocalhostRequest:YES]; |
| |
| [fetcher setSentDataBlock:^(NSInteger bytesSent, NSInteger totalBytesSent, NSInteger expectedBytes) { |
| [self uploadFetcher:fetcher |
| didSendBytes:bytesSent |
| totalBytesSent:totalBytesSent |
| totalBytesExpectedToSend:expectedBytes]; |
| }]; |
| |
| [fetcher setProperty:@20000 |
| forKey:kPauseAtKey]; |
| |
| [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { |
| [self testFetcher:fetcher |
| finishedWithData:data |
| error:error]; |
| }]; |
| |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| |
| // check the request of the final chunk fetcher to be sure we were uploading |
| // chunks as expected. |
| reqHdrs = [fetcher.mutableRequest allHTTPHeaderFields]; |
| |
| uploadReqURLPath = @"gettysburgaddress.txt.location"; |
| contentLength = [reqHdrs objectForKey:@"Content-Length"]; |
| contentRange = [reqHdrs objectForKey:@"Content-Range"]; |
| |
| XCTAssertTrue([[[request URL] absoluteString] hasSuffix:uploadReqURLPath], |
| @"upload request wrong"); |
| XCTAssertEqualObjects(contentLength, @"24499", @"content length"); |
| XCTAssertEqualObjects(contentRange, @"bytes 174501-198999/199000", @"range"); |
| |
| |
| // |
| // repeat the upload, and after sending 70000 bytes the progress |
| // callback will change the request URL for the next chunk fetch to make |
| // it fail with a retryable status error |
| // |
| |
| [self resetFetchResponse]; |
| |
| fetcher = [GTMHTTPUploadFetcher uploadFetcherWithRequest:request |
| uploadData:bigData |
| uploadMIMEType:@"text/plain" |
| chunkSize:75000 |
| fetcherService:nil]; |
| fetcher.retryEnabled = YES; |
| fetcher.retrySelector = retrySel; |
| fetcher.sentDataSelector = progressSel; |
| fetcher.allowLocalhostRequest = YES; |
| |
| // add a property to the fetcher that our progress callback will look for to |
| // know when to retry the upload |
| [fetcher setProperty:@70000 |
| forKey:kRetryAtKey]; |
| |
| [fetcher beginFetchWithDelegate:self |
| didFinishSelector:finishedSel]; |
| |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| |
| // check the request of the final chunk fetcher to be sure we were uploading |
| // chunks as expected. |
| reqHdrs = [fetcher.mutableRequest allHTTPHeaderFields]; |
| |
| uploadReqURLPath = @"gettysburgaddress.txt.location"; |
| contentLength = [reqHdrs objectForKey:@"Content-Length"]; |
| contentRange = [reqHdrs objectForKey:@"Content-Range"]; |
| |
| XCTAssertTrue([[[request URL] absoluteString] hasSuffix:uploadReqURLPath], |
| @"upload request wrong"); |
| XCTAssertEqualObjects(contentLength, @"24499", @"content length"); |
| XCTAssertEqualObjects(contentRange, @"bytes 174501-198999/199000", @"range"); |
| |
| |
| // |
| // repeat the forced-retry upload, using blocks |
| // |
| |
| [self resetFetchResponse]; |
| fetcher = [GTMHTTPUploadFetcher uploadFetcherWithRequest:request |
| uploadData:bigData |
| uploadMIMEType:@"text/plain" |
| chunkSize:75000 |
| fetcherService:nil]; |
| fetcher.allowLocalhostRequest = YES; |
| fetcher.retryEnabled = YES; |
| [fetcher setRetryBlock:^(BOOL suggestedWillRetry, NSError *error) { |
| BOOL shouldRetry = [self uploadRetryFetcher:fetcher |
| willRetry:suggestedWillRetry |
| forError:error]; |
| return shouldRetry; |
| }]; |
| |
| [fetcher setSentDataBlock:^(NSInteger bytesSent, NSInteger totalBytesSent, NSInteger expectedBytes) { |
| [self uploadFetcher:fetcher |
| didSendBytes:bytesSent |
| totalBytesSent:totalBytesSent |
| totalBytesExpectedToSend:expectedBytes]; |
| }]; |
| |
| // add a property to the fetcher that our progress callback will look for to |
| // know when to retry the upload |
| [fetcher setProperty:@70000 |
| forKey:kRetryAtKey]; |
| |
| [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { |
| [self testFetcher:fetcher |
| finishedWithData:data |
| error:error]; |
| }]; |
| |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| |
| // check the request of the final chunk fetcher to be sure we were uploading |
| // chunks as expected. |
| reqHdrs = [fetcher.mutableRequest allHTTPHeaderFields]; |
| |
| uploadReqURLPath = @"gettysburgaddress.txt.location"; |
| contentLength = [reqHdrs objectForKey:@"Content-Length"]; |
| contentRange = [reqHdrs objectForKey:@"Content-Range"]; |
| |
| XCTAssertTrue([[[request URL] absoluteString] hasSuffix:uploadReqURLPath], |
| @"upload request wrong"); |
| XCTAssertEqualObjects(contentLength, @"24499", @"content length"); |
| XCTAssertEqualObjects(contentRange, @"bytes 174501-198999/199000", @"range"); |
| |
| |
| // |
| // upload a small buffer |
| // |
| [self resetFetchResponse]; |
| |
| fetcher = [GTMHTTPUploadFetcher uploadFetcherWithRequest:request |
| uploadData:smallData |
| uploadMIMEType:@"text/plain" |
| chunkSize:75000 |
| fetcherService:nil]; |
| [fetcher setAllowLocalhostRequest:YES]; |
| |
| [fetcher beginFetchWithDelegate:self |
| didFinishSelector:finishedSel]; |
| |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| |
| // check that we fetched the expected data |
| XCTAssertEqualObjects(fetchedData_, gettysburgAddress, |
| @"Lincoln disappointed"); |
| XCTAssertNotNil(fetchedResponse_, |
| @"failed to get fetch response, status:%d error:%@", |
| fetchedStatus_, fetcherError_); |
| XCTAssertNotNil(fetchedRequest_, |
| @"failed to get fetch request, URL %@", urlString); |
| XCTAssertNil(fetcherError_, @"fetching data gave error: %@", fetcherError_); |
| XCTAssertEqual(fetchedStatus_, 200, |
| @"unexpected status for URL %@", urlString); |
| |
| // check the request of the final chunk fetcher to be sure we were uploading |
| // chunks as expected |
| reqHdrs = [fetcher.mutableRequest allHTTPHeaderFields]; |
| |
| uploadReqURLPath = @"gettysburgaddress.txt.location"; |
| contentLength = [reqHdrs objectForKey:@"Content-Length"]; |
| contentRange = [reqHdrs objectForKey:@"Content-Range"]; |
| |
| XCTAssertTrue([[[request URL] absoluteString] hasSuffix:uploadReqURLPath], |
| @"upload request wrong"); |
| XCTAssertEqualObjects(contentLength, @"13", @"content length"); |
| XCTAssertEqualObjects(contentRange, @"bytes 0-12/13", @"range"); |
| |
| // |
| // upload an empty buffer |
| // |
| [self resetFetchResponse]; |
| |
| fetcher = [GTMHTTPUploadFetcher uploadFetcherWithRequest:request |
| uploadData:[NSData data] |
| uploadMIMEType:@"text/plain" |
| chunkSize:75000 |
| fetcherService:nil]; |
| [fetcher setAllowLocalhostRequest:YES]; |
| |
| [fetcher beginFetchWithDelegate:self |
| didFinishSelector:finishedSel]; |
| |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| |
| // check that we fetched the expected data |
| XCTAssertEqualObjects(fetchedData_, gettysburgAddress, |
| @"unexpected response data"); |
| XCTAssertNotNil(fetchedResponse_, |
| @"failed to get fetch response, status:%d error:%@", |
| fetchedStatus_, fetcherError_); |
| XCTAssertNotNil(fetchedRequest_, |
| @"failed to get fetch request, URL %@", urlString); |
| XCTAssertNil(fetcherError_, @"fetching data gave error: %@", fetcherError_); |
| XCTAssertEqual(fetchedStatus_, 200, |
| @"unexpected status for URL %@", urlString); |
| |
| // |
| // delete the big file |
| // |
| [[NSFileManager defaultManager] removeItemAtPath:bigFilePath |
| error:NULL]; |
| } |
| |
| #pragma mark - |
| |
| - (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString |
| cachingDatedData:(BOOL)doCaching { |
| |
| return [self doFetchWithURLString:(NSString *)urlString |
| cachingDatedData:doCaching |
| retrySelector:nil |
| maxRetryInterval:0 |
| credential:nil |
| userData:nil]; |
| } |
| |
| - (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString |
| cachingDatedData:(BOOL)doCaching |
| retrySelector:(SEL)retrySel |
| maxRetryInterval:(NSTimeInterval)maxRetryInterval |
| credential:(NSURLCredential *)credential |
| userData:(id)userData { |
| NSURL *url = [NSURL URLWithString:urlString]; |
| NSURLRequest *req = [NSURLRequest requestWithURL:url |
| cachePolicy:NSURLRequestReloadIgnoringCacheData |
| timeoutInterval:kGiveUpInterval]; |
| GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:req]; |
| |
| XCTAssertNotNil(fetcher, @"Failed to allocate fetcher"); |
| |
| [fetcher setAllowLocalhostRequest:YES]; |
| |
| // setting the fetch history will add the "If-modified-since" header |
| // to repeat requests |
| [fetchHistory_ setShouldCacheETaggedData:doCaching]; |
| [fetcher setFetchHistory:fetchHistory_]; |
| |
| if (retrySel) { |
| [fetcher setRetryEnabled:YES]; |
| [fetcher setRetrySelector:retrySel]; |
| [fetcher setMaxRetryInterval:maxRetryInterval]; |
| [fetcher setUserData:userData]; |
| |
| // we force a minimum retry interval for unit testing; otherwise, |
| // we'd have no idea how many retries will occur before the max |
| // retry interval occurs, since the minimum would be random |
| [fetcher setMinRetryInterval:1.0]; |
| } |
| |
| [fetcher setCredential:credential]; |
| [fetcher setAllowLocalhostRequest:YES]; |
| |
| BOOL isFetching = [fetcher beginFetchWithDelegate:self |
| didFinishSelector:@selector(testFetcher:finishedWithData:error:)]; |
| XCTAssertTrue(isFetching, @"Begin fetch failed"); |
| |
| if (isFetching) { |
| [fetcher waitForCompletionWithTimeout:kGiveUpInterval]; |
| } |
| return fetcher; |
| } |
| |
| - (NSString *)localPathForFileName:(NSString *)name { |
| NSString *docRoot = [self docRootPath]; |
| NSString *filePath = [docRoot stringByAppendingPathComponent:name]; |
| return filePath; |
| } |
| |
| - (NSString *)localURLStringToTestFileName:(NSString *)name { |
| |
| // we need to create http URLs referring to the desired |
| // resource to be found by the http server running locally |
| |
| // return a localhost:port URL for the test file |
| NSString *urlString = [NSString stringWithFormat:@"http://localhost:%d/%@", |
| [testServer_ port], name]; |
| |
| // we exclude parameters |
| NSRange range = [name rangeOfString:@"?"]; |
| if (range.location != NSNotFound) { |
| name = [name substringToIndex:range.location]; |
| } |
| |
| // just for sanity, let's make sure we see the file locally, so |
| // we can expect the Python http server to find it too |
| NSString *filePath = [self localPathForFileName:name]; |
| |
| BOOL doesExist = [[NSFileManager defaultManager] fileExistsAtPath:filePath]; |
| XCTAssertTrue(doesExist, @"Missing test file %@", filePath); |
| |
| return urlString; |
| } |
| |
| - (void)testFetcher:(GTMHTTPFetcher *)fetcher |
| finishedWithData:(NSData *)data |
| error:(NSError *)error { |
| fetchedData_ = [data copy]; |
| fetchedStatus_ = [fetcher statusCode]; |
| fetchedRequest_ = [[fetcher mutableRequest] retain]; |
| fetchedResponse_ = [[fetcher response] retain]; |
| fetcherError_ = [error retain]; |
| } |
| |
| |
| // Selector for allowing up to N retries, where N is an NSNumber in the |
| // fetcher's userData |
| - (BOOL)countRetriesFetcher:(GTMHTTPFetcher *)fetcher |
| willRetry:(BOOL)suggestedWillRetry |
| forError:(NSError *)error { |
| |
| int count = [fetcher retryCount]; |
| int allowedRetryCount = [[fetcher userData] intValue]; |
| |
| BOOL shouldRetry = (count < allowedRetryCount); |
| |
| XCTAssertEqual([fetcher nextRetryInterval], pow(2.0, [fetcher retryCount]), |
| @"unexpected next retry interval (expected %f, was %f)", |
| pow(2.0, [fetcher retryCount]), |
| [fetcher nextRetryInterval]); |
| |
| NSData *statusData = [[error userInfo] objectForKey:kGTMHTTPFetcherStatusDataKey]; |
| NSString *dataStr = [[[NSString alloc] initWithData:statusData |
| encoding:NSUTF8StringEncoding] autorelease]; |
| NSInteger code = [error code]; |
| if (code == 503) { |
| NSString *expectedStr = |
| @"{ \"error\" : { \"message\" : \"Server Status 503\", \"code\" : 503 } }"; |
| XCTAssertEqualObjects(dataStr, expectedStr); |
| } |
| return shouldRetry; |
| } |
| |
| // Selector for retrying and changing the request to one that will succeed |
| - (BOOL)fixRequestFetcher:(GTMHTTPFetcher *)fetcher |
| willRetry:(BOOL)suggestedWillRetry |
| forError:(NSError *)error { |
| |
| XCTAssertEqual([fetcher nextRetryInterval], pow(2.0, [fetcher retryCount]), |
| @"unexpected next retry interval (expected %f, was %f)", |
| pow(2.0, [fetcher retryCount]), |
| [fetcher nextRetryInterval]); |
| |
| // fix it - change the request to a URL which does not have a status value |
| NSString *urlString = [self localURLStringToTestFileName:kValidFileName]; |
| |
| NSURL *url = [NSURL URLWithString:urlString]; |
| [[fetcher mutableRequest] setURL:url]; |
| |
| return YES; // do the retry fetch; it should succeed now |
| } |
| |
| @end |
| |
| @implementation TestAuthorizer |
| |
| @synthesize expired = hasExpired_; |
| |
| + (TestAuthorizer *)authorizer { |
| return [[[self alloc] init] autorelease]; |
| } |
| |
| + (TestAuthorizer *)expiredAuthorizer { |
| TestAuthorizer *authorizer = [self authorizer]; |
| authorizer.expired = YES; |
| return authorizer; |
| } |
| |
| - (void)authorizeRequest:(NSMutableURLRequest *)request |
| delegate:(id)delegate |
| didFinishSelector:(SEL)sel { |
| NSString *value = self.expired ? kExpiredBearerValue : kGoodBearerValue; |
| [request setValue:value forHTTPHeaderField:@"Authorization"]; |
| NSError *error = nil; |
| |
| if (delegate && sel) { |
| NSMethodSignature *sig = [delegate methodSignatureForSelector:sel]; |
| NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig]; |
| [invocation setSelector:sel]; |
| [invocation setTarget:delegate]; |
| [invocation setArgument:&self atIndex:2]; |
| [invocation setArgument:&request atIndex:3]; |
| [invocation setArgument:&error atIndex:4]; |
| [invocation invoke]; |
| } |
| } |
| |
| - (void)stopAuthorization { |
| } |
| |
| - (void)stopAuthorizationForRequest:(NSURLRequest *)request { |
| } |
| |
| - (BOOL)isAuthorizingRequest:(NSURLRequest *)request { |
| return NO; |
| } |
| |
| - (BOOL)isAuthorizedRequest:(NSURLRequest *)request { |
| NSString *value = [[request allHTTPHeaderFields] objectForKey:@"Authorization"]; |
| BOOL isValid = [value isEqual:kGoodBearerValue]; |
| return isValid; |
| } |
| |
| - (NSString *)userEmail { |
| return @""; |
| } |
| |
| - (BOOL)primeForRefresh { |
| self.expired = NO; |
| return YES; |
| } |
| |
| @end |