FIRInstanceIDTokenManagerTest.m 23 KB

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