PageRenderTime 56ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/core/externals/update-engine/externals/gdata-objectivec-client/Source/Tests/GDataServiceTest.m

http://macfuse.googlecode.com/
Objective C | 1565 lines | 949 code | 384 blank | 232 comment | 34 complexity | d45d4c1810094d3bbb106172c70b825c MD5 | raw file
Possible License(s): Apache-2.0, BSD-3-Clause, GPL-2.0
  1. /* Copyright (c) 2007 Google Inc.
  2. *
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. //
  16. // GDataServiceTest.m
  17. //
  18. // service tests are slow, so we might skip them when developing other
  19. // unit tests
  20. #if !GDATA_SKIPSERVICETEST
  21. #import <SenTestingKit/SenTestingKit.h>
  22. #import "GData.h"
  23. @interface GDataServiceTest : SenTestCase {
  24. NSTask *server_;
  25. BOOL isServerRunning_;
  26. GDataServiceGoogle *service_;
  27. GDataServiceTicket *ticket_;
  28. GDataObject *fetchedObject_;
  29. NSError *fetcherError_;
  30. int fetchStartedNotificationCount_;
  31. int fetchStoppedNotificationCount_;
  32. unsigned long long lastProgressDeliveredCount_;
  33. unsigned long long lastProgressTotalCount_;
  34. int parseStartedCount_;
  35. int parseStoppedCount_;
  36. int retryCounter_;
  37. int retryDelayStartedNotificationCount_;
  38. int retryDelayStoppedNotificationCount_;
  39. NSString *authToken_;
  40. NSError *authError_;
  41. }
  42. @end
  43. #define typeof __typeof__ // fixes http://www.brethorsting.com/blog/2006/02/stupid-issue-with-ocunit.html
  44. // Denote that a function/method retains the nsobject that it returns.
  45. // This fixes warnings generated by clang when we return a non-autoreleased
  46. // object from a function.
  47. // http://clang-analyzer.llvm.org/annotations.html#cocoa_mem
  48. #ifndef NS_RETURNS_RETAINED
  49. #ifndef __has_feature // Optional.
  50. #define __has_feature(x) 0 // Compatibility with non-clang compilers.
  51. #endif // __has_feature
  52. #if __has_feature(attribute_ns_returns_retained)
  53. #define NS_RETURNS_RETAINED __attribute__((ns_returns_retained))
  54. #else
  55. #define NS_RETURNS_RETAINED
  56. #endif // __has_feature(attribute_ns_returns_retained)
  57. #endif
  58. @interface MyGDataFeedSpreadsheetSurrogate: GDataFeedSpreadsheet
  59. - (NSString *)mySurrogateFeedName;
  60. @end
  61. @interface MyGDataEntrySpreadsheetSurrogate: GDataEntrySpreadsheet
  62. - (NSString *)mySurrogateEntryName;
  63. @end
  64. @interface MyGDataLinkSurrogate: GDataLink
  65. - (NSString *)mySurrogateLinkName;
  66. @end
  67. NSTask *StartHTTPServerTask(int portNumber) NS_RETURNS_RETAINED;
  68. // StartHTTPServerTask is used below and in GTMHTTPFetcherTest
  69. NSTask *StartHTTPServerTask(int portNumber) {
  70. // run the python http server, located in the Tests directory
  71. NSString *currentDir = [[NSFileManager defaultManager] currentDirectoryPath];
  72. NSString *serverPath = [currentDir stringByAppendingPathComponent:@"Tests/GDataTestHTTPServer.py"];
  73. // The framework builds as garbage collection-compatible, so unit tests run
  74. // with GC both enabled and disabled. But launching the python http server
  75. // with GC disabled causes it to return an error. To avoid that, we'll
  76. // change its launch environment to allow the python server run with GC.
  77. // We also remove the Malloc debugging variables, since they interfere with
  78. // the program output.
  79. NSDictionary *env = [[NSProcessInfo processInfo] environment];
  80. NSMutableDictionary *mutableEnv = [NSMutableDictionary dictionaryWithDictionary:env];
  81. [mutableEnv removeObjectForKey:@"OBJC_DISABLE_GC"];
  82. [mutableEnv removeObjectForKey:@"MallocGuardEdges"];
  83. [mutableEnv removeObjectForKey:@"MallocPreScribble"];
  84. [mutableEnv removeObjectForKey:@"MallocScribble"];
  85. NSArray *argArray = [NSArray arrayWithObjects:serverPath,
  86. @"-p", [NSString stringWithFormat:@"%d", portNumber],
  87. @"-r", [serverPath stringByDeletingLastPathComponent],
  88. nil];
  89. NSTask *server = [[NSTask alloc] init];
  90. [server setArguments:argArray];
  91. [server setLaunchPath:@"/usr/bin/python"];
  92. [server setEnvironment:mutableEnv];
  93. // pipe will be cleaned up when server is torn down.
  94. NSPipe *pipe = [NSPipe pipe];
  95. [server setStandardOutput:pipe];
  96. [server setStandardError:pipe];
  97. [server launch];
  98. NSData *launchMessageData = [[pipe fileHandleForReading] availableData];
  99. NSString *launchStr = [[[NSString alloc] initWithData:launchMessageData
  100. encoding:NSUTF8StringEncoding] autorelease];
  101. // our server sends out a string to confirm that it launched;
  102. // launchStr either has the confirmation, or the error message.
  103. NSString *expectedLaunchStr = @"started GDataTestServer.py...";
  104. BOOL isServerRunning = [launchStr isEqual:expectedLaunchStr];
  105. return isServerRunning ? server : nil;
  106. }
  107. @implementation GDataServiceTest
  108. static int kServerPortNumber = 54579;
  109. - (void)setUp {
  110. server_ = StartHTTPServerTask(kServerPortNumber);
  111. isServerRunning_ = (server_ != nil);
  112. STAssertTrue(isServerRunning_,
  113. @">>> Python http test server failed to launch; skipping service tests\n");
  114. // create the GData service object, and set it to authenticate
  115. // from our own python http server
  116. service_ = [[GDataServiceGoogleSpreadsheet alloc] init];
  117. NSString *authDomain = [NSString stringWithFormat:@"localhost:%d", kServerPortNumber];
  118. [service_ setSignInDomain:authDomain];
  119. // install observers for fetcher notifications
  120. NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
  121. [nc addObserver:self selector:@selector(fetchStateChanged:) name:kGTMHTTPFetcherStartedNotification object:nil];
  122. [nc addObserver:self selector:@selector(fetchStateChanged:) name:kGTMHTTPFetcherStoppedNotification object:nil];
  123. [nc addObserver:self selector:@selector(parseStateChanged:) name:kGDataServiceTicketParsingStartedNotification object:nil];
  124. [nc addObserver:self selector:@selector(parseStateChanged:) name:kGDataServiceTicketParsingStoppedNotification object:nil];
  125. [nc addObserver:self selector:@selector(retryDelayStateChanged:) name:kGTMHTTPFetcherRetryDelayStartedNotification object:nil];
  126. [nc addObserver:self selector:@selector(retryDelayStateChanged:) name:kGTMHTTPFetcherRetryDelayStoppedNotification object:nil];
  127. }
  128. - (void)fetchStateChanged:(NSNotification *)note {
  129. GTMHTTPFetcher *fetcher = [note object];
  130. GDataServiceTicketBase *ticket = [fetcher GDataTicket];
  131. STAssertNotNil(ticket, @"cannot get ticket from fetch notification");
  132. if ([[note name] isEqual:kGTMHTTPFetcherStartedNotification]) {
  133. ++fetchStartedNotificationCount_;
  134. } else {
  135. ++fetchStoppedNotificationCount_;
  136. }
  137. STAssertTrue(retryDelayStartedNotificationCount_ <= fetchStartedNotificationCount_,
  138. @"fetch notification imbalance: starts=%d stops=%d",
  139. fetchStartedNotificationCount_, retryDelayStartedNotificationCount_);
  140. }
  141. - (void)parseStateChanged:(NSNotification *)note {
  142. GDataServiceTicketBase *ticket = [note object];
  143. STAssertTrue([ticket isKindOfClass:[GDataServiceTicketBase class]],
  144. @"cannot get ticket from parse notification");
  145. if ([[note name] isEqual:kGDataServiceTicketParsingStartedNotification]) {
  146. ++parseStartedCount_;
  147. } else {
  148. ++parseStoppedCount_;
  149. }
  150. STAssertTrue(parseStoppedCount_ <= parseStartedCount_,
  151. @"parse notification imbalance: starts=%d stops=%d",
  152. parseStartedCount_,
  153. parseStoppedCount_);
  154. }
  155. - (void)retryDelayStateChanged:(NSNotification *)note {
  156. GTMHTTPFetcher *fetcher = [note object];
  157. GDataServiceTicketBase *ticket = [fetcher GDataTicket];
  158. STAssertNotNil(ticket, @"cannot get ticket from retry delay notification");
  159. if ([[note name] isEqual:kGTMHTTPFetcherRetryDelayStartedNotification]) {
  160. ++retryDelayStartedNotificationCount_;
  161. } else {
  162. ++retryDelayStoppedNotificationCount_;
  163. }
  164. STAssertTrue(retryDelayStoppedNotificationCount_ <= retryDelayStartedNotificationCount_,
  165. @"retry delay notification imbalance: starts=%d stops=%d",
  166. retryDelayStartedNotificationCount_,
  167. retryDelayStoppedNotificationCount_);
  168. }
  169. - (void)resetFetchResponse {
  170. [fetchedObject_ release];
  171. fetchedObject_ = nil;
  172. [fetcherError_ release];
  173. fetcherError_ = nil;
  174. [ticket_ release];
  175. ticket_ = nil;
  176. retryCounter_ = 0;
  177. lastProgressDeliveredCount_ = 0;
  178. lastProgressTotalCount_ = 0;
  179. // Set the UA to avoid log warnings during tests, except the first test,
  180. // which will use an auto-generated user agent
  181. if ([service_ userAgent] == nil) {
  182. [service_ setUserAgent:@"GData-UnitTests-99.99"];
  183. }
  184. if (![service_ shouldCacheResponseData]) {
  185. // we don't want to see 304s in our service response tests now,
  186. // though the tests below will check for them in the underlying
  187. // fetchers when we get a cached response
  188. [service_ clearResponseDataCache];
  189. }
  190. fetchStartedNotificationCount_ = 0;
  191. fetchStoppedNotificationCount_ = 0;
  192. parseStartedCount_ = 0;
  193. parseStoppedCount_ = 0;
  194. retryDelayStartedNotificationCount_ = 0;
  195. retryDelayStoppedNotificationCount_ = 0;
  196. }
  197. - (void)tearDown {
  198. [server_ terminate];
  199. [server_ waitUntilExit];
  200. [server_ release];
  201. server_ = nil;
  202. isServerRunning_ = NO;
  203. [service_ release];
  204. service_ = nil;
  205. [self resetFetchResponse];
  206. }
  207. - (NSURL *)fileURLToTestFileName:(NSString *)name {
  208. // we need to create http URLs referring to the desired
  209. // resource for the python http server running locally
  210. // return a localhost:port URL for the test file
  211. NSString *urlString = [NSString stringWithFormat:@"http://localhost:%d/%@",
  212. kServerPortNumber, name];
  213. NSURL *url = [NSURL URLWithString:urlString];
  214. // just for sanity, let's make sure we see the file locally, so
  215. // we can expect the Python http server to find it too
  216. NSString *filePath = [NSString stringWithFormat:@"Tests/%@", name];
  217. // we exclude the "?status=" that would indicate that the URL
  218. // should cause a retryable error
  219. NSRange range = [filePath rangeOfString:@"?status="];
  220. if (range.location == NSNotFound) {
  221. range = [filePath rangeOfString:@"?statusxml="];
  222. }
  223. if (range.length > 0) {
  224. filePath = [filePath substringToIndex:range.location];
  225. }
  226. // we remove the extensions that would indicate special ways of
  227. // testing the URL
  228. if ([[filePath pathExtension] isEqual:@"auth"] ||
  229. [[filePath pathExtension] isEqual:@"authsub"] ||
  230. [[filePath pathExtension] isEqual:@"location"]) {
  231. filePath = [filePath stringByDeletingPathExtension];
  232. }
  233. BOOL doesExist = [[NSFileManager defaultManager] fileExistsAtPath:filePath];
  234. STAssertTrue(doesExist, @"Missing test file %@", filePath);
  235. return url;
  236. }
  237. // deleteResource calls don't return data or an error, so we'll use
  238. // a global int for the callbacks to increment to say they're done
  239. // (because NSURLConnection is performing the fetches, all this
  240. // will be safely executed on the same thread)
  241. static int gFetchCounter = 0;
  242. - (void)waitForFetch {
  243. int fetchCounter = gFetchCounter;
  244. // Give time for the fetch to happen, but give up if
  245. // 10 seconds elapse with no response
  246. NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:10.0];
  247. while ((!fetchedObject_ && !fetcherError_)
  248. && fetchCounter == gFetchCounter
  249. && [giveUpDate timeIntervalSinceNow] > 0) {
  250. NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001];
  251. [[NSRunLoop currentRunLoop] runUntilDate:stopDate];
  252. }
  253. }
  254. - (void)testFetch {
  255. if (!isServerRunning_) return;
  256. // values & keys for testing that userdata is passed from
  257. // service to ticket
  258. NSDate *defaultUserData = [NSDate date];
  259. NSString *customUserData = @"my special ticket user data";
  260. NSString *testPropertyKey = @"testPropertyKey";
  261. NSDate *defaultPropertyValue = [NSDate dateWithTimeIntervalSinceNow:999];
  262. NSString *customPropertyValue = @"ticket properties override service props";
  263. [service_ setServiceUserData:defaultUserData];
  264. [service_ setServiceProperty:defaultPropertyValue forKey:testPropertyKey];
  265. // an ".auth" extension tells the server to require the success auth token,
  266. // but the server will ignore the .auth extension when looking for the file
  267. NSURL *feedURL = [self fileURLToTestFileName:@"FeedSpreadsheetTest1.xml"];
  268. NSURL *feedErrorURL = [self fileURLToTestFileName:@"FeedSpreadsheetTest1.xml?statusxml=499"];
  269. NSURL *authFeedURL = [self fileURLToTestFileName:@"FeedSpreadsheetTest1.xml.auth"];
  270. NSURL *authSubFeedURL = [self fileURLToTestFileName:@"FeedSpreadsheetTest1.xml.authsub"];
  271. //
  272. // test: download feed only, no auth, caching on
  273. //
  274. [service_ setShouldCacheResponseData:YES];
  275. ticket_ = (GDataServiceTicket *)
  276. [service_ fetchPublicFeedWithURL:feedURL
  277. feedClass:kGDataUseRegisteredClass
  278. delegate:self
  279. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  280. [ticket_ retain];
  281. [self waitForFetch];
  282. // we'll call into the GDataObject to get its ID to confirm it's good
  283. NSString *sheetID = @"http://spreadsheets.google.com/feeds/spreadsheets/private/full";
  284. STAssertEqualObjects([(GDataFeedSpreadsheet *)fetchedObject_ identifier],
  285. sheetID, @"fetching %@ error %@", feedURL, fetcherError_);
  286. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  287. STAssertEqualObjects([ticket_ userData], defaultUserData, @"userdata error");
  288. STAssertEqualObjects([ticket_ propertyForKey:testPropertyKey],
  289. defaultPropertyValue, @"default property missing");
  290. // no cookies should be sent with our first request
  291. NSURLRequest *request = [[ticket_ objectFetcher] mutableRequest];
  292. NSString *cookiesSent = [[request allHTTPHeaderFields] objectForKey:@"Cookie"];
  293. STAssertNil(cookiesSent, @"Cookies sent unexpectedly: %@", cookiesSent);
  294. // cookies should have been set with the response; specifically, TestCookie
  295. // should be set to the name of the file requested
  296. NSURLResponse *response = [[ticket_ objectFetcher] response];
  297. NSDictionary *responseHeaderFields = [(NSHTTPURLResponse *)response allHeaderFields];
  298. NSString *cookiesSetString = [responseHeaderFields objectForKey:@"Set-Cookie"];
  299. NSString *cookieExpected = [NSString stringWithFormat:@"TestCookie=%@",
  300. [[feedURL path] lastPathComponent]];
  301. STAssertEqualObjects(cookiesSetString, cookieExpected, @"Unexpected cookie");
  302. // check that the expected notifications happened
  303. STAssertEquals(fetchStartedNotificationCount_, 1, @"fetch start note missing");
  304. STAssertEquals(fetchStoppedNotificationCount_, 1, @"fetch stopped note missing");
  305. STAssertEquals(parseStartedCount_, 1, @"parse start note missing");
  306. STAssertEquals(parseStoppedCount_, 1, @"parse stopped note missing");
  307. STAssertEquals(retryDelayStartedNotificationCount_, 0, @"retry delay note unexpected");
  308. STAssertEquals(retryDelayStoppedNotificationCount_, 0, @"retry delay note unexpected");
  309. // save a copy of the retrieved object to compare with our cache responses
  310. // later
  311. GDataObject *objectCopy = [[fetchedObject_ copy] autorelease];
  312. //
  313. // test: download feed only, unmodified so fetching a cached copy
  314. //
  315. [self resetFetchResponse];
  316. ticket_ = (GDataServiceTicket *)
  317. [service_ fetchPublicFeedWithURL:feedURL
  318. feedClass:kGDataUseRegisteredClass
  319. delegate:self
  320. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  321. [ticket_ retain];
  322. [self waitForFetch];
  323. // the TestCookie set previously should be sent with this request
  324. request = [[ticket_ objectFetcher] mutableRequest];
  325. cookiesSent = [[request allHTTPHeaderFields] objectForKey:@"Cookie"];
  326. STAssertEqualObjects(cookiesSent, cookieExpected,
  327. @"Cookie not sent");
  328. // verify the object is unchanged from the uncached fetch
  329. STAssertEqualObjects(fetchedObject_, objectCopy,
  330. @"fetching from cache for %@", feedURL);
  331. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  332. // verify the underlying fetcher got a 304 (not modified) status
  333. #if 0
  334. // TODO: The python test server doesn't provide 304s for ETag matches
  335. STAssertEquals([[ticket_ objectFetcher] statusCode],
  336. (NSInteger)kGTMHTTPFetcherStatusNotModified,
  337. @"fetching cached copy of %@", feedURL);
  338. #endif
  339. //
  340. // test: download feed only, caching turned off so we get an
  341. // actual response again
  342. //
  343. [self resetFetchResponse];
  344. [service_ setShouldCacheResponseData:NO];
  345. ticket_ = (GDataServiceTicket *)
  346. [service_ fetchPublicFeedWithURL:feedURL
  347. feedClass:kGDataUseRegisteredClass
  348. delegate:self
  349. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  350. [ticket_ retain];
  351. [self waitForFetch];
  352. // verify the object is unchanged from the original fetch
  353. STAssertEqualObjects(fetchedObject_, objectCopy,
  354. @"fetching from cache for %@", feedURL);
  355. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  356. // verify the underlying fetcher got a 200 (good) status
  357. STAssertEquals([[ticket_ objectFetcher] statusCode], (NSInteger)200,
  358. @"fetching uncached copy of %@", feedURL);
  359. //
  360. // test: download feed only, no auth, forcing a structured xml error
  361. //
  362. [self resetFetchResponse];
  363. ticket_ = (GDataServiceTicket *)
  364. [service_ fetchPublicFeedWithURL:feedErrorURL
  365. feedClass:kGDataUseRegisteredClass
  366. delegate:self
  367. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  368. [ticket_ retain];
  369. [self waitForFetch];
  370. STAssertNil(fetchedObject_, @"fetching %@", feedURL);
  371. STAssertEquals([fetcherError_ code], (NSInteger)499,
  372. @"fetcherError_=%@", fetcherError_);
  373. // get the error group from the error's userInfo and test the main
  374. // error's fields
  375. GDataServerErrorGroup *errorGroup =
  376. [[fetcherError_ userInfo] objectForKey:kGDataStructuredErrorsKey];
  377. STAssertNotNil(errorGroup, @"lacks error group");
  378. GDataServerError *serverError = [errorGroup mainError];
  379. STAssertEqualObjects([serverError domain], kGDataErrorDomainCore, @"domain");
  380. STAssertEqualObjects([serverError code], @"code_499", @"code");
  381. STAssertTrue([[serverError internalReason] hasPrefix:@"forced status error"],
  382. @"internalReason: %@", [serverError internalReason]);
  383. STAssertEqualObjects([serverError extendedHelpURI], @"http://help.com",
  384. @"help");
  385. STAssertEqualObjects([serverError sendReportURI], @"http://report.com",
  386. @"sendReport");
  387. //
  388. // test: download feed only, no auth, allocating a surrogate feed class
  389. //
  390. [self resetFetchResponse];
  391. NSDictionary *surrogates = [NSDictionary dictionaryWithObjectsAndKeys:
  392. [MyGDataFeedSpreadsheetSurrogate class], [GDataFeedSpreadsheet class],
  393. [MyGDataEntrySpreadsheetSurrogate class], [GDataEntrySpreadsheet class],
  394. [MyGDataLinkSurrogate class], [GDataLink class],
  395. nil];
  396. [service_ setServiceSurrogates:surrogates];
  397. ticket_ = (GDataServiceTicket *)
  398. [service_ fetchPublicFeedWithURL:feedURL
  399. feedClass:kGDataUseRegisteredClass
  400. delegate:self
  401. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  402. [ticket_ retain];
  403. [self waitForFetch];
  404. // we'll call into the GDataObject to get its ID to confirm it's good
  405. STAssertEqualObjects([(GDataFeedSpreadsheet *)fetchedObject_ identifier],
  406. sheetID, @"fetching %@", feedURL);
  407. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  408. STAssertEqualObjects([ticket_ userData], defaultUserData, @"userdata error");
  409. // be sure we really got an instance of the surrogate feed, entry, and
  410. // link classes
  411. MyGDataFeedSpreadsheetSurrogate *feed = (MyGDataFeedSpreadsheetSurrogate *)fetchedObject_;
  412. STAssertEqualObjects([feed mySurrogateFeedName],
  413. @"mySurrogateFeedNameBar", @"fetching %@ with surrogate", feedURL);
  414. MyGDataEntrySpreadsheetSurrogate *entry = [[feed entries] objectAtIndex:0];
  415. STAssertEqualObjects([entry mySurrogateEntryName],
  416. @"mySurrogateEntryNameFoo", @"fetching %@ with surrogate", feedURL);
  417. MyGDataLinkSurrogate *link = [[entry links] objectAtIndex:0];
  418. STAssertEqualObjects([link mySurrogateLinkName],
  419. @"mySurrogateLinkNameBaz", @"fetching %@ with surrogate", feedURL);
  420. [service_ setServiceSurrogates:nil];
  421. //
  422. // test: download feed only, successful auth, with custom ticket userdata
  423. //
  424. [self resetFetchResponse];
  425. // any username & password are considered valid unless the password
  426. // begins with the string "bad"
  427. [service_ setUserCredentialsWithUsername:@"myaccount@mydomain.com"
  428. password:@"mypassword"];
  429. ticket_ = [service_ fetchFeedWithURL:authFeedURL
  430. feedClass:kGDataUseRegisteredClass
  431. delegate:self
  432. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  433. [ticket_ retain];
  434. [ticket_ setUserData:customUserData];
  435. [ticket_ setProperty:customPropertyValue forKey:testPropertyKey];
  436. [self waitForFetch];
  437. STAssertEqualObjects([(GDataFeedSpreadsheet *)fetchedObject_ identifier],
  438. sheetID, @"fetching %@", authFeedURL);
  439. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  440. STAssertEqualObjects([ticket_ userData], customUserData, @"userdata error");
  441. STAssertEqualObjects([ticket_ propertyForKey:testPropertyKey],
  442. customPropertyValue, @"custom property error");
  443. // check that the expected notifications happened for the authentication
  444. // fetch and the object fetch
  445. STAssertEquals(fetchStartedNotificationCount_, 2, @"start note missing");
  446. STAssertEquals(fetchStoppedNotificationCount_, 2, @"stopped note missing");
  447. STAssertEquals(retryDelayStartedNotificationCount_, 0, @"retry delay note unexpected");
  448. STAssertEquals(retryDelayStoppedNotificationCount_, 0, @"retry delay note unexpected");
  449. //
  450. // test: repeat last authenticated download so we're reusing the auth token
  451. //
  452. [self resetFetchResponse];
  453. ticket_ = [service_ fetchFeedWithURL:authFeedURL
  454. feedClass:kGDataUseRegisteredClass
  455. delegate:self
  456. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  457. [ticket_ retain];
  458. [self waitForFetch];
  459. STAssertEqualObjects([(GDataFeedSpreadsheet *)fetchedObject_ identifier],
  460. sheetID, @"fetching %@", authFeedURL);
  461. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  462. //
  463. // test: repeat last authenticated download so we're reusing the auth token,
  464. // but make the auth token invalid to force a re-auth
  465. //
  466. [self resetFetchResponse];
  467. [service_ setAuthToken:@"bogus"];
  468. ticket_ = [service_ fetchFeedWithURL:authFeedURL
  469. feedClass:kGDataUseRegisteredClass
  470. delegate:self
  471. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  472. [ticket_ retain];
  473. [self waitForFetch];
  474. STAssertEqualObjects([(GDataFeedSpreadsheet *)fetchedObject_ identifier],
  475. sheetID, @"fetching %@", authFeedURL);
  476. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  477. //
  478. // test: repeat last authenticated download (which ended with a valid token),
  479. // but make the auth token invalid and disallow a reauth by changing the
  480. // credential date after the fetch starts
  481. //
  482. [self resetFetchResponse];
  483. [service_ setAuthToken:@"bogus"];
  484. ticket_ = [service_ fetchFeedWithURL:authFeedURL
  485. feedClass:kGDataUseRegisteredClass
  486. delegate:self
  487. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  488. [ticket_ retain];
  489. [service_ setCredentialDate:[NSDate date]];
  490. [self waitForFetch];
  491. STAssertNil(fetchedObject_, @"fetchedObject_=%@", fetchedObject_);
  492. STAssertEquals([fetcherError_ code], (NSInteger)401,
  493. @"fetcherError_=%@", fetcherError_);
  494. //
  495. // test: do a valid fetch, but change the credential after the fetch begins
  496. //
  497. [self resetFetchResponse];
  498. [service_ setUserCredentialsWithUsername:@"myaccount@mydomain.com"
  499. password:@"mypasword"];
  500. ticket_ = [service_ fetchFeedWithURL:authFeedURL
  501. feedClass:kGDataUseRegisteredClass
  502. delegate:self
  503. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  504. [ticket_ retain];
  505. [service_ setUserCredentialsWithUsername:@"myaccount@mydomain.com"
  506. password:@"bad"];
  507. [self waitForFetch];
  508. STAssertEqualObjects([(GDataFeedSpreadsheet *)fetchedObject_ identifier],
  509. sheetID, @"fetching %@", authFeedURL);
  510. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  511. //
  512. // test: download feed only, unsuccessful auth
  513. //
  514. [self resetFetchResponse];
  515. [service_ setUserCredentialsWithUsername:@"myaccount@mydomain.com"
  516. password:@"bad"];
  517. ticket_ = [service_ fetchFeedWithURL:authFeedURL
  518. feedClass:kGDataUseRegisteredClass
  519. delegate:self
  520. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  521. [ticket_ retain];
  522. [self waitForFetch];
  523. STAssertNil(fetchedObject_, @"fetchedObject_=%@", fetchedObject_);
  524. STAssertEquals([fetcherError_ code], (NSInteger)403,
  525. @"fetcherError_=%@", fetcherError_);
  526. STAssertEqualObjects([ticket_ userData], defaultUserData, @"userdata error");
  527. STAssertEqualObjects([ticket_ propertyForKey:testPropertyKey],
  528. defaultPropertyValue, @"default property error");
  529. //
  530. // test: download feed only, unsuccessful auth - captcha required
  531. //
  532. [self resetFetchResponse];
  533. [service_ setUserCredentialsWithUsername:@"myaccount@mydomain.com"
  534. password:@"captcha"];
  535. ticket_ = [service_ fetchFeedWithURL:authFeedURL
  536. feedClass:kGDataUseRegisteredClass
  537. delegate:self
  538. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  539. [ticket_ retain];
  540. [self waitForFetch];
  541. STAssertNil(fetchedObject_, @"fetchedObject_=%@", fetchedObject_);
  542. STAssertEquals([fetcherError_ code], (NSInteger)403,
  543. @"fetcherError_=%@", fetcherError_);
  544. // get back the captcha token and partial and full URLs from the error
  545. NSDictionary *userInfo = [fetcherError_ userInfo];
  546. NSString *captchaToken = [userInfo objectForKey:@"CaptchaToken"];
  547. NSString *captchaUrl = [userInfo objectForKey:@"CaptchaUrl"];
  548. NSString *captchaFullUrl = [userInfo objectForKey:@"CaptchaFullUrl"];
  549. STAssertEqualObjects(captchaToken, @"CapToken", @"bad captcha token");
  550. STAssertEqualObjects(captchaUrl, @"CapUrl", @"bad captcha relative url");
  551. STAssertTrue([captchaFullUrl hasSuffix:@"/accounts/CapUrl"], @"bad captcha full:%@", captchaFullUrl);
  552. STAssertEqualObjects([ticket_ userData], defaultUserData, @"userdata error");
  553. //
  554. // test: download feed only, good captcha provided
  555. //
  556. [self resetFetchResponse];
  557. [service_ setUserCredentialsWithUsername:@"myaccount2@mydomain.com"
  558. password:@"captcha"];
  559. [service_ setCaptchaToken:@"CapToken" captchaAnswer:@"good"];
  560. ticket_ = [service_ fetchFeedWithURL:authFeedURL
  561. feedClass:kGDataUseRegisteredClass
  562. delegate:self
  563. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  564. [ticket_ retain];
  565. [self waitForFetch];
  566. // get back the captcha token and partial and full URLs from the error
  567. STAssertEqualObjects([(GDataFeedSpreadsheet *)fetchedObject_ identifier],
  568. sheetID, @"fetching %@", feedURL);
  569. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  570. STAssertEqualObjects([ticket_ userData], defaultUserData, @"userdata error");
  571. //
  572. // test: download feed only, bad captcha provided
  573. //
  574. [self resetFetchResponse];
  575. [service_ setUserCredentialsWithUsername:@"myaccount3@mydomain.com"
  576. password:@"captcha"];
  577. [service_ setCaptchaToken:@"CapToken" captchaAnswer:@"bad"];
  578. ticket_ = [service_ fetchFeedWithURL:authFeedURL
  579. feedClass:kGDataUseRegisteredClass
  580. delegate:self
  581. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  582. [ticket_ retain];
  583. [self waitForFetch];
  584. STAssertNil(fetchedObject_, @"fetchedObject_=%@", fetchedObject_);
  585. STAssertEquals([fetcherError_ code], (NSInteger)403,
  586. @"fetcherError_=%@", fetcherError_);
  587. // get back the captcha token and partial and full URLs from the error
  588. userInfo = [fetcherError_ userInfo];
  589. captchaToken = [userInfo objectForKey:@"CaptchaToken"];
  590. captchaUrl = [userInfo objectForKey:@"CaptchaUrl"];
  591. captchaFullUrl = [userInfo objectForKey:@"CaptchaFullUrl"];
  592. STAssertEqualObjects(captchaToken, @"CapToken", @"bad captcha token");
  593. STAssertEqualObjects(captchaUrl, @"CapUrl", @"bad captcha relative url");
  594. STAssertTrue([captchaFullUrl hasSuffix:@"/accounts/CapUrl"], @"bad captcha full:%@", captchaFullUrl);
  595. STAssertEqualObjects([ticket_ userData], defaultUserData, @"userdata error");
  596. //
  597. // test: insert/download entry, successful auth
  598. //
  599. [self resetFetchResponse];
  600. [service_ setUserCredentialsWithUsername:@"myaccount@mydomain.com"
  601. password:@"good"];
  602. NSURL *authEntryURL = [self fileURLToTestFileName:@"EntrySpreadsheetCellTest1.xml.auth"];
  603. ticket_ = [service_ fetchEntryByInsertingEntry:[GDataEntrySpreadsheetCell entry]
  604. forFeedURL:authEntryURL
  605. delegate:self
  606. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  607. [ticket_ retain];
  608. [self waitForFetch];
  609. NSString *entryID = @"http://spreadsheets.google.com/feeds/cells/o04181601172097104111.497668944883620000/od6/private/full/R1C1";
  610. STAssertEqualObjects([(GDataEntrySpreadsheetCell *)fetchedObject_ identifier],
  611. entryID, @"updating %@", authEntryURL);
  612. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  613. STAssertEqualObjects([ticket_ userData], defaultUserData, @"userdata error");
  614. //
  615. // test: update/download entry, successful auth
  616. //
  617. [self resetFetchResponse];
  618. ticket_ = [service_ fetchEntryByUpdatingEntry:[GDataEntrySpreadsheetCell entry]
  619. forEntryURL:authEntryURL
  620. delegate:self
  621. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  622. [ticket_ retain];
  623. [self waitForFetch];
  624. STAssertEqualObjects([(GDataEntrySpreadsheetCell *)fetchedObject_ identifier],
  625. entryID, @"fetching %@", authFeedURL);
  626. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  627. STAssertEqualObjects([ticket_ userData], defaultUserData, @"userdata error");
  628. //
  629. // test: update/download streamed entry data with progress monitoring
  630. // and logging, successful auth, logging on
  631. //
  632. [self resetFetchResponse];
  633. [GTMHTTPFetcher setLoggingEnabled:YES];
  634. [GTMHTTPFetcher setLoggingDirectory:NSTemporaryDirectory()];
  635. // report the logging directory (the log file names depend on the process
  636. // launch time, but this at least lets us manually inspect the logs)
  637. NSLog(@"GDataServiceTest http logging set to %@",
  638. [GTMHTTPFetcher loggingDirectory]);
  639. GDataEntryPhoto *photoEntry = [GDataEntryPhoto photoEntry];
  640. NSImage *image = [NSImage imageNamed:@"NSApplicationIcon"];
  641. NSData *tiffData = [image TIFFRepresentation];
  642. STAssertTrue([tiffData length] > 0, @"failed to make tiff image");
  643. [photoEntry setPhotoData:tiffData];
  644. [photoEntry setPhotoMIMEType:@"image/tiff"];
  645. [photoEntry setUploadSlug:@"unit test photo.tif"];
  646. [photoEntry setTitleWithString:@"Unit Test Photo"];
  647. SEL progressSel = @selector(ticket:hasDeliveredByteCount:ofTotalByteCount:);
  648. [service_ setServiceUploadProgressSelector:progressSel];
  649. // note that the authEntryURL still points to a spreadsheet entry, so
  650. // spreadsheet XML is what will be returned, but we don't really care
  651. ticket_ = [service_ fetchEntryByUpdatingEntry:photoEntry
  652. forEntryURL:authEntryURL
  653. delegate:self
  654. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  655. [ticket_ retain];
  656. [self waitForFetch];
  657. STAssertEqualObjects([(GDataEntrySpreadsheetCell *)fetchedObject_ identifier],
  658. entryID, @"fetching %@", authFeedURL);
  659. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  660. STAssertEqualObjects([ticket_ userData], defaultUserData, @"userdata error");
  661. STAssertTrue(lastProgressDeliveredCount_ > 0, @"no byte delivery reported");
  662. STAssertEquals(lastProgressDeliveredCount_, lastProgressTotalCount_,
  663. @"unexpected byte delivery count");
  664. [GTMHTTPFetcher setLoggingEnabled:NO];
  665. [GTMHTTPFetcher setLoggingDirectory:nil];
  666. [service_ setServiceUploadProgressSelector:nil];
  667. //
  668. // test: delete entry with ETag, successful auth
  669. //
  670. [self resetFetchResponse];
  671. GDataEntrySpreadsheetCell *entryToDelete = [GDataEntrySpreadsheetCell entry];
  672. [entryToDelete addLink:[GDataLink linkWithRel:@"edit"
  673. type:nil
  674. href:[authFeedURL absoluteString]]];
  675. [entryToDelete setETag:@"A0MCQHs-fyp7ImA9WxVVF0Q."];
  676. ticket_ = [service_ deleteEntry:entryToDelete
  677. delegate:self
  678. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  679. [ticket_ retain];
  680. [self waitForFetch];
  681. STAssertNil(fetchedObject_, @"deleting %@ returned \n%@", authEntryURL, fetchedObject_);
  682. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  683. STAssertEqualObjects([ticket_ userData], defaultUserData, @"userdata error");
  684. //
  685. // test: delete resource, successful auth, using method override header
  686. //
  687. [self resetFetchResponse];
  688. [service_ setShouldUseMethodOverrideHeader:YES];
  689. ticket_ = [service_ deleteResourceURL:authEntryURL
  690. ETag:nil
  691. delegate:self
  692. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  693. [ticket_ retain];
  694. [self waitForFetch];
  695. STAssertNil(fetchedObject_, @"deleting %@ returned \n%@", authEntryURL, fetchedObject_);
  696. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  697. STAssertEqualObjects([ticket_ userData], defaultUserData, @"userdata error");
  698. //
  699. // test: fetch feed with authsub, successful
  700. //
  701. [self resetFetchResponse];
  702. [service_ setUserCredentialsWithUsername:nil
  703. password:nil];
  704. [service_ setAuthSubToken:@"GoodAuthSubToken"];
  705. ticket_ = [service_ fetchFeedWithURL:authSubFeedURL
  706. feedClass:kGDataUseRegisteredClass
  707. delegate:self
  708. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  709. [ticket_ retain];
  710. [self waitForFetch];
  711. STAssertEqualObjects([(GDataFeedSpreadsheet *)fetchedObject_ identifier],
  712. sheetID, @"fetching %@", authSubFeedURL);
  713. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  714. //
  715. // test: fetch feed with authsub, bad token
  716. //
  717. [self resetFetchResponse];
  718. [service_ setAuthSubToken:@"bogus"];
  719. ticket_ = [service_ fetchFeedWithURL:authSubFeedURL
  720. feedClass:kGDataUseRegisteredClass
  721. delegate:self
  722. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  723. [ticket_ retain];
  724. [self waitForFetch];
  725. STAssertNil(fetchedObject_, @"fetchedObject_=%@", fetchedObject_);
  726. STAssertEquals([fetcherError_ code], (NSInteger)401,
  727. @"fetcherError_=%@", fetcherError_);
  728. STAssertEqualObjects([ticket_ userData], defaultUserData, @"userdata error");
  729. STAssertEqualObjects([ticket_ propertyForKey:testPropertyKey],
  730. defaultPropertyValue, @"default property error");
  731. }
  732. // fetch callbacks
  733. - (void)ticket:(GDataServiceTicket *)ticket finishedWithObject:(GDataObject *)object error:(NSError *)error {
  734. STAssertEquals(ticket, ticket_, @"Got unexpected ticket");
  735. if (error == nil) {
  736. fetchedObject_ = [object retain]; // save the fetched object, if any
  737. } else {
  738. fetcherError_ = [error retain]; // save the error
  739. STAssertNil(object, @"Unexpected object in callback");
  740. }
  741. ++gFetchCounter;
  742. }
  743. - (void)ticket:(GDataServiceTicket *)ticket
  744. hasDeliveredByteCount:(unsigned long long)numberOfBytesRead
  745. ofTotalByteCount:(unsigned long long)dataLength {
  746. lastProgressDeliveredCount_ = numberOfBytesRead;
  747. lastProgressTotalCount_ = dataLength;
  748. }
  749. #pragma mark Retry fetch tests
  750. - (void)testRetryFetches {
  751. if (!isServerRunning_) return;
  752. // an ".auth" extension tells the server to require the success auth token,
  753. // but the server will ignore the .auth extension when looking for the file
  754. NSURL *feedStatusURL = [self fileURLToTestFileName:@"FeedSpreadsheetTest1.xml?status=503"];
  755. [service_ setIsServiceRetryEnabled:YES];
  756. //
  757. // test: retry until timeout, then expect failure to be passed to the callback
  758. //
  759. [service_ setServiceMaxRetryInterval:5.]; // retry intervals of 1, 2, 4
  760. [service_ setServiceRetrySelector:@selector(stopRetryTicket:willRetry:forError:)];
  761. ticket_ = (GDataServiceTicket *)
  762. [service_ fetchPublicFeedWithURL:feedStatusURL
  763. feedClass:kGDataUseRegisteredClass
  764. delegate:self
  765. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  766. [ticket_ retain];
  767. [ticket_ setUserData:[NSNumber numberWithInt:1000]]; // lots of retries
  768. [self waitForFetch];
  769. STAssertNil(fetchedObject_, @"obtained object unexpectedly");
  770. STAssertEquals([fetcherError_ code], (NSInteger)503,
  771. @"fetcherError_ should be 503, was %@", fetcherError_);
  772. STAssertEquals([[ticket_ objectFetcher] retryCount], (NSUInteger) 3,
  773. @"retry count should be 3, was %lu",
  774. (unsigned long) [[ticket_ objectFetcher] retryCount]);
  775. // check that the expected notifications happened for the object
  776. // fetches and the retries
  777. STAssertEquals(fetchStartedNotificationCount_, 4, @"start note missing");
  778. STAssertEquals(fetchStoppedNotificationCount_, 4, @"stopped note missing");
  779. STAssertEquals(retryDelayStartedNotificationCount_, 3, @"retry delay note missing");
  780. STAssertEquals(retryDelayStoppedNotificationCount_, 3, @"retry delay note missing");
  781. //
  782. // test: retry twice, then give up
  783. //
  784. [self resetFetchResponse];
  785. [service_ setServiceMaxRetryInterval:10.]; // retry intervals of 1, 2, 4, 8
  786. [service_ setServiceRetrySelector:@selector(stopRetryTicket:willRetry:forError:)];
  787. ticket_ = (GDataServiceTicket *)
  788. [service_ fetchPublicFeedWithURL:feedStatusURL
  789. feedClass:kGDataUseRegisteredClass
  790. delegate:self
  791. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  792. [ticket_ retain];
  793. // set userData to the number of retries allowed
  794. [ticket_ setUserData:[NSNumber numberWithInt:2]];
  795. [self waitForFetch];
  796. STAssertNil(fetchedObject_, @"obtained object unexpectedly");
  797. STAssertEquals([fetcherError_ code], (NSInteger)503,
  798. @"fetcherError_ should be 503, was %@", fetcherError_);
  799. STAssertEquals([[ticket_ objectFetcher] retryCount], (NSUInteger) 2,
  800. @"retry count should be 2, was %lu",
  801. (unsigned long) [[ticket_ objectFetcher] retryCount]);
  802. //
  803. // test: retry, making the request succeed on the first retry
  804. // by fixing the URL
  805. //
  806. [self resetFetchResponse];
  807. [service_ setServiceMaxRetryInterval:100.];
  808. [service_ setServiceRetrySelector:@selector(fixRequestRetryTicket:willRetry:forError:)];
  809. ticket_ = (GDataServiceTicket *)
  810. [service_ fetchPublicFeedWithURL:feedStatusURL
  811. feedClass:kGDataUseRegisteredClass
  812. delegate:self
  813. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  814. [ticket_ retain];
  815. [self waitForFetch];
  816. STAssertNotNil(fetchedObject_, @"should have obtained fetched object");
  817. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  818. STAssertEquals([[ticket_ objectFetcher] retryCount], (NSUInteger) 1,
  819. @"retry count should be 1, was %lu",
  820. (unsigned long) [[ticket_ objectFetcher] retryCount]);
  821. //
  822. // test: retry, making the request succeed on the first retry
  823. // by fixing the URL, and changing the credential before the retry,
  824. // which should not affect the retry
  825. //
  826. [self resetFetchResponse];
  827. [service_ setUserCredentialsWithUsername:@"myaccount@mydomain.com"
  828. password:@"mypassword"];
  829. NSURL *authStatusURL = [self fileURLToTestFileName:@"FeedSpreadsheetTest1.xml.auth?status=503"];
  830. ticket_ = [service_ fetchFeedWithURL:authStatusURL
  831. feedClass:kGDataUseRegisteredClass
  832. delegate:self
  833. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  834. [ticket_ retain];
  835. [service_ setUserCredentialsWithUsername:@"myaccount@mydomain.com"
  836. password:@"bad"];
  837. [self waitForFetch];
  838. STAssertNotNil(fetchedObject_, @"should have obtained fetched object");
  839. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  840. STAssertEquals([[ticket_ objectFetcher] retryCount], (NSUInteger) 1,
  841. @"retry count should be 1, was %lu",
  842. (unsigned long) [[ticket_ objectFetcher] retryCount]);
  843. [self resetFetchResponse];
  844. }
  845. -(BOOL)stopRetryTicket:(GDataServiceTicket *)ticket willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error {
  846. GTMHTTPFetcher *fetcher = [ticket currentFetcher];
  847. [fetcher setMinRetryInterval:1.0]; // force exact starting interval of 1.0 sec
  848. NSUInteger count = [fetcher retryCount];
  849. NSInteger allowedRetryCount = [[ticket userData] intValue];
  850. BOOL shouldRetry = ((NSInteger)count < allowedRetryCount);
  851. STAssertEquals([fetcher nextRetryInterval], pow(2.0, [fetcher retryCount]),
  852. @"unexpected next retry interval (expected %f, was %f)",
  853. pow(2.0, [fetcher retryCount]),
  854. [fetcher nextRetryInterval]);
  855. return shouldRetry;
  856. }
  857. -(BOOL)fixRequestRetryTicket:(GDataServiceTicket *)ticket willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error {
  858. GTMHTTPFetcher *fetcher = [ticket currentFetcher];
  859. [fetcher setMinRetryInterval:1.0]; // force exact starting interval of 1.0 sec
  860. STAssertEquals([fetcher nextRetryInterval], pow(2.0, [fetcher retryCount]),
  861. @"unexpected next retry interval (expected %f, was %f)",
  862. pow(2.0, [fetcher retryCount]),
  863. [fetcher nextRetryInterval]);
  864. // fix it - change the request to a URL which does not have a status value
  865. NSURL *authFeedStatusURL = [self fileURLToTestFileName:@"FeedSpreadsheetTest1.xml"];
  866. [fetcher setMutableRequest:[NSMutableURLRequest requestWithURL:authFeedStatusURL]];
  867. return YES; // do the retry fetch; it should succeed now
  868. }
  869. #pragma mark Upload tests
  870. - (NSData *)generatedUploadDataWithLength:(NSUInteger)length {
  871. // fill a data block with data
  872. NSMutableData *data = [NSMutableData dataWithLength:length];
  873. unsigned char *bytes = [data mutableBytes];
  874. for (NSUInteger idx = 0; idx < length; idx++) {
  875. bytes[idx] = (unsigned char)((idx + 1) % 256);
  876. }
  877. return data;
  878. }
  879. static NSString* const kPauseAtKey = @"pauseAt";
  880. static NSString* const kRetryAtKey = @"retryAt";
  881. static NSString* const kOriginalURLKey = @"origURL";
  882. - (void)testChunkedUpload {
  883. if (!isServerRunning_) return;
  884. NSData *bigData = [self generatedUploadDataWithLength:199000];
  885. NSData *smallData = [self generatedUploadDataWithLength:13];
  886. // write the big data into a temp file
  887. NSString *tempDir = NSTemporaryDirectory();
  888. NSString *bigFileName = @"GDataServiceTest_BigFile";
  889. NSString *bigFilePath = [tempDir stringByAppendingPathComponent:bigFileName];
  890. [bigData writeToFile:bigFilePath atomically:NO];
  891. NSFileHandle *bigFileHandle = [NSFileHandle fileHandleForReadingAtPath:bigFilePath];
  892. SEL progressSel = @selector(uploadTicket:hasDeliveredByteCount:ofTotalByteCount:);
  893. [service_ setServiceUploadProgressSelector:progressSel];
  894. SEL retrySel = @selector(uploadRetryTicket:willRetry:forError:);
  895. [service_ setServiceRetrySelector:retrySel];
  896. [service_ setIsServiceRetryEnabled:YES];
  897. [self resetFetchResponse];
  898. [service_ setServiceUploadChunkSize:75000];
  899. //
  900. // test a big upload using an NSFileHandle
  901. //
  902. // with chunk size 75000, for a data block of 199000 bytes, we expect to send
  903. // two 75000-byte chunks and then a 49000-byte final chunk
  904. // a ".location" tells the server to return a "Location" header with the
  905. // same path but an ".upload" suffix replacing the ".location" suffix
  906. NSURL *uploadURL = [self fileURLToTestFileName:@"EntrySpreadsheetCellTest1.xml.location"];
  907. [service_ setUserCredentialsWithUsername:@"myaccount@mydomain.com"
  908. password:@"good"];
  909. GDataEntrySpreadsheetCell *newEntry = [GDataEntrySpreadsheetCell entry];
  910. [newEntry setUploadFileHandle:bigFileHandle];
  911. [newEntry setUploadMIMEType:@"foo/bar"];
  912. ticket_ = [service_ fetchEntryByInsertingEntry:newEntry
  913. forFeedURL:uploadURL
  914. delegate:self
  915. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  916. [ticket_ retain];
  917. [self waitForFetch];
  918. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  919. // check that we got back the expected entry
  920. NSString *entryID = @"http://spreadsheets.google.com/feeds/cells/o04181601172097104111.497668944883620000/od6/private/full/R1C1";
  921. STAssertEqualObjects([(GDataEntrySpreadsheetCell *)fetchedObject_ identifier],
  922. entryID, @"uploading %@", uploadURL);
  923. // check the request of the final object fetcher to be sure we were uploading
  924. // chunks as expected
  925. GTMHTTPUploadFetcher *uploadFetcher = (GTMHTTPUploadFetcher *) [ticket_ objectFetcher];
  926. GTMHTTPFetcher *fetcher = [uploadFetcher activeFetcher];
  927. NSURLRequest *request = [fetcher mutableRequest];
  928. NSDictionary *reqHdrs = [request allHTTPHeaderFields];
  929. NSString *uploadReqURLStr = @"http://localhost:54579/EntrySpreadsheetCellTest1.xml.upload";
  930. NSString *contentLength = [reqHdrs objectForKey:@"Content-Length"];
  931. NSString *contentRange = [reqHdrs objectForKey:@"Content-Range"];
  932. STAssertEqualObjects([[request URL] absoluteString], uploadReqURLStr,
  933. @"upload request wrong");
  934. STAssertEqualObjects(contentLength, @"49000", @"content length");
  935. STAssertEqualObjects(contentRange, @"bytes 150000-198999/199000", @"range");
  936. [self resetFetchResponse];
  937. //
  938. // repeat the previous upload, using NSData
  939. //
  940. [newEntry setUploadData:bigData];
  941. [newEntry setUploadFileHandle:nil];
  942. ticket_ = [service_ fetchEntryByInsertingEntry:newEntry
  943. forFeedURL:uploadURL
  944. delegate:self
  945. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  946. [ticket_ retain];
  947. [self waitForFetch];
  948. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  949. // check that we got back the expected entry
  950. entryID = @"http://spreadsheets.google.com/feeds/cells/o04181601172097104111.497668944883620000/od6/private/full/R1C1";
  951. STAssertEqualObjects([(GDataEntrySpreadsheetCell *)fetchedObject_ identifier],
  952. entryID, @"uploading %@", uploadURL);
  953. // check the request of the final object fetcher to be sure we were uploading
  954. // chunks as expected
  955. uploadFetcher = (GTMHTTPUploadFetcher *) [ticket_ objectFetcher];
  956. fetcher = [uploadFetcher activeFetcher];
  957. request = [fetcher mutableRequest];
  958. reqHdrs = [request allHTTPHeaderFields];
  959. uploadReqURLStr = @"http://localhost:54579/EntrySpreadsheetCellTest1.xml.upload";
  960. contentLength = [reqHdrs objectForKey:@"Content-Length"];
  961. contentRange = [reqHdrs objectForKey:@"Content-Range"];
  962. STAssertEqualObjects([[request URL] absoluteString], uploadReqURLStr,
  963. @"upload request wrong");
  964. STAssertEqualObjects(contentLength, @"49000", @"content length");
  965. STAssertEqualObjects(contentRange, @"bytes 150000-198999/199000", @"range");
  966. [self resetFetchResponse];
  967. //
  968. // repeat the first upload, pausing after 20000 bytes
  969. //
  970. ticket_ = [service_ fetchEntryByInsertingEntry:newEntry
  971. forFeedURL:uploadURL
  972. delegate:self
  973. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  974. // add a property to the ticket that our progress callback will look for to
  975. // know when to pause and resume the upload
  976. [ticket_ setProperty:[NSNumber numberWithInt:20000]
  977. forKey:kPauseAtKey];
  978. [ticket_ retain];
  979. [self waitForFetch];
  980. STAssertEqualObjects([(GDataEntrySpreadsheetCell *)fetchedObject_ identifier],
  981. entryID, @"uploading %@", uploadURL);
  982. uploadFetcher = (GTMHTTPUploadFetcher *) [ticket_ objectFetcher];
  983. fetcher = [uploadFetcher activeFetcher];
  984. request = [fetcher mutableRequest];
  985. reqHdrs = [request allHTTPHeaderFields];
  986. contentLength = [reqHdrs objectForKey:@"Content-Length"];
  987. contentRange = [reqHdrs objectForKey:@"Content-Range"];
  988. STAssertEqualObjects([[request URL] absoluteString], uploadReqURLStr,
  989. @"upload request wrong");
  990. STAssertEqualObjects(contentLength, @"24499", @"content length");
  991. STAssertEqualObjects(contentRange, @"bytes 174501-198999/199000", @"range");
  992. [self resetFetchResponse];
  993. //
  994. // repeat the first upload, and after sending 70000 bytes the progress
  995. // callback will change the request URL for the next chunk fetch to make
  996. // it fail with a retryable status error
  997. //
  998. ticket_ = [service_ fetchEntryByInsertingEntry:newEntry
  999. forFeedURL:uploadURL
  1000. delegate:self
  1001. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  1002. // add a property to the ticket that our progress callback will use to
  1003. // force a retry after 70000 bytes are uploaded
  1004. [ticket_ setProperty:[NSNumber numberWithInt:70000]
  1005. forKey:kRetryAtKey];
  1006. [ticket_ retain];
  1007. [self waitForFetch];
  1008. STAssertEqualObjects([(GDataEntrySpreadsheetCell *)fetchedObject_ identifier],
  1009. entryID, @"uploading %@", uploadURL);
  1010. uploadFetcher = (GTMHTTPUploadFetcher *) [ticket_ objectFetcher];
  1011. fetcher = [uploadFetcher activeFetcher];
  1012. request = [fetcher mutableRequest];
  1013. reqHdrs = [request allHTTPHeaderFields];
  1014. contentLength = [reqHdrs objectForKey:@"Content-Length"];
  1015. contentRange = [reqHdrs objectForKey:@"Content-Range"];
  1016. STAssertEqualObjects([[request URL] absoluteString], uploadReqURLStr,
  1017. @"upload request wrong");
  1018. STAssertEqualObjects(contentLength, @"24499", @"content length");
  1019. STAssertEqualObjects(contentRange, @"bytes 174501-198999/199000", @"range");
  1020. [self resetFetchResponse];
  1021. //
  1022. // repeat the first upload, but uploading data only, without the entry XML
  1023. //
  1024. [newEntry setShouldUploadDataOnly:YES];
  1025. [newEntry setUploadSlug:@"filename slug.txt"];
  1026. ticket_ = [service_ fetchEntryByInsertingEntry:newEntry
  1027. forFeedURL:uploadURL
  1028. delegate:self
  1029. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  1030. [ticket_ retain];
  1031. [self waitForFetch];
  1032. STAssertEqualObjects([(GDataEntrySpreadsheetCell *)fetchedObject_ identifier],
  1033. entryID, @"uploading %@", uploadURL);
  1034. uploadFetcher = (GTMHTTPUploadFetcher *) [ticket_ objectFetcher];
  1035. fetcher = [uploadFetcher activeFetcher];
  1036. request = [fetcher mutableRequest];
  1037. reqHdrs = [request allHTTPHeaderFields];
  1038. contentLength = [reqHdrs objectForKey:@"Content-Length"];
  1039. contentRange = [reqHdrs objectForKey:@"Content-Range"];
  1040. STAssertEqualObjects([[request URL] absoluteString], uploadReqURLStr,
  1041. @"upload request wrong");
  1042. STAssertEqualObjects(contentLength, @"49000", @"content length");
  1043. STAssertEqualObjects(contentRange, @"bytes 150000-198999/199000", @"range");
  1044. [self resetFetchResponse];
  1045. //
  1046. // upload a small data block
  1047. //
  1048. [newEntry setUploadData:smallData];
  1049. [newEntry setShouldUploadDataOnly:NO];
  1050. ticket_ = [service_ fetchEntryByInsertingEntry:newEntry
  1051. forFeedURL:uploadURL
  1052. delegate:self
  1053. didFinishSelector:@selector(ticket:finishedWithObject:error:)];
  1054. [ticket_ retain];
  1055. [self waitForFetch];
  1056. STAssertNil(fetcherError_, @"fetcherError_=%@", fetcherError_);
  1057. // check that we got back the expected entry
  1058. STAssertEqualObjects([(GDataEntrySpreadsheetCell *)fetchedObject_ identifier],
  1059. entryID, @"uploading %@", uploadURL);
  1060. // check the request of the final (and only) object fetcher to be sure we
  1061. // were uploading chunks as expected
  1062. uploadFetcher = (GTMHTTPUploadFetcher *) [ticket_ objectFetcher];
  1063. fetcher = [uploadFetcher activeFetcher];
  1064. request = [fetcher mutableRequest];
  1065. reqHdrs = [request allHTTPHeaderFields];
  1066. contentLength = [reqHdrs objectForKey:@"Content-Length"];
  1067. contentRange = [reqHdrs objectForKey:@"Content-Range"];
  1068. STAssertEqualObjects([[request URL] absoluteString], uploadReqURLStr,
  1069. @"upload request wrong");
  1070. STAssertEqualObjects(contentLength, @"13", @"content length");
  1071. STAssertEqualObjects(contentRange, @"bytes 0-12/13", @"range");
  1072. [self resetFetchResponse];
  1073. [service_ setServiceUploadChunkSize:0];
  1074. [service_ setServiceUploadProgressSelector:NULL];
  1075. [service_ setServiceRetrySelector:NULL];
  1076. [[NSFileManager defaultManager] removeItemAtPath:bigFilePath error:NULL];
  1077. }
  1078. - (void)uploadTicket:(GDataServiceTicket *)ticket
  1079. hasDeliveredByteCount:(unsigned long long)numberOfBytesRead
  1080. ofTotalByteCount:(unsigned long long)dataLength {
  1081. lastProgressDeliveredCount_ = numberOfBytesRead;
  1082. lastProgressTotalCount_ = dataLength;
  1083. NSNumber *pauseAtNum = [ticket propertyForKey:kPauseAtKey];
  1084. if (pauseAtNum) {
  1085. int pauseAt = [pauseAtNum intValue];
  1086. if (pauseAt < (int)numberOfBytesRead) {
  1087. // we won't be paused again
  1088. [ticket setProperty:nil forKey:kPauseAtKey];
  1089. // we've reached the point where we should pause
  1090. //
  1091. // use perform selector to avoid pausing immediately, as that would nuke
  1092. // the chunk upload fetcher that is calling us back now
  1093. [ticket performSelector:@selector(pauseUpload) withObject:nil afterDelay:0.0];
  1094. [ticket performSelector:@selector(resumeUpload) withObject:nil afterDelay:1.0];
  1095. }
  1096. }
  1097. NSNumber *retryAtNum = [ticket propertyForKey:kRetryAtKey];
  1098. if (retryAtNum) {
  1099. int retryAt = [retryAtNum intValue];
  1100. if (retryAt < (int)numberOfBytesRead) {
  1101. // we won't be retrying again
  1102. [ticket setProperty:nil forKey:kRetryAtKey];
  1103. // save the current locationURL before appending &status=503
  1104. GTMHTTPUploadFetcher *uploadFetcher = (GTMHTTPUploadFetcher *) [ticket objectFetcher];
  1105. NSURL *origURL = [uploadFetcher locationURL];
  1106. [ticket setProperty:origURL forKey:kOriginalURLKey];
  1107. NSString *newURLStr = [[origURL absoluteString] stringByAppendingString:@"?status=503"];
  1108. [uploadFetcher setLocationURL:[NSURL URLWithString:newURLStr]];
  1109. }
  1110. }
  1111. }
  1112. -(BOOL)uploadRetryTicket:(GDataServiceTicket *)ticket willRetry:(BOOL)suggestedWillRetry forError:(NSError *)error {
  1113. // change this fetch's request (and future requests) to have the original URL,
  1114. // not the one with status=503 appended
  1115. NSURL *origURL = [ticket propertyForKey:kOriginalURLKey];
  1116. GTMHTTPUploadFetcher *uploadFetcher = (GTMHTTPUploadFetcher *) [ticket objectFetcher];
  1117. [[[uploadFetcher activeFetcher] mutableRequest] setURL:origURL];
  1118. [uploadFetcher setLocationURL:origURL];
  1119. [ticket setProperty:nil forKey:kOriginalURLKey];
  1120. return suggestedWillRetry; // do the retry fetch; it should succeed now
  1121. }
  1122. #pragma mark Standalone auth tests
  1123. - (void)resetAuthResponse {
  1124. [authToken_ release];
  1125. authToken_ = nil;
  1126. [authError_ release];
  1127. authError_ = nil;
  1128. }
  1129. - (void)waitForAuth {
  1130. // Give time for the auth to happen, but give up if
  1131. // 10 seconds elapse with no response
  1132. NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:10.0];
  1133. while ((!authToken_ && !authError_)
  1134. && [giveUpDate timeIntervalSinceNow] > 0) {
  1135. NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001];
  1136. [[NSRunLoop currentRunLoop] runUntilDate:stopDate];
  1137. }
  1138. }
  1139. - (void)testStandaloneServiceAuth {
  1140. if (!isServerRunning_) return;
  1141. // test successful auth
  1142. [service_ setUserCredentialsWithUsername:@"myaccount@mydomain.com"
  1143. password:@"mypassword"];
  1144. [service_ authenticateWithDelegate:self
  1145. didAuthenticateSelector:@selector(ticket:authenticatedWithError:)];
  1146. [self waitForAuth];
  1147. STAssertTrue([authToken_ length] > 0, @"auth token missing");
  1148. STAssertNil(authError_, @"unexpected auth error: %@", authError_);
  1149. [self resetAuthResponse];
  1150. // test unsuccessful auth
  1151. [service_ setUserCredentialsWithUsername:@"myaccount@mydomain.com"
  1152. password:@"bad"];
  1153. [service_ authenticateWithDelegate:self
  1154. didAuthenticateSelector:@selector(ticket:authenticatedWithError:)];
  1155. [self waitForAuth];
  1156. STAssertNil(authToken_, @"unexpected auth token: %@", authToken_);
  1157. STAssertNotNil(authError_, @"auth error missing");
  1158. [self resetAuthResponse];
  1159. }
  1160. - (void)ticket:(GDataServiceTicket *)ticket
  1161. authenticatedWithError:(NSError *)error {
  1162. authToken_ = [[[ticket service] authToken] retain];
  1163. authError_ = [error retain];
  1164. }
  1165. @end
  1166. @implementation MyGDataFeedSpreadsheetSurrogate
  1167. - (NSString *)mySurrogateFeedName {
  1168. return @"mySurrogateFeedNameBar";
  1169. }
  1170. @end
  1171. @implementation MyGDataEntrySpreadsheetSurrogate
  1172. - (NSString *)mySurrogateEntryName {
  1173. return @"mySurrogateEntryNameFoo";
  1174. }
  1175. @end
  1176. @implementation MyGDataLinkSurrogate
  1177. - (NSString *)mySurrogateLinkName {
  1178. return @"mySurrogateLinkNameBaz";
  1179. }
  1180. @end
  1181. #endif // !GDATA_SKIPSERVICETEST