FIRInstanceIDTokenManagerTest.m 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  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 <OCMock/OCMock.h>
  18. #import "FIRInstanceIDFakeKeychain.h"
  19. #import "FIRInstanceIDTokenManager+Test.h"
  20. #import "Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.h"
  21. #import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h"
  22. #import "Firebase/InstanceID/FIRInstanceIDCheckinStore.h"
  23. #import "Firebase/InstanceID/FIRInstanceIDStore.h"
  24. #import "Firebase/InstanceID/FIRInstanceIDTokenDeleteOperation.h"
  25. #import "Firebase/InstanceID/FIRInstanceIDTokenFetchOperation.h"
  26. #import "Firebase/InstanceID/FIRInstanceIDTokenInfo.h"
  27. #import "Firebase/InstanceID/FIRInstanceIDTokenManager.h"
  28. #import "Firebase/InstanceID/FIRInstanceIDTokenOperation.h"
  29. #import "Firebase/InstanceID/FIRInstanceIDTokenStore.h"
  30. static NSString *const kSubDirectoryName = @"FirebaseInstanceIDTokenManagerTest";
  31. static NSString *const kAuthorizedEntity = @"test-authorized-entity";
  32. static NSString *const kScope = @"test-scope";
  33. static NSString *const kToken = @"test-token";
  34. // Use a string (which is converted to NSData) as a placeholder for an actual APNs device token.
  35. static NSString *const kNewAPNSTokenString = @"newAPNSData";
  36. @interface FIRInstanceIDTokenOperation ()
  37. - (void)performTokenOperation;
  38. - (void)finishWithResult:(FIRInstanceIDTokenOperationResult)result
  39. token:(nullable NSString *)token
  40. error:(nullable NSError *)error;
  41. @end
  42. @interface FIRInstanceIDTokenManager (ExposedForTests)
  43. - (BOOL)checkForTokenRefreshPolicy;
  44. - (void)updateToAPNSDeviceToken:(NSData *)deviceToken isSandbox:(BOOL)isSandbox;
  45. /**
  46. * Create a fetch operation. This method can be stubbed to return a particular operation instance,
  47. * which makes it easier to unit test different behaviors.
  48. */
  49. - (FIRInstanceIDTokenFetchOperation *)
  50. createFetchOperationWithAuthorizedEntity:(NSString *)authorizedEntity
  51. scope:(NSString *)scope
  52. options:(NSDictionary<NSString *, NSString *> *)options
  53. keyPair:(FIRInstanceIDKeyPair *)keyPair;
  54. /**
  55. * Create a delete operation. This method can be stubbed to return a particular operation instance,
  56. * which makes it easier to unit test different behaviors.
  57. */
  58. - (FIRInstanceIDTokenDeleteOperation *)
  59. createDeleteOperationWithAuthorizedEntity:(NSString *)authorizedEntity
  60. scope:(NSString *)scope
  61. checkinPreferences:(FIRInstanceIDCheckinPreferences *)checkinPreferences
  62. keyPair:(FIRInstanceIDKeyPair *)keyPair
  63. action:(FIRInstanceIDTokenAction)action;
  64. @end
  65. @interface FIRInstanceIDTokenManagerTest : XCTestCase
  66. @property(nonatomic, readwrite, strong) FIRInstanceIDTokenManager *tokenManager;
  67. @property(nonatomic, readwrite, strong) id mockTokenManager;
  68. @property(nonatomic, readwrite, strong) FIRInstanceIDBackupExcludedPlist *checkinPlist;
  69. @property(nonatomic, readwrite, strong) FIRInstanceIDFakeKeychain *fakeKeyChain;
  70. @property(nonatomic, readwrite, strong) FIRInstanceIDTokenStore *tokenStore;
  71. @property(nonatomic, readwrite, strong) FIRInstanceIDCheckinPreferences *fakeCheckin;
  72. @end
  73. @implementation FIRInstanceIDTokenManagerTest
  74. - (void)setUp {
  75. [super setUp];
  76. [FIRInstanceIDStore createSubDirectory:kSubDirectoryName];
  77. NSString *checkinPlistFilename = @"com.google.test.IIDCheckinTest";
  78. self.checkinPlist =
  79. [[FIRInstanceIDBackupExcludedPlist alloc] initWithFileName:checkinPlistFilename
  80. subDirectory:kSubDirectoryName];
  81. // checkin store
  82. FIRInstanceIDFakeKeychain *fakeCheckinKeychain = [[FIRInstanceIDFakeKeychain alloc] init];
  83. FIRInstanceIDCheckinStore *checkinStore =
  84. [[FIRInstanceIDCheckinStore alloc] initWithCheckinPlist:self.checkinPlist
  85. keychain:fakeCheckinKeychain];
  86. // token store
  87. _fakeKeyChain = [[FIRInstanceIDFakeKeychain alloc] init];
  88. _tokenStore = [[FIRInstanceIDTokenStore alloc] initWithKeychain:_fakeKeyChain];
  89. _tokenManager = [[FIRInstanceIDTokenManager alloc] initWithCheckinStore:checkinStore
  90. tokenStore:self.tokenStore];
  91. _mockTokenManager = OCMPartialMock(_tokenManager);
  92. _fakeCheckin = [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:@"fakeDeviceID"
  93. secretToken:@"fakeSecretToken"];
  94. }
  95. - (void)tearDown {
  96. NSError *error;
  97. if (![self.checkinPlist deleteFile:&error]) {
  98. XCTFail(@"Failed to delete checkin plist %@", error);
  99. }
  100. self.tokenManager = nil;
  101. [FIRInstanceIDStore removeSubDirectory:kSubDirectoryName error:nil];
  102. [super tearDown];
  103. }
  104. /**
  105. * Tests that when a new InstanceID token is successfully produced,
  106. * the callback is invoked with a token that is not an empty string and with no error.
  107. */
  108. - (void)testNewTokenSuccess {
  109. XCTestExpectation *tokenExpectation =
  110. [self expectationWithDescription:@"New token handler invoked."];
  111. NSDictionary *tokenOptions = [NSDictionary dictionary];
  112. // Create a fake operation that always returns success
  113. FIRInstanceIDTokenFetchOperation *operation =
  114. [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  115. scope:kScope
  116. options:tokenOptions
  117. checkinPreferences:self.fakeCheckin
  118. keyPair:[OCMArg any]];
  119. id mockOperation = OCMPartialMock(operation);
  120. [[[mockOperation stub] andDo:^(NSInvocation *invocation) {
  121. [invocation.target finishWithResult:FIRInstanceIDTokenOperationSucceeded
  122. token:kToken
  123. error:nil];
  124. }] performTokenOperation];
  125. // Return our fake operation when asked for an operation
  126. [[[self.mockTokenManager stub] andReturn:operation]
  127. createFetchOperationWithAuthorizedEntity:[OCMArg any]
  128. scope:[OCMArg any]
  129. options:[OCMArg any]
  130. keyPair:[OCMArg any]];
  131. [self.tokenManager fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  132. scope:kScope
  133. keyPair:[OCMArg any]
  134. options:tokenOptions
  135. handler:^(NSString *token, NSError *error) {
  136. // Keep 'operation' alive, so it's not
  137. // prematurely destroyed
  138. XCTAssertNotNil(operation);
  139. XCTAssertNotNil(mockOperation);
  140. XCTAssertNotNil(token);
  141. XCTAssertGreaterThan(token.length, 0);
  142. XCTAssertNil(error);
  143. [tokenExpectation fulfill];
  144. }];
  145. [self waitForExpectationsWithTimeout:1 handler:nil];
  146. }
  147. /**
  148. * Tests that when a new InstanceID token is fetched from the server but unsuccessfully
  149. * saved on the client we should return an error instead of the fetched token.
  150. */
  151. - (void)testNewTokenSaveFailure {
  152. XCTestExpectation *tokenExpectation =
  153. [self expectationWithDescription:@"New token handler invoked."];
  154. NSDictionary *tokenOptions = [NSDictionary dictionary];
  155. // Simulate write to keychain failure.
  156. self.fakeKeyChain.cannotWriteToKeychain = YES;
  157. // Create a fake operation that always returns success
  158. FIRInstanceIDTokenFetchOperation *operation =
  159. [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  160. scope:kScope
  161. options:tokenOptions
  162. checkinPreferences:self.fakeCheckin
  163. keyPair:[OCMArg any]];
  164. id mockOperation = OCMPartialMock(operation);
  165. [[[mockOperation stub] andDo:^(NSInvocation *invocation) {
  166. [invocation.target finishWithResult:FIRInstanceIDTokenOperationSucceeded
  167. token:kToken
  168. error:nil];
  169. }] performTokenOperation];
  170. // Return our fake operation when asked for an operation
  171. [[[self.mockTokenManager stub] andReturn:operation]
  172. createFetchOperationWithAuthorizedEntity:[OCMArg any]
  173. scope:[OCMArg any]
  174. options:[OCMArg any]
  175. keyPair:[OCMArg any]];
  176. [self.tokenManager fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  177. scope:kScope
  178. keyPair:[OCMArg any]
  179. options:tokenOptions
  180. handler:^(NSString *token, NSError *error) {
  181. // Keep 'operation' alive, so it's not prematurely
  182. // destroyed
  183. XCTAssertNotNil(operation);
  184. XCTAssertNotNil(mockOperation);
  185. XCTAssertNil(token);
  186. XCTAssertNotNil(error);
  187. [tokenExpectation fulfill];
  188. }];
  189. [self waitForExpectationsWithTimeout:1
  190. handler:^(NSError *error) {
  191. XCTAssertNil(error.localizedDescription);
  192. }];
  193. }
  194. /**
  195. * Tests that when there is a failure in producing a new InstanceID token,
  196. * the callback is invoked with an error and a nil token.
  197. */
  198. - (void)testNewTokenFailure {
  199. XCTestExpectation *tokenExpectation =
  200. [self expectationWithDescription:@"New token handler invoked."];
  201. NSDictionary *tokenOptions = [NSDictionary dictionary];
  202. // Create a fake operation that always returns failure
  203. FIRInstanceIDTokenFetchOperation *operation =
  204. [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  205. scope:kScope
  206. options:tokenOptions
  207. checkinPreferences:self.fakeCheckin
  208. keyPair:[OCMArg any]];
  209. id mockOperation = OCMPartialMock(operation);
  210. [[[mockOperation stub] andDo:^(NSInvocation *invocation) {
  211. NSError *someError = [[NSError alloc] initWithDomain:@"InstanceIDUnitTest" code:0 userInfo:nil];
  212. [invocation.target finishWithResult:FIRInstanceIDTokenOperationError token:nil error:someError];
  213. }] performTokenOperation];
  214. // Return our fake operation when asked for an operation
  215. [[[self.mockTokenManager stub] andReturn:operation]
  216. createFetchOperationWithAuthorizedEntity:[OCMArg any]
  217. scope:[OCMArg any]
  218. options:[OCMArg any]
  219. keyPair:[OCMArg any]];
  220. [self.tokenManager fetchNewTokenWithAuthorizedEntity:kAuthorizedEntity
  221. scope:kScope
  222. keyPair:[OCMArg any]
  223. options:tokenOptions
  224. handler:^(NSString *token, NSError *error) {
  225. // Keep 'operation' alive, so it's not
  226. // prematurely destroyed
  227. XCTAssertNotNil(operation);
  228. XCTAssertNotNil(mockOperation);
  229. XCTAssertNil(token);
  230. XCTAssertNotNil(error);
  231. [tokenExpectation fulfill];
  232. }];
  233. [self waitForExpectationsWithTimeout:1
  234. handler:^(NSError *error) {
  235. XCTAssertNil(error.localizedDescription);
  236. }];
  237. }
  238. /**
  239. * Tests that when a token is deleted successfully, the callback is invoked with no error.
  240. */
  241. - (void)testDeleteTokenSuccess {
  242. XCTestExpectation *deleteExpectation =
  243. [self expectationWithDescription:@"Delete handler invoked."];
  244. // Create a fake operation that always succeeds
  245. FIRInstanceIDTokenDeleteOperation *operation = [[FIRInstanceIDTokenDeleteOperation alloc]
  246. initWithAuthorizedEntity:kAuthorizedEntity
  247. scope:kScope
  248. checkinPreferences:self.fakeCheckin
  249. keyPair:[OCMArg any]
  250. action:FIRInstanceIDTokenActionDeleteToken];
  251. id mockOperation = OCMPartialMock(operation);
  252. [[[mockOperation stub] andDo:^(NSInvocation *invocation) {
  253. [invocation.target finishWithResult:FIRInstanceIDTokenOperationSucceeded token:nil error:nil];
  254. }] performTokenOperation];
  255. // Return our fake operation when asked for an operation
  256. [[[self.mockTokenManager stub] andReturn:operation]
  257. createDeleteOperationWithAuthorizedEntity:[OCMArg any]
  258. scope:[OCMArg any]
  259. checkinPreferences:[OCMArg any]
  260. keyPair:[OCMArg any]
  261. action:FIRInstanceIDTokenActionDeleteToken];
  262. [self.tokenManager deleteTokenWithAuthorizedEntity:kAuthorizedEntity
  263. scope:kScope
  264. keyPair:[OCMArg any]
  265. handler:^(NSError *error) {
  266. // Keep 'operation' alive, so it's not prematurely
  267. // destroyed
  268. XCTAssertNotNil(operation);
  269. XCTAssertNotNil(mockOperation);
  270. XCTAssertNil(error);
  271. [deleteExpectation fulfill];
  272. }];
  273. [self waitForExpectationsWithTimeout:1
  274. handler:^(NSError *error) {
  275. XCTAssertNil(error.localizedDescription);
  276. }];
  277. }
  278. /**
  279. * Tests that when a token deletion fails, the callback is invoked with an error.
  280. */
  281. - (void)testDeleteTokenFailure {
  282. XCTestExpectation *deleteExpectation =
  283. [self expectationWithDescription:@"Delete handler invoked."];
  284. // Create a fake operation that always fails
  285. FIRInstanceIDTokenDeleteOperation *operation = [[FIRInstanceIDTokenDeleteOperation alloc]
  286. initWithAuthorizedEntity:kAuthorizedEntity
  287. scope:kScope
  288. checkinPreferences:self.fakeCheckin
  289. keyPair:[OCMArg any]
  290. action:FIRInstanceIDTokenActionDeleteToken];
  291. id mockOperation = OCMPartialMock(operation);
  292. [[[mockOperation stub] andDo:^(NSInvocation *invocation) {
  293. NSError *someError = [[NSError alloc] initWithDomain:@"InstanceIDUnitTest" code:0 userInfo:nil];
  294. [invocation.target finishWithResult:FIRInstanceIDTokenOperationError token:nil error:someError];
  295. }] performTokenOperation];
  296. // Return our fake operation when asked for an operation
  297. [[[self.mockTokenManager stub] andReturn:operation]
  298. createDeleteOperationWithAuthorizedEntity:[OCMArg any]
  299. scope:[OCMArg any]
  300. checkinPreferences:[OCMArg any]
  301. keyPair:[OCMArg any]
  302. action:FIRInstanceIDTokenActionDeleteToken];
  303. [self.tokenManager deleteTokenWithAuthorizedEntity:kAuthorizedEntity
  304. scope:kScope
  305. keyPair:[OCMArg any]
  306. handler:^(NSError *error) {
  307. // Keep 'operation' alive, so it's not prematurely
  308. // destroyed
  309. XCTAssertNotNil(operation);
  310. XCTAssertNotNil(mockOperation);
  311. XCTAssertNotNil(error);
  312. [deleteExpectation fulfill];
  313. }];
  314. [self waitForExpectationsWithTimeout:1
  315. handler:^(NSError *error) {
  316. XCTAssertNil(error.localizedDescription);
  317. }];
  318. }
  319. #pragma mark - Cached Token Invalidation
  320. - (void)testCachedTokensInvalidatedOnAppVersionChange {
  321. // Write some fake tokens to cache with a old app version "0.9"
  322. NSArray<NSString *> *entities = @[ @"entity1", @"entity2" ];
  323. for (NSString *entity in entities) {
  324. FIRInstanceIDTokenInfo *info =
  325. [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:entity
  326. scope:kScope
  327. token:@"abcdef"
  328. appVersion:@"0.9"
  329. firebaseAppID:nil];
  330. [self.tokenStore saveTokenInfo:info handler:nil];
  331. }
  332. // Ensure they tokens now exist.
  333. for (NSString *entity in entities) {
  334. FIRInstanceIDTokenInfo *cachedTokenInfo =
  335. [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope];
  336. XCTAssertNotNil(cachedTokenInfo);
  337. }
  338. // Trigger a potential reset, the current app version is 1.0 which is newer than
  339. // the one set in tokenInfo.
  340. [self.tokenManager checkForTokenRefreshPolicy];
  341. // Ensure that token data is now missing
  342. for (NSString *entity in entities) {
  343. FIRInstanceIDTokenInfo *cachedTokenInfo =
  344. [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope];
  345. XCTAssertNil(cachedTokenInfo);
  346. }
  347. }
  348. - (void)testCachedTokensInvalidatedOnAPNSAddition {
  349. // Write some fake tokens to cache, which have no APNs info
  350. NSArray<NSString *> *entities = @[ @"entity1", @"entity2" ];
  351. for (NSString *entity in entities) {
  352. FIRInstanceIDTokenInfo *info =
  353. [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:entity
  354. scope:kScope
  355. token:@"abcdef"
  356. appVersion:nil
  357. firebaseAppID:nil];
  358. [self.tokenStore saveTokenInfo:info handler:nil];
  359. }
  360. // Ensure the tokens now exist.
  361. for (NSString *entity in entities) {
  362. FIRInstanceIDTokenInfo *cachedTokenInfo =
  363. [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope];
  364. XCTAssertNotNil(cachedTokenInfo);
  365. }
  366. // Trigger a potential reset.
  367. [self triggerAPNSTokenChange];
  368. // Ensure that token data is now missing
  369. for (NSString *entity in entities) {
  370. FIRInstanceIDTokenInfo *cachedTokenInfo =
  371. [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope];
  372. XCTAssertNil(cachedTokenInfo);
  373. }
  374. }
  375. - (void)testCachedTokensInvalidatedOnAPNSChange {
  376. // Write some fake tokens to cache
  377. NSArray<NSString *> *entities = @[ @"entity1", @"entity2" ];
  378. NSData *oldAPNSData = [@"oldAPNSToken" dataUsingEncoding:NSUTF8StringEncoding];
  379. for (NSString *entity in entities) {
  380. FIRInstanceIDTokenInfo *info =
  381. [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:entity
  382. scope:kScope
  383. token:@"abcdef"
  384. appVersion:nil
  385. firebaseAppID:nil];
  386. info.APNSInfo = [[FIRInstanceIDAPNSInfo alloc] initWithDeviceToken:oldAPNSData isSandbox:NO];
  387. [self.tokenStore saveTokenInfo:info handler:nil];
  388. }
  389. // Ensure the tokens now exist.
  390. for (NSString *entity in entities) {
  391. FIRInstanceIDTokenInfo *cachedTokenInfo =
  392. [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope];
  393. XCTAssertNotNil(cachedTokenInfo);
  394. }
  395. // Trigger a potential reset.
  396. [self triggerAPNSTokenChange];
  397. // Ensure that token data is now missing
  398. for (NSString *entity in entities) {
  399. FIRInstanceIDTokenInfo *cachedTokenInfo =
  400. [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope];
  401. XCTAssertNil(cachedTokenInfo);
  402. }
  403. }
  404. - (void)testCachedTokensNotInvalidatedIfAPNSSame {
  405. // Write some fake tokens to cache, with the current APNs token
  406. NSArray<NSString *> *entities = @[ @"entity1", @"entity2" ];
  407. NSString *apnsDataString = kNewAPNSTokenString;
  408. NSData *currentAPNSData = [apnsDataString dataUsingEncoding:NSUTF8StringEncoding];
  409. for (NSString *entity in entities) {
  410. FIRInstanceIDTokenInfo *info =
  411. [[FIRInstanceIDTokenInfo alloc] initWithAuthorizedEntity:entity
  412. scope:kScope
  413. token:@"abcdef"
  414. appVersion:nil
  415. firebaseAppID:nil];
  416. info.APNSInfo = [[FIRInstanceIDAPNSInfo alloc] initWithDeviceToken:currentAPNSData
  417. isSandbox:NO];
  418. [self.tokenStore saveTokenInfo:info handler:nil];
  419. }
  420. // Ensure the tokens now exist.
  421. for (NSString *entity in entities) {
  422. FIRInstanceIDTokenInfo *cachedTokenInfo =
  423. [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope];
  424. XCTAssertNotNil(cachedTokenInfo);
  425. }
  426. // Trigger a potential reset.
  427. [self triggerAPNSTokenChange];
  428. // Ensure that token data is still there
  429. for (NSString *entity in entities) {
  430. FIRInstanceIDTokenInfo *cachedTokenInfo =
  431. [self.tokenManager cachedTokenInfoWithAuthorizedEntity:entity scope:kScope];
  432. XCTAssertNotNil(cachedTokenInfo);
  433. }
  434. }
  435. - (void)triggerAPNSTokenChange {
  436. // Trigger a potential reset.
  437. NSData *deviceToken = [kNewAPNSTokenString dataUsingEncoding:NSUTF8StringEncoding];
  438. [self.tokenManager updateTokensToAPNSDeviceToken:deviceToken isSandbox:NO];
  439. }
  440. @end