FIRInstanceIDTest.m 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022
  1. /*
  2. * Copyright 2019 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <XCTest/XCTest.h>
  17. #import <FirebaseCore/FIRAppInternal.h>
  18. #import <FirebaseCore/FIROptionsInternal.h>
  19. #import <OCMock/OCMock.h>
  20. #import "Firebase/InstanceID/FIRInstanceID+Testing.h"
  21. #import "Firebase/InstanceID/FIRInstanceIDAuthService.h"
  22. #import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h"
  23. #import "Firebase/InstanceID/FIRInstanceIDConstants.h"
  24. #import "Firebase/InstanceID/FIRInstanceIDKeyPair.h"
  25. #import "Firebase/InstanceID/FIRInstanceIDTokenInfo.h"
  26. #import "Firebase/InstanceID/FIRInstanceIDTokenManager.h"
  27. #import "Firebase/InstanceID/FIRInstanceIDUtilities.h"
  28. #import "Firebase/InstanceID/NSError+FIRInstanceID.h"
  29. static NSString *const kFakeIID = @"12345678";
  30. static NSString *const kFakeAPNSToken = @"this is a fake apns token";
  31. static NSString *const kAuthorizedEntity = @"test-audience";
  32. static NSString *const kScope = @"test-scope";
  33. static NSString *const kToken = @"test-token";
  34. static FIRInstanceIDTokenInfo *sTokenInfo;
  35. // Faking checkin calls
  36. static NSString *const kDeviceAuthId = @"device-id";
  37. static NSString *const kSecretToken = @"secret-token";
  38. static NSString *const kDigest = @"com.google.digest";
  39. static NSString *const kVersionInfo = @"1.0";
  40. // FIRApp configuration.
  41. static NSString *const kGCMSenderID = @"correct_gcm_sender_id";
  42. static NSString *const kGoogleAppID = @"1:123:ios:123abc";
  43. @interface FIRInstanceID (ExposedForTest)
  44. - (NSInteger)retryIntervalToFetchDefaultToken;
  45. - (BOOL)isFCMAutoInitEnabled;
  46. - (void)didCompleteConfigure;
  47. - (NSString *)cachedTokenIfAvailable;
  48. - (void)deleteIdentityWithHandler:(FIRInstanceIDDeleteHandler)handler;
  49. + (FIRInstanceID *)instanceIDForTests;
  50. @end
  51. @interface FIRInstanceIDTest : XCTestCase
  52. @property(nonatomic, readwrite, assign) BOOL hasCheckinInfo;
  53. @property(nonatomic, readwrite, strong) FIRInstanceID *instanceID;
  54. @property(nonatomic, readwrite, strong) id mockInstanceID;
  55. @property(nonatomic, readwrite, strong) id mockTokenManager;
  56. @property(nonatomic, readwrite, strong) id mockKeyPairStore;
  57. @property(nonatomic, readwrite, strong) id mockAuthService;
  58. @property(nonatomic, readwrite, strong) id<NSObject> tokenRefreshNotificationObserver;
  59. @property(nonatomic, readwrite, copy) FIRInstanceIDTokenHandler newTokenCompletion;
  60. @property(nonatomic, readwrite, copy) FIRInstanceIDDeleteTokenHandler deleteTokenCompletion;
  61. @end
  62. @implementation FIRInstanceIDTest
  63. - (void)setUp {
  64. [super setUp];
  65. _instanceID = [[FIRInstanceID alloc] initPrivately];
  66. [_instanceID start];
  67. if (!sTokenInfo) {
  68. sTokenInfo = [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:kAuthorizedEntity
  69. scope:kScope
  70. token:kToken
  71. appVersion:nil
  72. firebaseAppID:nil];
  73. sTokenInfo.cacheTime = [NSDate date];
  74. }
  75. [self mockInstanceIDObjects];
  76. }
  77. - (void)tearDown {
  78. [[NSNotificationCenter defaultCenter] removeObserver:self.tokenRefreshNotificationObserver];
  79. self.instanceID = nil;
  80. self.mockTokenManager = nil;
  81. self.mockInstanceID = nil;
  82. [super tearDown];
  83. }
  84. - (void)mockInstanceIDObjects {
  85. // Mock that we have valid checkin info. Individual tests can override this.
  86. self.hasCheckinInfo = YES;
  87. self.mockAuthService = OCMClassMock([FIRInstanceIDAuthService class]);
  88. [[[self.mockAuthService stub] andDo:^(NSInvocation *invocation) {
  89. [invocation setReturnValue:&_hasCheckinInfo];
  90. }] hasValidCheckinInfo];
  91. self.mockTokenManager = OCMClassMock([FIRInstanceIDTokenManager class]);
  92. [[[self.mockTokenManager stub] andReturn:self.mockAuthService] authService];
  93. self.mockKeyPairStore = OCMClassMock([FIRInstanceIDKeyPairStore class]);
  94. _instanceID.fcmSenderID = kAuthorizedEntity;
  95. self.mockInstanceID = OCMPartialMock(_instanceID);
  96. [self.mockInstanceID setTokenManager:self.mockTokenManager];
  97. [self.mockInstanceID setKeyPairStore:self.mockKeyPairStore];
  98. id instanceIDClassMock = OCMClassMock([FIRInstanceID class]);
  99. OCMStub(ClassMethod([instanceIDClassMock minIntervalForDefaultTokenRetry])).andReturn(2);
  100. OCMStub(ClassMethod([instanceIDClassMock maxRetryIntervalForDefaultTokenInSeconds]))
  101. .andReturn(10);
  102. }
  103. /**
  104. * Tests that the FIRInstanceID's sharedInstance class method produces an instance of
  105. * FIRInstanceID with an associated FIRInstanceIDTokenManager.
  106. */
  107. - (void)testSharedInstance {
  108. // The shared instance should be `nil` before the app is configured.
  109. XCTAssertNil([FIRInstanceID instanceID]);
  110. // The shared instance relies on the default app being configured. Configure it.
  111. FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID
  112. GCMSenderID:kGCMSenderID];
  113. [FIRApp configureWithName:kFIRDefaultAppName options:options];
  114. FIRInstanceID *instanceID = [FIRInstanceID instanceID];
  115. XCTAssertNotNil(instanceID);
  116. XCTAssertNotNil(instanceID.tokenManager);
  117. // Ensure a second call returns the same instance as the first.
  118. FIRInstanceID *secondInstanceID = [FIRInstanceID instanceID];
  119. XCTAssertEqualObjects(instanceID, secondInstanceID);
  120. // Reset the default app for the next test.
  121. [FIRApp resetApps];
  122. }
  123. - (void)testFCMAutoInitEnabled {
  124. XCTAssertFalse([_instanceID isFCMAutoInitEnabled],
  125. @"When FCM is not available, FCM Auto Init Enabled should be NO.");
  126. }
  127. - (void)testTokenShouldBeRefreshedIfCacheTokenNeedsToBeRefreshed {
  128. [[[self.mockInstanceID stub] andReturn:kToken] cachedTokenIfAvailable];
  129. [[[self.mockTokenManager stub] andReturnValue:@(YES)] checkForTokenRefreshPolicy];
  130. [[[self.mockInstanceID stub] andDo:^(NSInvocation *invocation){
  131. }] tokenWithAuthorizedEntity:[OCMArg any]
  132. scope:[OCMArg any]
  133. options:[OCMArg any]
  134. handler:[OCMArg any]];
  135. [self.mockInstanceID didCompleteConfigure];
  136. OCMVerify([self.mockInstanceID fetchDefaultToken]);
  137. XCTAssertEqualObjects([self.mockInstanceID token], kToken);
  138. }
  139. - (void)testTokenShouldBeRefreshedIfNoCacheTokenButAutoInitAllowed {
  140. [[[self.mockInstanceID stub] andReturn:nil] cachedTokenIfAvailable];
  141. [[[self.mockInstanceID stub] andReturnValue:@(YES)] isFCMAutoInitEnabled];
  142. [[[self.mockInstanceID stub] andDo:^(NSInvocation *invocation){
  143. }] tokenWithAuthorizedEntity:[OCMArg any]
  144. scope:[OCMArg any]
  145. options:[OCMArg any]
  146. handler:[OCMArg any]];
  147. [self.mockInstanceID didCompleteConfigure];
  148. OCMVerify([self.mockInstanceID fetchDefaultToken]);
  149. }
  150. - (void)testTokenIsDeletedAlongWithIdentity {
  151. [[[self.mockInstanceID stub] andReturnValue:@(YES)] isFCMAutoInitEnabled];
  152. [[[self.mockInstanceID stub] andDo:^(NSInvocation *invocation){
  153. }] tokenWithAuthorizedEntity:[OCMArg any]
  154. scope:[OCMArg any]
  155. options:[OCMArg any]
  156. handler:[OCMArg any]];
  157. [self.mockInstanceID deleteIdentityWithHandler:^(NSError *_Nullable error) {
  158. XCTAssertNil([self.mockInstanceID token]);
  159. }];
  160. }
  161. - (void)testTokenIsFetchedDuringIIDGeneration {
  162. XCTestExpectation *tokenExpectation = [self
  163. expectationWithDescription:@"Token is refreshed when getID is called to avoid IID conflict."];
  164. NSError *error;
  165. [[[self.mockKeyPairStore stub] andReturn:kFakeIID] appIdentityWithError:[OCMArg setTo:error]];
  166. [self.mockInstanceID getIDWithHandler:^(NSString *identity, NSError *error) {
  167. XCTAssertNotNil(identity);
  168. XCTAssertEqual(identity, kFakeIID);
  169. OCMVerify([self.mockInstanceID token]);
  170. [tokenExpectation fulfill];
  171. }];
  172. [self waitForExpectationsWithTimeout:0.1
  173. handler:^(NSError *error) {
  174. XCTAssertNil(error);
  175. }];
  176. }
  177. /**
  178. * Tests that when a new InstanceID token is successfully produced,
  179. * the callback is invoked with a token that is not an empty string and with no error.
  180. */
  181. - (void)testNewTokenSuccess {
  182. XCTestExpectation *tokenExpectation =
  183. [self expectationWithDescription:@"New token handler invoked."];
  184. NSString *APNSKey = kFIRInstanceIDTokenOptionsAPNSKey;
  185. NSString *serverKey = kFIRInstanceIDTokenOptionsAPNSIsSandboxKey;
  186. [self stubKeyPairStoreToReturnValidKeypair];
  187. [self mockAuthServiceToAlwaysReturnValidCheckin];
  188. NSData *fakeAPNSDeviceToken = [kFakeAPNSToken dataUsingEncoding:NSUTF8StringEncoding];
  189. BOOL isSandbox = YES;
  190. NSDictionary *tokenOptions = @{
  191. APNSKey : fakeAPNSDeviceToken,
  192. serverKey : @(isSandbox),
  193. };
  194. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  195. self.newTokenCompletion(kToken, nil);
  196. }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  197. scope:kScope
  198. keyPair:[OCMArg any]
  199. options:[OCMArg checkWithBlock:^BOOL(id obj) {
  200. NSDictionary *options = (NSDictionary *)obj;
  201. XCTAssertTrue([options[APNSKey] isEqual:fakeAPNSDeviceToken]);
  202. XCTAssertTrue([options[serverKey] isEqual:@(isSandbox)]);
  203. return YES;
  204. }]
  205. handler:[OCMArg checkWithBlock:^BOOL(id obj) {
  206. self.newTokenCompletion = obj;
  207. return obj != nil;
  208. }]];
  209. [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity
  210. scope:kScope
  211. options:tokenOptions
  212. handler:^(NSString *token, NSError *error) {
  213. XCTAssertNotNil(token);
  214. XCTAssertGreaterThan(token.length, 0);
  215. XCTAssertNil(error);
  216. [tokenExpectation fulfill];
  217. }];
  218. [self waitForExpectationsWithTimeout:1
  219. handler:^(NSError *error) {
  220. XCTAssertNil(error);
  221. }];
  222. }
  223. /**
  224. * Get Token should fail if we do not have valid checkin info and are unable to
  225. * retreive one.
  226. */
  227. - (void)testNewTokenCheckinFailure {
  228. self.hasCheckinInfo = NO;
  229. __block FIRInstanceIDDeviceCheckinCompletion checkinHandler;
  230. [[[self.mockAuthService stub] andDo:^(NSInvocation *invocation) {
  231. if (checkinHandler) {
  232. FIRInstanceIDErrorCode code = kFIRInstanceIDErrorCodeUnknown;
  233. NSError *error = [NSError errorWithFIRInstanceIDErrorCode:code];
  234. checkinHandler(nil, error);
  235. }
  236. }] fetchCheckinInfoWithHandler:[OCMArg checkWithBlock:^BOOL(id obj) {
  237. return (checkinHandler = obj) != nil;
  238. }]];
  239. XCTestExpectation *tokenExpectation =
  240. [self expectationWithDescription:@"New token handler invoked."];
  241. NSDictionary *tokenOptions = @{
  242. kFIRInstanceIDTokenOptionsAPNSKey : [kFakeAPNSToken dataUsingEncoding:NSUTF8StringEncoding],
  243. kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(YES),
  244. };
  245. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  246. self.newTokenCompletion(kToken, nil);
  247. }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  248. scope:kScope
  249. keyPair:[OCMArg any]
  250. options:[OCMArg any]
  251. handler:[OCMArg checkWithBlock:^BOOL(id obj) {
  252. self.newTokenCompletion = obj;
  253. return obj != nil;
  254. }]];
  255. [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity
  256. scope:kScope
  257. options:tokenOptions
  258. handler:^(NSString *token, NSError *error) {
  259. XCTAssertNil(token);
  260. XCTAssertNotNil(error);
  261. [tokenExpectation fulfill];
  262. }];
  263. [self waitForExpectationsWithTimeout:60.0
  264. handler:^(NSError *error) {
  265. XCTAssertNil(error);
  266. }];
  267. }
  268. /**
  269. * Get token with no valid checkin should wait for any existing checkin operation to finish.
  270. * If the checkin succeeds within a stipulated amount of time period getting the token should
  271. * also succeed.
  272. */
  273. - (void)testNewTokenSuccessAfterWaiting {
  274. self.hasCheckinInfo = NO;
  275. __block FIRInstanceIDDeviceCheckinCompletion checkinHandler;
  276. [[[self.mockAuthService stub] andDo:^(NSInvocation *invocation) {
  277. if (checkinHandler) {
  278. FIRInstanceIDErrorCode code = kFIRInstanceIDErrorCodeUnknown;
  279. NSError *error = [NSError errorWithFIRInstanceIDErrorCode:code];
  280. checkinHandler(nil, error);
  281. }
  282. }] fetchCheckinInfoWithHandler:[OCMArg checkWithBlock:^BOOL(id obj) {
  283. return (checkinHandler = obj) != nil;
  284. }]];
  285. XCTestExpectation *tokenExpectation =
  286. [self expectationWithDescription:@"New token handler invoked."];
  287. NSDictionary *tokenOptions = @{
  288. kFIRInstanceIDTokenOptionsAPNSKey : [kFakeAPNSToken dataUsingEncoding:NSUTF8StringEncoding],
  289. kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(YES),
  290. };
  291. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  292. self.newTokenCompletion(kToken, nil);
  293. }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  294. scope:kScope
  295. keyPair:[OCMArg any]
  296. options:[OCMArg any]
  297. handler:[OCMArg checkWithBlock:^BOOL(id obj) {
  298. self.newTokenCompletion = obj;
  299. return obj != nil;
  300. }]];
  301. [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity
  302. scope:kScope
  303. options:tokenOptions
  304. handler:^(NSString *token, NSError *error) {
  305. XCTAssertNil(token);
  306. XCTAssertNotNil(error);
  307. [tokenExpectation fulfill];
  308. }];
  309. [self waitForExpectationsWithTimeout:60.0
  310. handler:^(NSError *error) {
  311. XCTAssertNil(error);
  312. }];
  313. }
  314. /**
  315. * Test that the prod APNS token is correctly prefixed with "prod".
  316. */
  317. - (void)testAPNSTokenIsPrefixedCorrectlyForServerType {
  318. NSString *APNSKey = kFIRInstanceIDTokenOptionsAPNSKey;
  319. NSString *serverTypeKey = kFIRInstanceIDTokenOptionsAPNSIsSandboxKey;
  320. NSDictionary *prodTokenOptions = @{
  321. APNSKey : [kFakeAPNSToken dataUsingEncoding:NSUTF8StringEncoding],
  322. serverTypeKey : @(NO),
  323. };
  324. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation){
  325. }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  326. scope:kScope
  327. keyPair:[OCMArg any]
  328. options:[OCMArg checkWithBlock:^BOOL(id obj) {
  329. NSDictionary *options = (NSDictionary *)obj;
  330. XCTAssertTrue([options[APNSKey] hasPrefix:@"p_"]);
  331. XCTAssertFalse([options[serverTypeKey] boolValue]);
  332. return YES;
  333. }]
  334. handler:OCMOCK_ANY];
  335. [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity
  336. scope:kScope
  337. options:prodTokenOptions
  338. handler:^(NSString *token, NSError *error){
  339. }];
  340. }
  341. /**
  342. * Tests that when there is a failure in producing a new InstanceID token,
  343. * the callback is invoked with an error and a nil token.
  344. */
  345. - (void)testNewTokenFailure {
  346. XCTestExpectation *tokenExpectation =
  347. [self expectationWithDescription:@"New token handler invoked."];
  348. NSDictionary *tokenOptions = [NSDictionary dictionary];
  349. [self mockAuthServiceToAlwaysReturnValidCheckin];
  350. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  351. NSError *someError = [[NSError alloc] initWithDomain:@"InstanceIDUnitTest" code:0 userInfo:nil];
  352. self.newTokenCompletion(nil, someError);
  353. }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  354. scope:kScope
  355. keyPair:[OCMArg any]
  356. options:tokenOptions
  357. handler:[OCMArg checkWithBlock:^BOOL(id obj) {
  358. self.newTokenCompletion = obj;
  359. return obj != nil;
  360. }]];
  361. [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity
  362. scope:kScope
  363. options:tokenOptions
  364. handler:^(NSString *token, NSError *error) {
  365. XCTAssertNil(token);
  366. XCTAssertNotNil(error);
  367. [tokenExpectation fulfill];
  368. }];
  369. [self waitForExpectationsWithTimeout:1
  370. handler:^(NSError *error) {
  371. XCTAssertNil(error);
  372. }];
  373. }
  374. /**
  375. * Tests that when a token is deleted successfully, the callback is invoked with no error.
  376. */
  377. - (void)testDeleteTokenSuccess {
  378. XCTestExpectation *deleteExpectation =
  379. [self expectationWithDescription:@"Delete handler invoked."];
  380. [self stubKeyPairStoreToReturnValidKeypair];
  381. [self mockAuthServiceToAlwaysReturnValidCheckin];
  382. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  383. #pragma clang diagnostic push
  384. #pragma clang diagnostic ignored "-Wnonnull"
  385. self.deleteTokenCompletion(nil);
  386. #pragma clang diagnostic pop
  387. }] deleteTokenWithAuthorizedEntity:kAuthorizedEntity
  388. scope:kScope
  389. keyPair:[OCMArg any]
  390. handler:[OCMArg checkWithBlock:^BOOL(id obj) {
  391. self.deleteTokenCompletion = obj;
  392. return obj != nil;
  393. }]];
  394. [self.instanceID deleteTokenWithAuthorizedEntity:kAuthorizedEntity
  395. scope:kScope
  396. handler:^(NSError *error) {
  397. XCTAssertNil(error);
  398. [deleteExpectation fulfill];
  399. }];
  400. [self waitForExpectationsWithTimeout:1
  401. handler:^(NSError *error) {
  402. XCTAssertNil(error);
  403. }];
  404. }
  405. /**
  406. * Tests that when a token deletion fails, the callback is invoked with an error.
  407. */
  408. - (void)testDeleteTokenFailure {
  409. XCTestExpectation *deleteExpectation =
  410. [self expectationWithDescription:@"Delete handler invoked."];
  411. [self mockAuthServiceToAlwaysReturnValidCheckin];
  412. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  413. NSError *someError = [[NSError alloc] initWithDomain:@"InstanceIDUnitTest" code:0 userInfo:nil];
  414. self.deleteTokenCompletion(someError);
  415. }] deleteTokenWithAuthorizedEntity:kAuthorizedEntity
  416. scope:kScope
  417. keyPair:[OCMArg any]
  418. handler:[OCMArg checkWithBlock:^BOOL(id obj) {
  419. self.deleteTokenCompletion = obj;
  420. return obj != nil;
  421. }]];
  422. [self.instanceID deleteTokenWithAuthorizedEntity:kAuthorizedEntity
  423. scope:kScope
  424. handler:^(NSError *error) {
  425. XCTAssertNotNil(error);
  426. [deleteExpectation fulfill];
  427. }];
  428. [self waitForExpectationsWithTimeout:1
  429. handler:^(NSError *error) {
  430. XCTAssertNil(error);
  431. }];
  432. }
  433. /**
  434. * Tests that not having a senderID will fetch a `nil` default token.
  435. */
  436. - (void)testDefaultToken_noSenderID {
  437. _instanceID.fcmSenderID = nil;
  438. XCTAssertNil([self.mockInstanceID token]);
  439. }
  440. /**
  441. * Tests that not having a cached token results in trying to fetch a new default token.
  442. */
  443. - (void)testDefaultToken_noCachedToken {
  444. [[[self.mockTokenManager stub] andReturn:nil]
  445. cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity
  446. scope:@"*"];
  447. OCMExpect([self.mockInstanceID fetchDefaultToken]);
  448. NSString *token = [self.mockInstanceID token];
  449. XCTAssertNil(token);
  450. [self.mockInstanceID stopMocking];
  451. OCMVerify([self.mockInstanceID fetchDefaultToken]);
  452. }
  453. /**
  454. * Tests that when we have a cached default token, calling `getToken` returns that token
  455. * without hitting the network.
  456. */
  457. - (void)testDefaultToken_validCachedToken {
  458. [[[self.mockTokenManager stub] andReturn:sTokenInfo]
  459. cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity
  460. scope:@"*"];
  461. [[self.mockInstanceID reject] fetchDefaultToken];
  462. NSString *token = [self.mockInstanceID token];
  463. XCTAssertEqualObjects(token, kToken);
  464. }
  465. /**
  466. * Test that when we fetch a new default token and cache it successfully we post a
  467. * tokenRefresh notification which allows to fetch the cached token.
  468. */
  469. - (void)testDefaultTokenFetch_returnValidToken {
  470. XCTestExpectation *defaultTokenExpectation =
  471. [self expectationWithDescription:@"Successfully got default token."];
  472. __block FIRInstanceIDTokenInfo *cachedTokenInfo = nil;
  473. [self stubKeyPairStoreToReturnValidKeypair];
  474. [self mockAuthServiceToAlwaysReturnValidCheckin];
  475. // Mock Token manager to always succeed the token fetch, and return
  476. // a particular cached value.
  477. // Return a dynamic cachedToken variable whenever the cached is checked.
  478. // This uses an invocation-based mock because the |cachedToken| pointer
  479. // will change. Normal stubbing will always return the initial pointer,
  480. // which in this case is 0x0 (nil).
  481. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  482. [invocation setReturnValue:&cachedTokenInfo];
  483. }] cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity scope:kFIRInstanceIDDefaultTokenScope];
  484. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  485. self.newTokenCompletion(kToken, nil);
  486. }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  487. scope:kFIRInstanceIDDefaultTokenScope
  488. keyPair:[OCMArg any]
  489. options:[OCMArg any]
  490. handler:[OCMArg checkWithBlock:^BOOL(id obj) {
  491. self.newTokenCompletion = obj;
  492. return obj != nil;
  493. }]];
  494. __block int notificationPostCount = 0;
  495. __block NSString *notificationToken = nil;
  496. NSString *notificationName = kFIRInstanceIDTokenRefreshNotification;
  497. self.tokenRefreshNotificationObserver = [[NSNotificationCenter defaultCenter]
  498. addObserverForName:notificationName
  499. object:nil
  500. queue:nil
  501. usingBlock:^(NSNotification *_Nonnull note) {
  502. // Should have saved token to cache
  503. cachedTokenInfo = sTokenInfo;
  504. notificationPostCount++;
  505. notificationToken = [[self.instanceID token] copy];
  506. [defaultTokenExpectation fulfill];
  507. }];
  508. NSString *token = [self.mockInstanceID token];
  509. [self waitForExpectationsWithTimeout:10.0 handler:nil];
  510. [[NSNotificationCenter defaultCenter] removeObserver:self.tokenRefreshNotificationObserver];
  511. XCTAssertNil(token);
  512. XCTAssertEqualObjects(notificationToken, kToken);
  513. }
  514. /**
  515. * Tests that if we fail to fetch the token from the server for the first time we retry again
  516. * later with exponential backoff unless we succeed.
  517. */
  518. - (void)testDefaultTokenFetch_retryFetchToken {
  519. const int trialsBeforeSuccess = 3;
  520. __block int newTokenFetchCount = 0;
  521. __block int64_t lastFetchTimestampInSeconds;
  522. XCTestExpectation *defaultTokenExpectation =
  523. [self expectationWithDescription:@"Successfully got default token."];
  524. __block FIRInstanceIDTokenInfo *cachedTokenInfo = nil;
  525. [self stubKeyPairStoreToReturnValidKeypair];
  526. [self mockAuthServiceToAlwaysReturnValidCheckin];
  527. // Mock Token manager.
  528. // Return a dynamic cachedToken variable whenever the cached is checked.
  529. // This uses an invocation-based mock because the |cachedToken| pointer
  530. // will change. Normal stubbing will always return the initial pointer,
  531. // which in this case is 0x0 (nil).
  532. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  533. [invocation setReturnValue:&cachedTokenInfo];
  534. }] cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity scope:kFIRInstanceIDDefaultTokenScope];
  535. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  536. newTokenFetchCount++;
  537. int64_t delaySinceLastFetchInSeconds =
  538. FIRInstanceIDCurrentTimestampInSeconds() - lastFetchTimestampInSeconds;
  539. // Test exponential backoff.
  540. if (newTokenFetchCount > 1) {
  541. XCTAssertLessThanOrEqual(1 << (newTokenFetchCount - 1), delaySinceLastFetchInSeconds);
  542. }
  543. lastFetchTimestampInSeconds = FIRInstanceIDCurrentTimestampInSeconds();
  544. if (newTokenFetchCount < trialsBeforeSuccess) {
  545. NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeTimeout];
  546. self.newTokenCompletion(nil, error);
  547. } else {
  548. self.newTokenCompletion(kToken, nil);
  549. }
  550. }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  551. scope:kFIRInstanceIDDefaultTokenScope
  552. keyPair:[OCMArg any]
  553. options:[OCMArg any]
  554. handler:[OCMArg checkWithBlock:^BOOL(id obj) {
  555. self.newTokenCompletion = obj;
  556. return obj != nil;
  557. }]];
  558. __block int notificationPostCount = 0;
  559. __block NSString *notificationToken = nil;
  560. NSString *notificationName = kFIRInstanceIDTokenRefreshNotification;
  561. self.tokenRefreshNotificationObserver = [[NSNotificationCenter defaultCenter]
  562. addObserverForName:notificationName
  563. object:nil
  564. queue:nil
  565. usingBlock:^(NSNotification *_Nonnull note) {
  566. // Should have saved token to cache
  567. cachedTokenInfo = sTokenInfo;
  568. notificationPostCount++;
  569. notificationToken = [[self.instanceID token] copy];
  570. [defaultTokenExpectation fulfill];
  571. }];
  572. NSString *token = [self.mockInstanceID token];
  573. [self waitForExpectationsWithTimeout:20.0 handler:nil];
  574. [[NSNotificationCenter defaultCenter] removeObserver:self.tokenRefreshNotificationObserver];
  575. XCTAssertNil(token);
  576. XCTAssertEqualObjects(notificationToken, kToken);
  577. XCTAssertEqual(notificationPostCount, 1);
  578. XCTAssertEqual(newTokenFetchCount, trialsBeforeSuccess);
  579. }
  580. /**
  581. * Tests that when we don't have a cached default token multiple invocations to `getToken`
  582. * lead to a single networking call to fetch the token. Also verify that we post one unique
  583. * TokenRefresh notification for multiple invocations.
  584. */
  585. - (void)testDefaultToken_multipleInvocations {
  586. __block int newTokenFetchCount = 0;
  587. XCTestExpectation *defaultTokenExpectation =
  588. [self expectationWithDescription:@"Successfully got default token."];
  589. __block FIRInstanceIDTokenInfo *cachedTokenInfo = nil;
  590. [self stubKeyPairStoreToReturnValidKeypair];
  591. [self mockAuthServiceToAlwaysReturnValidCheckin];
  592. // Mock Token manager.
  593. // Return a dynamic cachedToken variable whenever the cached is checked.
  594. // This uses an invocation-based mock because the |cachedToken| pointer
  595. // will change. Normal stubbing will always return the initial pointer,
  596. // which in this case is 0x0 (nil).
  597. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  598. [invocation setReturnValue:&cachedTokenInfo];
  599. }] cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity scope:kFIRInstanceIDDefaultTokenScope];
  600. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  601. // Invoke callback after some delay (network delay)
  602. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)),
  603. dispatch_get_main_queue(), ^{
  604. self.newTokenCompletion(kToken, nil);
  605. });
  606. newTokenFetchCount++;
  607. XCTAssertEqual(newTokenFetchCount, 1);
  608. }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  609. scope:kFIRInstanceIDDefaultTokenScope
  610. keyPair:[OCMArg any]
  611. options:[OCMArg any]
  612. handler:[OCMArg checkWithBlock:^BOOL(id obj) {
  613. self.newTokenCompletion = obj;
  614. return obj != nil;
  615. }]];
  616. __block int notificationPostCount = 0;
  617. __block NSString *notificationToken = nil;
  618. NSString *notificationName = kFIRInstanceIDTokenRefreshNotification;
  619. self.tokenRefreshNotificationObserver = [[NSNotificationCenter defaultCenter]
  620. addObserverForName:notificationName
  621. object:nil
  622. queue:nil
  623. usingBlock:^(NSNotification *_Nonnull note) {
  624. // Should have saved token to cache
  625. cachedTokenInfo = sTokenInfo;
  626. notificationPostCount++;
  627. notificationToken = [[self.instanceID token] copy];
  628. [defaultTokenExpectation fulfill];
  629. }];
  630. NSString *token = [self.mockInstanceID token];
  631. // Invoke get token again with some delay. Our initial request to getToken hasn't yet
  632. // returned from the server.
  633. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
  634. dispatch_get_main_queue(), ^{
  635. XCTAssertNil([self.mockInstanceID token]);
  636. });
  637. // Invoke again after further delay.
  638. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)),
  639. dispatch_get_main_queue(), ^{
  640. XCTAssertNil([self.mockInstanceID token]);
  641. });
  642. [self waitForExpectationsWithTimeout:15.0 handler:nil];
  643. [[NSNotificationCenter defaultCenter] removeObserver:self.tokenRefreshNotificationObserver];
  644. XCTAssertNil(token);
  645. XCTAssertEqualObjects(notificationToken, kToken);
  646. XCTAssertEqual(notificationPostCount, 1);
  647. XCTAssertEqual(newTokenFetchCount, 1);
  648. }
  649. - (void)testDefaultToken_maxRetries {
  650. __block int newTokenFetchCount = 0;
  651. XCTestExpectation *defaultTokenExpectation =
  652. [self expectationWithDescription:@"Did retry maximum times to fetch default token."];
  653. [self stubKeyPairStoreToReturnValidKeypair];
  654. [self mockAuthServiceToAlwaysReturnValidCheckin];
  655. // Mock Token manager.
  656. [[[self.mockTokenManager stub] andReturn:nil]
  657. cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity
  658. scope:kFIRInstanceIDDefaultTokenScope];
  659. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  660. newTokenFetchCount++;
  661. NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeNetwork];
  662. self.newTokenCompletion(nil, error);
  663. if (newTokenFetchCount == [FIRInstanceID maxRetryCountForDefaultToken]) {
  664. [defaultTokenExpectation fulfill];
  665. }
  666. }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  667. scope:kFIRInstanceIDDefaultTokenScope
  668. keyPair:[OCMArg any]
  669. options:[OCMArg any]
  670. handler:[OCMArg checkWithBlock:^BOOL(id obj) {
  671. self.newTokenCompletion = obj;
  672. return obj != nil;
  673. }]];
  674. // Mock Instance ID's retry interval to 0, to vastly speed up this test.
  675. [[[self.mockInstanceID stub] andReturnValue:@(0)] retryIntervalToFetchDefaultToken];
  676. // Try to fetch token once. It should set off retries since we mock failure.
  677. NSString *token = [self.mockInstanceID token];
  678. [self waitForExpectationsWithTimeout:1.0 handler:nil];
  679. XCTAssertNil(token);
  680. XCTAssertEqual(newTokenFetchCount, [FIRInstanceID maxRetryCountForDefaultToken]);
  681. }
  682. /**
  683. * Tests a Keychain read failure while we try to fetch a new InstanceID token. If the Keychain
  684. * read fails we won't be able to fetch the public key which is required while fetching a new
  685. * token. In such a case we should return KeyPair failure.
  686. */
  687. - (void)testNewTokenFetch_keyChainError {
  688. XCTestExpectation *tokenExpectation =
  689. [self expectationWithDescription:@"New token handler invoked."];
  690. [self mockAuthServiceToAlwaysReturnValidCheckin];
  691. // Simulate keypair fetch/generation failure.
  692. [[[self.mockKeyPairStore stub] andReturn:nil] loadKeyPairWithError:[OCMArg anyObjectRef]];
  693. [[self.mockTokenManager reject] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  694. scope:kScope
  695. keyPair:[OCMArg any]
  696. options:[OCMArg any]
  697. handler:[OCMArg any]];
  698. [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity
  699. scope:kScope
  700. options:nil
  701. handler:^(NSString *token, NSError *error) {
  702. XCTAssertNil(token);
  703. XCTAssertNotNil(error);
  704. [tokenExpectation fulfill];
  705. }];
  706. [self waitForExpectationsWithTimeout:1 handler:nil];
  707. OCMVerifyAll(self.mockTokenManager);
  708. }
  709. /**
  710. * If a token fetch includes in its options an "apns_token" object, but not a "apns_sandbox" key,
  711. * ensure that an "apns_sandbox" key is added to the token options (via automatic detection).
  712. */
  713. - (void)testTokenFetchAPNSServerTypeIsIncludedIfAPNSTokenProvided {
  714. XCTestExpectation *apnsServerTypeExpectation =
  715. [self expectationWithDescription:@"apns_sandbox key was included in token options"];
  716. [self stubKeyPairStoreToReturnValidKeypair];
  717. [self mockAuthServiceToAlwaysReturnValidCheckin];
  718. NSData *apnsToken = [kFakeAPNSToken dataUsingEncoding:NSUTF8StringEncoding];
  719. // Option is purposefully missing the apns_sandbox key
  720. NSDictionary *tokenOptions = @{kFIRInstanceIDTokenOptionsAPNSKey : apnsToken};
  721. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  722. // Inspect
  723. NSDictionary *options;
  724. [invocation getArgument:&options atIndex:5];
  725. if (options[kFIRInstanceIDTokenOptionsAPNSIsSandboxKey] != nil) {
  726. [apnsServerTypeExpectation fulfill];
  727. }
  728. self.newTokenCompletion(kToken, nil);
  729. }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  730. scope:kScope
  731. keyPair:[OCMArg any]
  732. options:[OCMArg any]
  733. handler:[OCMArg checkWithBlock:^BOOL(id obj) {
  734. self.newTokenCompletion = obj;
  735. return obj != nil;
  736. }]];
  737. [self.instanceID tokenWithAuthorizedEntity:kAuthorizedEntity
  738. scope:kScope
  739. options:tokenOptions
  740. handler:^(NSString *token, NSError *error){
  741. }];
  742. [self waitForExpectationsWithTimeout:60.0
  743. handler:^(NSError *error) {
  744. XCTAssertNil(error);
  745. }];
  746. }
  747. /**
  748. * Tests that if a token was fetched, but during the fetch the APNs data was set, that a new
  749. * token is fetched to associate the APNs data, and is not returned from the cache.
  750. */
  751. - (void)testTokenFetch_ignoresCacheIfAPNSInfoDifferent {
  752. XCTestExpectation *tokenRequestExpectation =
  753. [self expectationWithDescription:@"Token was fetched from the network"];
  754. // Initialize a token in the cache *WITHOUT* APNSInfo
  755. // This token is |kToken|, but we will simulate that a fetch will return another token
  756. NSString *oldCachedToken = kToken;
  757. NSString *fetchedToken = @"abcd123_newtoken";
  758. __block FIRInstanceIDTokenInfo *cachedTokenInfo =
  759. [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:kAuthorizedEntity
  760. scope:kFIRInstanceIDDefaultTokenScope
  761. token:oldCachedToken
  762. appVersion:@"1.0"
  763. firebaseAppID:@"firebaseAppID"];
  764. [self stubKeyPairStoreToReturnValidKeypair];
  765. [self mockAuthServiceToAlwaysReturnValidCheckin];
  766. // During this test use the default scope ("*") to simulate the default token behavior.
  767. // Return a dynamic cachedToken variable whenever the cached is checked.
  768. // This uses an invocation-based mock because the |cachedToken| pointer
  769. // will change. Normal stubbing will always return the initial pointer,
  770. // which in this case is 0x0 (nil).
  771. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  772. [invocation setReturnValue:&cachedTokenInfo];
  773. }] cachedTokenInfoWithAuthorizedEntity:kAuthorizedEntity scope:kFIRInstanceIDDefaultTokenScope];
  774. // Mock the network request to return |fetchedToken|, so we can clearly see if the token is
  775. // is different than what was cached.
  776. [[[self.mockTokenManager stub] andDo:^(NSInvocation *invocation) {
  777. [tokenRequestExpectation fulfill];
  778. self.newTokenCompletion(fetchedToken, nil);
  779. }] fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  780. scope:kFIRInstanceIDDefaultTokenScope
  781. keyPair:[OCMArg any]
  782. options:[OCMArg any]
  783. handler:[OCMArg checkWithBlock:^BOOL(id obj) {
  784. self.newTokenCompletion = obj;
  785. return obj != nil;
  786. }]];
  787. // Begin request
  788. // Token options has APNS data, which is not associated with the cached token
  789. NSDictionary *tokenOptions = @{
  790. kFIRInstanceIDTokenOptionsAPNSKey : [@"apns" dataUsingEncoding:NSUTF8StringEncoding],
  791. kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(NO)
  792. };
  793. [self.instanceID
  794. tokenWithAuthorizedEntity:kAuthorizedEntity
  795. scope:kFIRInstanceIDDefaultTokenScope
  796. options:tokenOptions
  797. handler:^(NSString *_Nullable token, NSError *_Nullable error) {
  798. XCTAssertEqualObjects(token, fetchedToken);
  799. }];
  800. [self waitForExpectationsWithTimeout:0.5 handler:nil];
  801. }
  802. /**
  803. * Tests that if there is a keychain failure while fetching the InstanceID of the token we should
  804. * return nil for the identity.
  805. */
  806. - (void)testInstanceIDFetch_keyChainError {
  807. XCTestExpectation *tokenExpectation =
  808. [self expectationWithDescription:@"InstanceID fetch handler invoked."];
  809. // Simulate keypair fetch/generation failure.
  810. NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidKeyPair];
  811. [[[self.mockKeyPairStore stub] andReturn:nil] appIdentityWithError:[OCMArg setTo:error]];
  812. [self.instanceID getIDWithHandler:^(NSString *_Nullable identity, NSError *_Nullable error) {
  813. XCTAssertNil(identity);
  814. XCTAssertNotNil(error);
  815. [tokenExpectation fulfill];
  816. }];
  817. [self waitForExpectationsWithTimeout:1 handler:nil];
  818. }
  819. - (void)testInstanceIDDelete_keyChainError {
  820. XCTestExpectation *tokenExpectation =
  821. [self expectationWithDescription:@"InstanceID deleteID handler invoked."];
  822. // Simulate keypair fetch/generation failure.
  823. NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeInvalidKeyPair];
  824. [[[self.mockKeyPairStore stub] andReturn:nil] appIdentityWithError:[OCMArg setTo:error]];
  825. [self.instanceID deleteIDWithHandler:^(NSError *_Nullable error) {
  826. XCTAssertNotNil(error);
  827. [tokenExpectation fulfill];
  828. }];
  829. [self waitForExpectationsWithTimeout:1 handler:nil];
  830. }
  831. #pragma mark - Private Helpers
  832. - (void)stubKeyPairStoreToReturnValidKeypair {
  833. [[[self.mockKeyPairStore stub] andReturn:[self createValidMockKeypair]]
  834. loadKeyPairWithError:[OCMArg anyObjectRef]];
  835. }
  836. - (id)createValidMockKeypair {
  837. id mockKeypair = OCMClassMock([FIRInstanceIDKeyPair class]);
  838. [[[mockKeypair stub] andReturnValue:@YES] isValid];
  839. return mockKeypair;
  840. }
  841. - (FIRInstanceIDCheckinPreferences *)validCheckinPreferences {
  842. NSDictionary *gservicesData = @{
  843. kFIRInstanceIDVersionInfoStringKey : kVersionInfo,
  844. kFIRInstanceIDLastCheckinTimeKey : @(FIRInstanceIDCurrentTimestampInMilliseconds())
  845. };
  846. FIRInstanceIDCheckinPreferences *checkinPreferences =
  847. [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceAuthId
  848. secretToken:kSecretToken];
  849. [checkinPreferences updateWithCheckinPlistContents:gservicesData];
  850. return checkinPreferences;
  851. }
  852. - (void)mockAuthServiceToAlwaysReturnValidCheckin {
  853. FIRInstanceIDCheckinPreferences *validCheckin = [self validCheckinPreferences];
  854. __block FIRInstanceIDDeviceCheckinCompletion checkinHandler;
  855. [[[self.mockAuthService stub] andDo:^(NSInvocation *invocation) {
  856. if (checkinHandler) {
  857. checkinHandler(validCheckin, nil);
  858. }
  859. }] fetchCheckinInfoWithHandler:[OCMArg checkWithBlock:^BOOL(id obj) {
  860. return (checkinHandler = obj) != nil;
  861. }]];
  862. }
  863. @end