aboutsummaryrefslogtreecommitdiff
path: root/Foundation/GTMHTTPFetcherTest.m
diff options
context:
space:
mode:
Diffstat (limited to 'Foundation/GTMHTTPFetcherTest.m')
-rw-r--r--Foundation/GTMHTTPFetcherTest.m543
1 files changed, 543 insertions, 0 deletions
diff --git a/Foundation/GTMHTTPFetcherTest.m b/Foundation/GTMHTTPFetcherTest.m
new file mode 100644
index 0000000..16c8d0f
--- /dev/null
+++ b/Foundation/GTMHTTPFetcherTest.m
@@ -0,0 +1,543 @@
+//
+// GTMHTTPFetcherTest.m
+//
+// Copyright 2007-2008 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 <SenTestingKit/SenTestingKit.h>
+#import <unistd.h>
+#import "GTMHTTPFetcher.h"
+#import "GTMSenTestCase.h"
+
+@interface GTMHTTPFetcherTest : SenTestCase {
+ // these ivars are checked after fetches, and are reset by resetFetchResponse
+ NSData *fetchedData_;
+ NSError *fetcherError_;
+ NSInteger fetchedStatus_;
+ NSURLResponse *fetchedResponse_;
+ NSMutableURLRequest *fetchedRequest_;
+
+ // setup/teardown ivars
+ NSMutableDictionary *fetchHistory_;
+ NSTask *server_; // python http server
+ BOOL didServerLaunch_; // Tracks the state of our server
+ BOOL didServerDie_;
+ NSMutableData *launchBuffer_; // Storage for output from our server
+}
+@end
+
+@interface GTMHTTPFetcherTest (PrivateMethods)
+- (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString
+ cachingDatedData:(BOOL)doCaching;
+
+- (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString
+ cachingDatedData:(BOOL)doCaching
+ retrySelector:(SEL)retrySel
+ maxRetryInterval:(NSTimeInterval)maxRetryInterval
+ userData:(id)userData;
+
+- (NSString *)fileURLStringToTestFileName:(NSString *)name;
+@end
+
+@implementation GTMHTTPFetcherTest
+
+static const int kServerPortNumber = 54579;
+static const NSTimeInterval kRunLoopInterval = 0.01;
+// The bogus-fetch test can take >10s to pass. Pick something way higher
+// to avoid failing.
+static const NSTimeInterval kGiveUpInterval = 60.0; // bail on the test if 60 seconds elapse
+
+static NSString *const kValidFileName = @"GTMHTTPFetcherTestPage.html";
+
+- (void)gotData:(NSNotification*)notification {
+ // our server sends out a string to confirm that it launched
+ NSFileHandle *handle = [notification object];
+ NSData *launchMessageData = [handle availableData];
+ [launchBuffer_ appendData:launchMessageData];
+ NSString *launchStr =
+ [[[NSString alloc] initWithData:launchBuffer_
+ encoding:NSUTF8StringEncoding] autorelease];
+ didServerLaunch_ =
+ [launchStr rangeOfString:@"started GTMHTTPFetcherTestServer"].location != NSNotFound;
+ if (!didServerLaunch_) {
+ _GTMDevLog(@"gotData launching httpserver: %@", launchStr);
+ [handle readInBackgroundAndNotify];
+ }
+}
+
+- (void)didDie:(NSNotification*)notification {
+ _GTMDevLog(@"server died");
+ didServerDie_ = YES;
+}
+
+
+- (void)setUp {
+ fetchHistory_ = [[NSMutableDictionary alloc] init];
+
+ // run the python http server, located in the Tests directory
+ NSBundle *testBundle = [NSBundle bundleForClass:[self class]];
+ STAssertNotNil(testBundle, nil);
+
+ NSString *serverPath =
+ [testBundle pathForResource:@"GTMHTTPFetcherTestServer" ofType:@""];
+ STAssertNotNil(serverPath, nil);
+
+ NSArray *argArray = [NSArray arrayWithObjects:serverPath,
+ @"-p", [NSString stringWithFormat:@"%d", kServerPortNumber],
+ @"-r", [serverPath stringByDeletingLastPathComponent], nil];
+
+ server_ = [[NSTask alloc] init];
+ [server_ setArguments:argArray];
+ [server_ setLaunchPath:@"/usr/bin/python"];
+ [server_ setEnvironment:[NSDictionary dictionary]]; // don't inherit anything from us
+
+ // pipes will be cleaned up when server_ is torn down.
+ NSPipe *outputPipe = [NSPipe pipe];
+ NSPipe *errorPipe = [NSPipe pipe];
+
+ [server_ setStandardOutput:outputPipe];
+ [server_ setStandardError:errorPipe];
+
+ NSFileHandle *outputHandle = [outputPipe fileHandleForReading];
+ NSFileHandle *errorHandle = [errorPipe fileHandleForReading];
+
+ didServerLaunch_ = NO;
+ didServerDie_ = NO;
+
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(gotData:)
+ name:NSFileHandleDataAvailableNotification
+ object:outputHandle];
+ [center addObserver:self
+ selector:@selector(gotData:)
+ name:NSFileHandleDataAvailableNotification
+ object:errorHandle];
+ [center addObserver:self
+ selector:@selector(didDie:)
+ name:NSTaskDidTerminateNotification
+ object:server_];
+
+ [launchBuffer_ autorelease];
+ launchBuffer_ = [[NSMutableData data] retain];
+ [outputHandle waitForDataInBackgroundAndNotify];
+ [errorHandle waitForDataInBackgroundAndNotify];
+ [server_ launch];
+
+ NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:kGiveUpInterval];
+ while ((!didServerDie_ && !didServerLaunch_) &&
+ [giveUpDate timeIntervalSinceNow] > 0) {
+ NSDate* loopIntervalDate =
+ [NSDate dateWithTimeIntervalSinceNow:kRunLoopInterval];
+ [[NSRunLoop currentRunLoop] runUntilDate:loopIntervalDate];
+ }
+
+ [center removeObserver:self];
+
+ STAssertTrue(didServerLaunch_ && [server_ isRunning] && !didServerDie_,
+ @"Python http server not launched.\n"
+ "Args:%@\n"
+ "Environment:%@\n",
+ [argArray componentsJoinedByString:@" "], [server_ environment]);
+}
+
+- (void)resetFetchResponse {
+ [fetchedData_ release];
+ fetchedData_ = nil;
+
+ [fetcherError_ release];
+ fetcherError_ = nil;
+
+ [fetchedRequest_ release];
+ fetchedRequest_ = nil;
+
+ [fetchedResponse_ release];
+ fetchedResponse_ = nil;
+
+ fetchedStatus_ = 0;
+}
+
+- (void)tearDown {
+ [server_ terminate];
+ [server_ waitUntilExit];
+ [server_ release];
+ server_ = nil;
+ [launchBuffer_ release];
+ launchBuffer_ = nil;
+ [self resetFetchResponse];
+
+ [fetchHistory_ release];
+ fetchHistory_ = nil;
+}
+
+- (void)testValidFetch {
+
+ NSString *urlString = [self fileURLStringToTestFileName:kValidFileName];
+
+ [self doFetchWithURLString:urlString cachingDatedData:YES];
+
+ STAssertNotNil(fetchedData_,
+ @"failed to fetch data, status:%ld error:%@, URL:%@",
+ (long)fetchedStatus_, fetcherError_, urlString);
+ STAssertNotNil(fetchedResponse_,
+ @"failed to get fetch response, status:%ld error:%@",
+ (long)fetchedStatus_, fetcherError_);
+ STAssertNotNil(fetchedRequest_, @"failed to get fetch request, URL %@",
+ urlString);
+ STAssertNil(fetcherError_, @"fetching data gave error: %@", fetcherError_);
+ STAssertEquals(fetchedStatus_, (NSInteger)200,
+ @"fetching data expected status 200, instead got %ld, for URL %@",
+ (long)fetchedStatus_, urlString);
+
+ // no cookies should be sent with our first request
+ NSDictionary *headers = [fetchedRequest_ allHTTPHeaderFields];
+ NSString *cookiesSent = [headers objectForKey:@"Cookie"];
+ STAssertNil(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];
+ STAssertEqualObjects(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 modified since" header will be set (because
+ // we're calling setFetchHistory: below) and caching ON, and verify that we
+ // got a good data from the cache, along with a "Not modified" status
+
+ [self resetFetchResponse];
+
+ [self doFetchWithURLString:urlString cachingDatedData:YES];
+
+ STAssertEqualObjects(fetchedData_, originalFetchedData,
+ @"cache data mismatch");
+
+ STAssertNotNil(fetchedData_,
+ @"failed to fetch data, status:%ld error:%@, URL:%@",
+ (long)fetchedStatus_, fetcherError_, urlString);
+ STAssertNotNil(fetchedResponse_,
+ @"failed to get fetch response, status:%;d error:%@",
+ (long)fetchedStatus_, fetcherError_);
+ STAssertNotNil(fetchedRequest_, @"failed to get fetch request, URL %@",
+ urlString);
+ STAssertNil(fetcherError_, @"fetching data gave error: %@", fetcherError_);
+
+ STAssertEquals(fetchedStatus_, (NSInteger)kGTMHTTPFetcherStatusNotModified, // 304
+ @"fetching data expected status 304, instead got %ld, for URL %@",
+ (long)fetchedStatus_, urlString);
+
+ // the TestCookie set previously should be sent with this request
+ cookiesSent = [[fetchedRequest_ allHTTPHeaderFields] objectForKey:@"Cookie"];
+ STAssertEqualObjects(cookiesSent, cookieExpected, @"Cookie not sent");
+
+ // Now fetch twice without caching enabled, and verify that we got a
+ // "Not modified" status, along with a non-nil but empty NSData (which
+ // is normal for that status code)
+
+ [self resetFetchResponse];
+
+ [fetchHistory_ removeAllObjects];
+
+ [self doFetchWithURLString:urlString cachingDatedData:NO];
+
+ STAssertEqualObjects(fetchedData_, originalFetchedData,
+ @"cache data mismatch");
+
+ [self resetFetchResponse];
+ [self doFetchWithURLString:urlString cachingDatedData:NO];
+
+ STAssertNotNil(fetchedData_, @"");
+ STAssertEquals([fetchedData_ length], (NSUInteger)0, @"unexpected data");
+ STAssertEquals(fetchedStatus_, (NSInteger)kGTMHTTPFetcherStatusNotModified,
+ @"fetching data expected status 304, instead got %d", fetchedStatus_);
+ STAssertNil(fetcherError_, @"unexpected error: %@", fetcherError_);
+
+}
+
+- (void)testBogusFetch {
+ // fetch a live, invalid URL
+ NSString *badURLString = @"http://localhost:86/";
+ [self doFetchWithURLString:badURLString cachingDatedData:NO];
+
+ const int kServiceUnavailableStatus = 503;
+
+ if (fetchedStatus_ == kServiceUnavailableStatus) {
+ // some proxies give a "service unavailable" error for bogus fetches
+ } else {
+
+ if (fetchedData_) {
+ NSString *str = [[[NSString alloc] initWithData:fetchedData_
+ encoding:NSUTF8StringEncoding] autorelease];
+ STAssertNil(fetchedData_, @"fetched unexpected data: %@", str);
+ }
+
+ STAssertNotNil(fetcherError_, @"failed to receive fetching error");
+ STAssertEquals(fetchedStatus_, (NSInteger)0,
+ @"fetching data expected no status from no response, instead got %d",
+ fetchedStatus_);
+ }
+
+ // fetch with a specific status code from our http server
+ [self resetFetchResponse];
+
+ NSString *invalidWebPageFile =
+ [kValidFileName stringByAppendingString:@"?status=400"];
+ NSString *statusUrlString =
+ [self fileURLStringToTestFileName:invalidWebPageFile];
+
+ [self doFetchWithURLString:statusUrlString cachingDatedData:NO];
+
+ STAssertNotNil(fetchedData_, @"fetch lacked data with error info");
+ STAssertNil(fetcherError_, @"expected bad status but got an error");
+ STAssertEquals(fetchedStatus_, (NSInteger)400,
+ @"unexpected status, error=%@", fetcherError_);
+}
+
+- (void)testRetryFetches {
+
+ GTMHTTPFetcher *fetcher;
+
+ NSString *invalidFile = [kValidFileName stringByAppendingString:@"?status=503"];
+ NSString *urlString = [self fileURLStringToTestFileName:invalidFile];
+
+ SEL countRetriesSel = @selector(countRetriesFetcher:willRetry:forError:);
+ SEL fixRequestSel = @selector(fixRequestFetcher:willRetry:forError:);
+
+ //
+ // test: retry until timeout, then expect failure with status message
+ //
+
+ NSNumber *lotsOfRetriesNumber = [NSNumber numberWithInt:1000];
+
+ fetcher= [self doFetchWithURLString:urlString
+ cachingDatedData:NO
+ retrySelector:countRetriesSel
+ maxRetryInterval:5.0 // retry intervals of 1, 2, 4
+ userData:lotsOfRetriesNumber];
+
+ STAssertNotNil(fetchedData_, @"error data is expected");
+ STAssertEquals(fetchedStatus_, (NSInteger)503, nil);
+ STAssertEquals([fetcher retryCount], 3U, @"retry count unexpected");
+
+ //
+ // test: retry twice, then give up
+ //
+ [self resetFetchResponse];
+
+ NSNumber *twoRetriesNumber = [NSNumber numberWithInt:2];
+
+ fetcher= [self doFetchWithURLString:urlString
+ cachingDatedData:NO
+ retrySelector:countRetriesSel
+ maxRetryInterval:10.0 // retry intervals of 1, 2, 4, 8
+ userData:twoRetriesNumber];
+
+ STAssertNotNil(fetchedData_, @"error data is expected");
+ STAssertEquals(fetchedStatus_, (NSInteger)503, nil);
+ STAssertEquals([fetcher retryCount], 2U, @"retry count unexpected");
+
+
+ //
+ // test: retry, making the request succeed on the first retry
+ // by fixing the URL
+ //
+ [self resetFetchResponse];
+
+ fetcher= [self doFetchWithURLString:urlString
+ cachingDatedData:NO
+ retrySelector:fixRequestSel
+ maxRetryInterval:30.0 // should only retry once due to selector
+ userData:lotsOfRetriesNumber];
+
+ STAssertNotNil(fetchedData_, @"data is expected");
+ STAssertEquals(fetchedStatus_, (NSInteger)200, nil);
+ STAssertEquals([fetcher retryCount], 1U, @"retry count unexpected");
+}
+
+#pragma mark -
+
+- (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString
+ cachingDatedData:(BOOL)doCaching {
+
+ return [self doFetchWithURLString:(NSString *)urlString
+ cachingDatedData:(BOOL)doCaching
+ retrySelector:nil
+ maxRetryInterval:0
+ userData:nil];
+}
+
+- (GTMHTTPFetcher *)doFetchWithURLString:(NSString *)urlString
+ cachingDatedData:(BOOL)doCaching
+ retrySelector:(SEL)retrySel
+ maxRetryInterval:(NSTimeInterval)maxRetryInterval
+ userData:(id)userData {
+
+ NSURL *url = [NSURL URLWithString:urlString];
+ NSURLRequest *req = [NSURLRequest requestWithURL:url
+ cachePolicy:NSURLRequestReloadIgnoringCacheData
+ timeoutInterval:kGiveUpInterval];
+ GTMHTTPFetcher *fetcher = [GTMHTTPFetcher httpFetcherWithRequest:req];
+
+ STAssertNotNil(fetcher, @"Failed to allocate fetcher");
+
+ // setting the fetch history will add the "If-modified-since" header
+ // to repeat requests
+ [fetcher setFetchHistory:fetchHistory_];
+ if (doCaching != [fetcher shouldCacheDatedData]) {
+ // only set the value when it changes since setting it to nil clears out
+ // some of the state and our tests need the state between some non caching
+ // fetches.
+ [fetcher setShouldCacheDatedData:doCaching];
+ }
+
+ if (retrySel) {
+ [fetcher setIsRetryEnabled: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];
+ }
+
+ BOOL isFetching =
+ [fetcher beginFetchWithDelegate:self
+ didFinishSelector:@selector(testFetcher:finishedWithData:)
+ didFailSelector:@selector(testFetcher:failedWithError:)];
+ STAssertTrue(isFetching, @"Begin fetch failed");
+
+ if (isFetching) {
+
+ // Give time for the fetch to happen, but give up if 10 seconds elapse with
+ // no response
+ NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:kGiveUpInterval];
+ while ((!fetchedData_ && !fetcherError_) &&
+ [giveUpDate timeIntervalSinceNow] > 0) {
+ NSDate* loopIntervalDate =
+ [NSDate dateWithTimeIntervalSinceNow:kRunLoopInterval];
+ [[NSRunLoop currentRunLoop] runUntilDate:loopIntervalDate];
+ }
+ }
+
+ return fetcher;
+}
+
+- (NSString *)fileURLStringToTestFileName:(NSString *)name {
+
+ // we need to create http URLs referring to the desired
+ // resource to be found by the python http server running locally
+
+ // return a localhost:port URL for the test file
+ NSString *urlString = [NSString stringWithFormat:@"http://localhost:%d/%@",
+ kServerPortNumber, name];
+
+
+ // we exclude the "?status=" that would indicate that the URL
+ // should cause a retryable error
+ NSRange range = [name rangeOfString:@"?status="];
+ if (range.length > 0) {
+ name = [name substringToIndex:range.location];
+ }
+
+ // we exclude the ".auth" extension that would indicate that the URL
+ // should be tested with authentication
+ if ([[name pathExtension] isEqual:@"auth"]) {
+ name = [name stringByDeletingPathExtension];
+ }
+
+ // just for sanity, let's make sure we see the file locally, so
+ // we can expect the Python http server to find it too
+ NSBundle *testBundle = [NSBundle bundleForClass:[self class]];
+ STAssertNotNil(testBundle, nil);
+
+ NSString *filePath =
+ [testBundle pathForResource:[name stringByDeletingPathExtension]
+ ofType:[name pathExtension]];
+ STAssertNotNil(filePath, nil);
+
+ return urlString;
+}
+
+
+
+- (void)testFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data {
+ fetchedData_ = [data copy];
+ fetchedStatus_ = [fetcher statusCode]; // this implicitly tests that the fetcher has kept the response
+ fetchedRequest_ = [[fetcher request] retain];
+ fetchedResponse_ = [[fetcher response] retain];
+}
+
+- (void)testFetcher:(GTMHTTPFetcher *)fetcher failedWithError:(NSError *)error {
+ // if it's a status error, don't hang onto the error, just the status/data
+ if ([[error domain] isEqual:kGTMHTTPFetcherStatusDomain]) {
+ fetchedData_ = [[[error userInfo] objectForKey:kGTMHTTPFetcherStatusDataKey] copy];
+ fetchedStatus_ = [error code]; // this implicitly tests that the fetcher has kept the response
+ } else {
+ fetcherError_ = [error retain];
+ fetchedStatus_ = [fetcher statusCode];
+ }
+}
+
+
+// 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);
+
+ STAssertEquals([fetcher nextRetryInterval], pow(2.0, [fetcher retryCount]),
+ @"unexpected next retry interval (expected %f, was %f)",
+ pow(2.0, [fetcher retryCount]),
+ [fetcher nextRetryInterval]);
+
+ return shouldRetry;
+}
+
+// Selector for retrying and changing the request to one that will succeed
+- (BOOL)fixRequestFetcher:(GTMHTTPFetcher *)fetcher
+ willRetry:(BOOL)suggestedWillRetry
+ forError:(NSError *)error {
+
+ STAssertEquals([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 fileURLStringToTestFileName:kValidFileName];
+
+ NSURL *url = [NSURL URLWithString:urlString];
+ [fetcher setRequest:[NSURLRequest requestWithURL:url]];
+
+ return YES; // do the retry fetch; it should succeed now
+}
+
+@end
+