/core/externals/update-engine/Core/KSUpdateCheckActionTest.m

http://macfuse.googlecode.com/ · Objective C · 428 lines · 304 code · 60 blank · 64 comment · 30 complexity · 0062d6dcaa7f06ea3508d2928db83779 MD5 · raw file

  1. // Copyright 2008 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. #import <SenTestingKit/SenTestingKit.h>
  15. #import "KSUpdateCheckAction.h"
  16. #import "KSActionConstants.h"
  17. #import "KSActionPipe.h"
  18. #import "KSActionProcessor.h"
  19. #import "KSExistenceChecker.h"
  20. #import "KSMockFetcherFactory.h"
  21. #import "KSPlistServer.h"
  22. #import "KSTicket.h"
  23. #import "KSUpdateInfo.h"
  24. @interface KSUpdateCheckActionTest : SenTestCase {
  25. @private
  26. KSActionProcessor *processor_;
  27. NSURL *url_;
  28. KSServer *splitServer_;
  29. KSServer *singleServer_;
  30. NSArray *twoTickets_;
  31. NSArray *lottaTickets_;
  32. // variables set by an action's callback for which we are the delegate
  33. int delegatedStatus_;
  34. NSData *delegatedData_;
  35. NSError *delegatedError_;
  36. }
  37. @end
  38. /* --------------------------------------------------------------- */
  39. // Helper classes for mocking.
  40. // A mock KSServer which creates one request (and needs one fetcher) for each
  41. // ticket. The request data (HTTPBody) is a string-based number for each
  42. // ticket; e.g. "0\0", "1\0". The resultsForResponse creates a dictionary with
  43. // the given data, reading the int from the request data.
  44. @interface KSSplitMockServer : KSServer
  45. @end
  46. // A mock KSServer which creates one request (and only one fetcher) for ALL
  47. // tickets. The request data (HTTPBody) is a string-based number which is the
  48. // count of tickets; e.g. "8\0". The resultsForResponse creates a count of
  49. // result dictionaries with a key of "NumberKey" and a value of the value from
  50. // the response.
  51. @interface KSSingleMockServer : KSServer
  52. @end
  53. /* --------------------------------------------------------------- */
  54. @implementation KSSplitMockServer
  55. // One request per ticket to create many fetchers
  56. - (NSArray *)requestsForTickets:(NSArray *)tickets {
  57. NSMutableArray *array = [NSMutableArray arrayWithCapacity:[tickets count]];
  58. for (int i = 0; i < [tickets count]; i++) {
  59. NSString *str = [NSString stringWithFormat:@"%d", i];
  60. NSData *data = [NSData dataWithBytes:[str UTF8String] length:[str length]];
  61. NSURL *url = [NSURL URLWithString:@"file://foo"];
  62. NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
  63. [request setHTTPMethod:@"POST"];
  64. [request setHTTPBody:data];
  65. [array addObject:request];
  66. }
  67. return array;
  68. }
  69. // One action, ever.
  70. - (NSArray *)updateInfosForResponse:(NSURLResponse *)response
  71. data:(NSData *)data
  72. outOfBandData:(NSDictionary **)oob {
  73. if (oob) *oob = nil;
  74. int x = 0;
  75. sscanf([data bytes], "%d", &x);
  76. NSDictionary *result = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:x]
  77. forKey:@"NumberKey"];
  78. NSArray *array = [NSArray arrayWithObject:result];
  79. return array;
  80. }
  81. @end
  82. /* --------------------------------------------------------------- */
  83. @implementation KSSingleMockServer
  84. // Only one request for all tickets
  85. - (NSArray *)requestsForTickets:(NSArray *)tickets {
  86. NSMutableArray *array = [NSMutableArray arrayWithCapacity:1];
  87. NSString *countString = [NSString stringWithFormat:@"%d", [tickets count]];
  88. NSData *data = [countString dataUsingEncoding:NSUTF8StringEncoding];
  89. NSURL *url = [NSURL URLWithString:@"file://foo"];
  90. NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
  91. [request setHTTPMethod:@"POST"];
  92. [request setHTTPBody:data];
  93. [array addObject:request];
  94. return array;
  95. }
  96. // N KSActions, where N is the number embedded in data.
  97. - (NSArray *)updateInfosForResponse:(NSURLResponse *)response
  98. data:(NSData *)data
  99. outOfBandData:(NSDictionary **)oob {
  100. if (oob) *oob = nil;
  101. int x = 0;
  102. sscanf([data bytes], "%d", &x);
  103. NSMutableArray *array = [NSMutableArray array];
  104. for (int count = 0; count < x; count++) {
  105. NSDictionary *result = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:count]
  106. forKey:@"NumberKey"];
  107. [array addObject:result];
  108. }
  109. return array;
  110. }
  111. @end
  112. /* --------------------------------------------------------------- */
  113. @implementation KSUpdateCheckActionTest
  114. - (NSArray *)createTickets:(int)count forServer:(KSServer *)server {
  115. NSMutableArray *tickets = [[[NSMutableArray alloc] init] autorelease];
  116. for (int x = 0; x < count; x++) {
  117. NSString *productid = [NSString stringWithFormat:@"{guid-%d}", x];
  118. KSExistenceChecker *xc = [KSExistenceChecker falseChecker];
  119. [tickets addObject:[KSTicket ticketWithProductID:productid
  120. version:@"1.0"
  121. existenceChecker:xc
  122. serverURL:url_]];
  123. }
  124. return tickets;
  125. }
  126. - (void)setUp {
  127. processor_ = [[KSActionProcessor alloc] init];
  128. url_ = [NSURL URLWithString:@"file://foo"];
  129. splitServer_ = [[KSSplitMockServer alloc] initWithURL:url_];
  130. singleServer_ = [[KSSingleMockServer alloc] initWithURL:url_];
  131. twoTickets_ = [[self createTickets:2 forServer:splitServer_] retain];
  132. lottaTickets_ = [[self createTickets:8 forServer:singleServer_] retain];
  133. }
  134. - (void)tearDown {
  135. [processor_ release];
  136. [splitServer_ release];
  137. [singleServer_ release];
  138. [twoTickets_ release];
  139. [lottaTickets_ release];
  140. [delegatedData_ release];
  141. [delegatedError_ release];
  142. }
  143. - (void)loopUntilEmpty {
  144. STAssertNotNil(processor_, nil);
  145. int count = 10;
  146. while (([processor_ isProcessing] == YES) && (count > 0)) {
  147. [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.2]];
  148. count--;
  149. }
  150. STAssertTrue(count > 0, nil); // make sure we didn't time out
  151. }
  152. - (void)confirmNoErrors {
  153. STAssertTrue(delegatedStatus_ == 0, nil);
  154. STAssertTrue(delegatedData_ == 0, nil);
  155. STAssertTrue(delegatedError_ == 0, nil);
  156. }
  157. - (void)runAction:(KSAction *)action {
  158. STAssertTrue([action isRunning] == NO, nil);
  159. [processor_ startProcessing];
  160. STAssertTrue([action isRunning] == YES, nil);
  161. [self loopUntilEmpty];
  162. STAssertTrue([action isRunning] == NO, nil);
  163. }
  164. - (void)testBasics {
  165. KSUpdateCheckAction *action = [KSUpdateCheckAction checkerWithServer:splitServer_
  166. tickets:twoTickets_];
  167. STAssertNotNil(action, nil);
  168. STAssertTrue([[action description] length] > 1, nil);
  169. STAssertTrue([action outstandingRequests] == 0, nil);
  170. STAssertTrue([action isRunning] == NO, nil);
  171. action = [KSUpdateCheckAction checkerWithServer:splitServer_
  172. tickets:nil];
  173. STAssertNil(action, nil);
  174. action = [KSUpdateCheckAction checkerWithServer:splitServer_
  175. tickets:[NSArray array]];
  176. STAssertNil(action, nil);
  177. action = [[[KSUpdateCheckAction alloc] init] autorelease];
  178. STAssertNil(action, nil);
  179. }
  180. // Uses a real GDataHTTPFetcher and a KSPlistServer to fetch a canned server
  181. // response, and ensures that the returned "result" dictionary from the
  182. // KSUpdateCheckAction matches what we're expecting.
  183. - (void)testServerResultDictionary {
  184. NSBundle *mainBundle = [NSBundle bundleForClass:[self class]];
  185. NSString *serverPlist = [mainBundle pathForResource:@"ServerSuccess"
  186. ofType:@"plist"];
  187. STAssertNotNil(serverPlist, nil);
  188. NSURL *serverURL = [NSURL fileURLWithPath:serverPlist];
  189. // Creates a KSServer for our "Plist server" that is really a text file
  190. // containing Plist-style XML
  191. KSServer *server = [KSPlistServer serverWithURL:serverURL];
  192. STAssertNotNil(server, nil);
  193. // Create a ticket that matches the one product in our plist server file
  194. KSTicket *ticket = [KSTicket ticketWithProductID:@"COM.GOOGLE.UPDATEENGINE.KSUPDATEENGINE_TEST"
  195. version:@"0"
  196. existenceChecker:[KSPathExistenceChecker checkerWithPath:@"/"]
  197. serverURL:serverURL];
  198. STAssertNotNil(ticket, nil);
  199. KSUpdateCheckAction *action =
  200. [KSUpdateCheckAction checkerWithServer:server
  201. tickets:[NSArray arrayWithObject:ticket]];
  202. STAssertNotNil(action, nil);
  203. STAssertTrue([action outstandingRequests] == 0, nil);
  204. STAssertTrue([action isRunning] == NO, nil);
  205. [processor_ enqueueAction:action];
  206. [self runAction:action];
  207. [self confirmNoErrors];
  208. NSDictionary *dict =
  209. [NSDictionary dictionaryWithObjectsAndKeys:
  210. @"TyWAiay1UCIV0gqGbfjF4R009mg=", kServerCodeHash,
  211. @"TyWAiay1UCIV0gqGbfjF4R009mg=", @"Hash",
  212. [NSNumber numberWithInt:69042], kServerCodeSize,
  213. @"69042", @"Size",
  214. [NSURL URLWithString:@"file:///tmp/Test-SUCCESS.dmg"], kServerCodebaseURL,
  215. @"file:///tmp/Test-SUCCESS.dmg", @"Codebase",
  216. @"COM.GOOGLE.UPDATEENGINE.KSUPDATEENGINE_TEST", kServerProductID,
  217. @"COM.GOOGLE.UPDATEENGINE.KSUPDATEENGINE_TEST", @"ProductID",
  218. @"TRUEPREDICATE", @"Predicate",
  219. nil];
  220. // This is what the KSOutOfBandDataAction emits.
  221. NSArray *updateInfos = [NSArray arrayWithObject:dict];
  222. NSDictionary *expect =
  223. [NSDictionary dictionaryWithObjectsAndKeys:
  224. serverURL, KSActionServerURLKey,
  225. updateInfos, KSActionUpdateInfosKey,
  226. nil];
  227. STAssertEqualObjects([[action outPipe] contents], expect, nil);
  228. }
  229. - (id)resultsFromMockTestWithServer:(KSServer *)server tickets:(NSArray *)tickets {
  230. NSString *bytes = [NSString stringWithFormat:@"%d", [tickets count]];
  231. NSData *data = [NSData dataWithBytes:[bytes UTF8String] length:[bytes length]];
  232. KSFetcherFactory *factory = [KSMockFetcherFactory alwaysFinishWithData:data];
  233. KSUpdateCheckAction *action = [[[KSUpdateCheckAction alloc]
  234. initWithFetcherFactory:factory
  235. server:server
  236. tickets:tickets] autorelease];
  237. STAssertNotNil(data, nil);
  238. STAssertNotNil(factory, nil);
  239. STAssertNotNil(action, nil);
  240. [processor_ enqueueAction:action];
  241. [self runAction:action];
  242. [self confirmNoErrors];
  243. return [[action outPipe] contents];
  244. }
  245. // Lots of mocks in this one which hide a bit of complexity. The fetchers from
  246. // the fetcher factory used here always claims to finish correctly, returning
  247. // the data (as "results") passed into the factory. The splitServer_ is a
  248. // KSSplitServer, which has a seperate fetcher for each ticket ("split"). A
  249. // KSSplitServer creates a string-encoded number (as NSData) for each ticket as
  250. // requests (ignored by the fetcher). Since the server uses the mock fetcher,
  251. // the net result here is that we get a dictionary with "NumberKey" => X, where
  252. // X is the number embedded in the data passed in to the factory (e.g. "2" for
  253. // two tickets).
  254. //
  255. // In short, 2 tickets --> 2 fetchers --> 2 (exactly the same) results
  256. - (void)testSplitServer {
  257. NSDictionary *results = [self resultsFromMockTestWithServer:splitServer_
  258. tickets:twoTickets_];
  259. NSArray *updateInfos = [results objectForKey:KSActionUpdateInfosKey];
  260. STAssertTrue([updateInfos count] == 1, nil);
  261. NSDictionary *expect =
  262. [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:2]
  263. forKey:@"NumberKey"];
  264. STAssertEqualObjects([updateInfos objectAtIndex:0], expect, nil);
  265. }
  266. // Similar to testSplitServer above, but we only use one fetcher for several
  267. // tickets. (The fetcher was specified on server creation.) Unlike the
  268. // KSSplitServer, the KSSingleServer creates a result dictionary with a value of
  269. // X, where X increments from 0 to to the value in the data passed into the
  270. // factory (which is the number of tickets).
  271. //
  272. // In short, 8 tickets --> 1 fetcher --> 8 (unique) actions.
  273. - (void)testSingleFetcher {
  274. NSDictionary *results = [self resultsFromMockTestWithServer:singleServer_
  275. tickets:lottaTickets_];
  276. NSArray *updateInfos = [results objectForKey:KSActionUpdateInfosKey];
  277. STAssertTrue([updateInfos count] == [lottaTickets_ count], nil);
  278. for (int i = 0; i < [updateInfos count]; i++) {
  279. NSDictionary *expect =
  280. [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:i]
  281. forKey:@"NumberKey"];
  282. STAssertTrue([[updateInfos objectAtIndex:i] isEqual:expect], nil);
  283. }
  284. }
  285. // Test termination of a KSUpdateCheckAction.
  286. - (void)testTermination {
  287. NSData *data = [NSData dataWithBytes:"hi" length:2];
  288. KSFetcherFactory *factory = [KSMockFetcherFactory alwaysFinishWithData:data];
  289. KSUpdateCheckAction *action = [[[KSUpdateCheckAction alloc]
  290. initWithFetcherFactory:factory
  291. server:splitServer_
  292. tickets:twoTickets_] autorelease];
  293. STAssertNotNil(data, nil);
  294. STAssertNotNil(factory, nil);
  295. STAssertNotNil(action, nil);
  296. [processor_ enqueueAction:action];
  297. // can't use runAction routine; we don't want to run the runloop.
  298. STAssertTrue([processor_ actionsCompleted] == 0, nil);
  299. STAssertTrue([action isRunning] == NO, nil);
  300. STAssertTrue([action outstandingRequests] == 0, nil);
  301. [processor_ startProcessing];
  302. STAssertTrue([action isRunning] == YES, nil);
  303. STAssertTrue([action outstandingRequests] > 0, nil);
  304. // intentionally no running of the run loop here so this action
  305. // can't finish. (If it finished we couldn't cancel it!)
  306. [self confirmNoErrors];
  307. [processor_ stopProcessing]; // should call terminateAction
  308. STAssertTrue([action isRunning] == NO, nil);
  309. STAssertTrue([action outstandingRequests] == 0, nil);
  310. STAssertTrue([processor_ actionsCompleted] == 0, nil);
  311. }
  312. // Make sure bad input (tickets point to different servers) gets caught.
  313. - (void)testBadTickets {
  314. NSMutableArray *mixedTickets = [NSMutableArray array];
  315. [mixedTickets addObjectsFromArray:twoTickets_];
  316. NSString *productid = [NSString stringWithFormat:@"{guid-%d}", 102];
  317. KSExistenceChecker *xc = [KSExistenceChecker falseChecker];
  318. NSURL *altURL = [NSURL URLWithString:@"file://foo/alt/bar"];
  319. [mixedTickets addObject:[KSTicket ticketWithProductID:productid
  320. version:@"1.0"
  321. existenceChecker:xc
  322. serverURL:altURL]];
  323. NSData *data = [NSData dataWithBytes:"hi" length:2];
  324. KSFetcherFactory *factory = [KSMockFetcherFactory alwaysFinishWithData:data];
  325. STAssertNotNil(data, nil);
  326. STAssertNotNil(factory, nil);
  327. KSUpdateCheckAction *action1 = [[[KSUpdateCheckAction alloc]
  328. initWithFetcherFactory:factory
  329. server:splitServer_
  330. tickets:mixedTickets]
  331. autorelease];
  332. STAssertNil(action1, nil);
  333. KSUpdateCheckAction *action2 = [[[KSUpdateCheckAction alloc]
  334. initWithFetcherFactory:factory
  335. server:singleServer_
  336. tickets:mixedTickets]
  337. autorelease];
  338. STAssertNil(action2, nil);
  339. }
  340. // Again, a funny fetcher factory which is supposed to fail
  341. - (void)testFailedWithError {
  342. NSError *err = [NSError errorWithDomain:@"domain" code:55789 userInfo:nil];
  343. KSFetcherFactory *factory = [KSMockFetcherFactory alwaysFailWithError:err];
  344. KSUpdateCheckAction *action = [[[KSUpdateCheckAction alloc]
  345. initWithFetcherFactory:factory
  346. server:splitServer_
  347. tickets:twoTickets_] autorelease];
  348. STAssertNotNil(err, nil);
  349. STAssertNotNil(factory, nil);
  350. STAssertNotNil(action, nil);
  351. [action setDelegate:self];
  352. [processor_ enqueueAction:action];
  353. [self confirmNoErrors];
  354. [self runAction:action];
  355. // make sure the errors are exactly what we expected
  356. STAssertTrue(delegatedStatus_ == 0, nil);
  357. STAssertTrue(delegatedData_ == 0, nil);
  358. STAssertTrue([delegatedError_ isEqual:err], nil);
  359. }
  360. - (void)fetcher:(GDataHTTPFetcher *)fetcher failedWithStatus:(int)status
  361. data:(NSData *)data {
  362. delegatedStatus_ = status;
  363. delegatedData_ = [data retain];
  364. }
  365. - (void)fetcher:(GDataHTTPFetcher *)fetcher failedWithError:(NSError *)error {
  366. delegatedError_ = [error retain];
  367. }
  368. @end