FIRMessagingTokenOperationsTest.m 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. /*
  2. * Copyright 2021 Google LLC
  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 "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
  19. #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h"
  20. #import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
  21. #import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
  22. #import "FirebaseMessaging/Sources/Token/FIRMessagingAuthService.h"
  23. #import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinPreferences.h"
  24. #import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinService.h"
  25. #import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinStore.h"
  26. #import "FirebaseMessaging/Sources/Token/FIRMessagingKeychain.h"
  27. #import "FirebaseMessaging/Sources/Token/FIRMessagingTokenDeleteOperation.h"
  28. #import "FirebaseMessaging/Sources/Token/FIRMessagingTokenFetchOperation.h"
  29. #import "FirebaseMessaging/Sources/Token/FIRMessagingTokenOperation.h"
  30. #import "FirebaseMessaging/Sources/Token/FIRMessagingTokenStore.h"
  31. #import "SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.h"
  32. static NSString *kDeviceID = @"fakeDeviceID";
  33. static NSString *kSecretToken = @"fakeSecretToken";
  34. static NSString *kDigestString = @"test-digest";
  35. static NSString *kVersionInfoString = @"version_info-1.0.0";
  36. static NSString *kAuthorizedEntity = @"sender-1234567";
  37. static NSString *kScope = @"fcm";
  38. static NSString *kRegistrationToken = @"token-12345";
  39. @interface FIRMessagingTokenOperation (ExposedForTest)
  40. - (void)performTokenOperation;
  41. + (NSString *)HTTPAuthHeaderFromCheckin:(FIRMessagingCheckinPreferences *)checkin;
  42. @end
  43. @interface FIRInstallationsAuthTokenResult (Tests)
  44. - (instancetype)initWithToken:(NSString *)token expirationDate:(NSDate *)expirationDate;
  45. @end
  46. // A Fake operation that allows us to check that perform was called.
  47. // We are not using mocks here because we have no way of forcing NSOperationQueues to release
  48. // their operations, and this means that there is always going to be a race condition between
  49. // when we "stop" our partial mock vs when NSOperationQueue attempts to access the mock object on a
  50. // separate thread. We had mocks previously.
  51. @interface FIRMessagingTokenOperationFake : FIRMessagingTokenOperation
  52. @property(nonatomic, assign) BOOL performWasCalled;
  53. @end
  54. @implementation FIRMessagingTokenOperationFake
  55. - (void)performTokenOperation {
  56. self.performWasCalled = YES;
  57. }
  58. @end
  59. @interface FIRMessagingTokenOperationsTest : XCTestCase
  60. @property(nonatomic) id URLSessionMock;
  61. @property(strong, readonly, nonatomic) FIRMessagingAuthService *authService;
  62. @property(strong, readonly, nonatomic) id mockAuthService;
  63. @property(strong, readonly, nonatomic) id mockTokenStore;
  64. @property(strong, readonly, nonatomic) FIRMessagingCheckinService *checkinService;
  65. @property(strong, readonly, nonatomic) id mockCheckinService;
  66. @property(strong, readonly, nonatomic) id mockInstallations;
  67. @property(strong, readonly, nonatomic) id mockHeartbeatInfo;
  68. @property(strong, readonly, nonatomic) NSString *instanceID;
  69. @property(nonatomic, readwrite, strong) FIRMessagingCheckinPreferences *checkinPreferences;
  70. @end
  71. @implementation FIRMessagingTokenOperationsTest
  72. - (void)setUp {
  73. [super setUp];
  74. // Stub NSURLSession constructor before instantiating FIRMessagingCheckinService to inject
  75. // URLSessionMock.
  76. self.URLSessionMock = OCMClassMock([NSURLSession class]);
  77. OCMStub(ClassMethod([self.URLSessionMock sessionWithConfiguration:[OCMArg any]]))
  78. .andReturn(self.URLSessionMock);
  79. _mockTokenStore = OCMClassMock([FIRMessagingTokenStore class]);
  80. _checkinService = [[FIRMessagingCheckinService alloc] init];
  81. _mockCheckinService = OCMPartialMock(_checkinService);
  82. _authService = [[FIRMessagingAuthService alloc]
  83. initWithCheckinStore:[[FIRMessagingCheckinStore alloc] init]];
  84. _instanceID = @"instanceID";
  85. // `FIRMessagingTokenOperation` uses `FIRInstallations` under the hood to get FIS auth token.
  86. // Stub `FIRInstallations` to avoid using a real object.
  87. [self stubInstallations];
  88. // `FIRMessagingTokenFetchOperation` uses `FIRHeartbeatInfo` to retrieve a heartbeat code.
  89. // Stub `FIRHeartbeatInfo` to avoid using a real object.
  90. [self stubHeartbeatInfo];
  91. }
  92. - (void)tearDown {
  93. _authService = nil;
  94. [_mockCheckinService stopMocking];
  95. _mockCheckinService = nil;
  96. _checkinService = nil;
  97. _mockTokenStore = nil;
  98. [_mockInstallations stopMocking];
  99. [_mockHeartbeatInfo stopMocking];
  100. }
  101. - (void)testThatTokenOperationsAuthHeaderStringMatchesCheckin {
  102. int64_t tenHoursAgo = FIRMessagingCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000;
  103. FIRMessagingCheckinPreferences *checkin =
  104. [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo];
  105. NSString *expectedAuthHeader = [FIRMessagingTokenOperation HTTPAuthHeaderFromCheckin:checkin];
  106. XCTestExpectation *authHeaderMatchesCheckinExpectation =
  107. [self expectationWithDescription:@"Auth header string in request matches checkin info"];
  108. FIRMessagingTokenFetchOperation *operation =
  109. [[FIRMessagingTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  110. scope:kScope
  111. options:nil
  112. checkinPreferences:checkin
  113. instanceID:self.instanceID];
  114. NSURL *expectedRequestURL = [NSURL URLWithString:FIRMessagingTokenRegisterServer()];
  115. NSHTTPURLResponse *expectedResponse = [[NSHTTPURLResponse alloc] initWithURL:expectedRequestURL
  116. statusCode:200
  117. HTTPVersion:@"HTTP/1.1"
  118. headerFields:nil];
  119. [FIRURLSessionOCMockStub
  120. stubURLSessionDataTaskWithResponse:expectedResponse
  121. body:[self dataForResponseWithValidToken:YES]
  122. error:nil
  123. URLSessionMock:self.URLSessionMock
  124. requestValidationBlock:^BOOL(NSURLRequest *_Nonnull sentRequest) {
  125. NSDictionary<NSString *, NSString *> *headers = sentRequest.allHTTPHeaderFields;
  126. NSString *authHeader = headers[@"Authorization"];
  127. if ([authHeader isEqualToString:expectedAuthHeader]) {
  128. [authHeaderMatchesCheckinExpectation fulfill];
  129. }
  130. return YES;
  131. }];
  132. [operation start];
  133. [self waitForExpectationsWithTimeout:0.25
  134. handler:^(NSError *_Nullable error) {
  135. XCTAssertNil(error.localizedDescription);
  136. }];
  137. }
  138. - (void)testThatTokenOperationWithoutCheckInFails {
  139. // If asserts are enabled, test for the assert to be thrown, otherwise check for the resulting
  140. // error in the completion handler.
  141. XCTestExpectation *failedExpectation =
  142. [self expectationWithDescription:@"Operation failed without checkin info"];
  143. // This will return hasCheckinInfo == NO
  144. FIRMessagingCheckinPreferences *emptyCheckinPreferences =
  145. [[FIRMessagingCheckinPreferences alloc] initWithDeviceID:@"" secretToken:@""];
  146. FIRMessagingTokenOperation *operation =
  147. [[FIRMessagingTokenOperation alloc] initWithAction:FIRMessagingTokenActionFetch
  148. forAuthorizedEntity:kAuthorizedEntity
  149. scope:kScope
  150. options:nil
  151. checkinPreferences:emptyCheckinPreferences
  152. instanceID:self.instanceID];
  153. [operation addCompletionHandler:^(FIRMessagingTokenOperationResult result,
  154. NSString *_Nullable token, NSError *_Nullable error) {
  155. [failedExpectation fulfill];
  156. }];
  157. @try {
  158. [operation start];
  159. } @catch (NSException *exception) {
  160. if (exception.name == NSInternalInconsistencyException) {
  161. [failedExpectation fulfill];
  162. }
  163. } @finally {
  164. }
  165. [self waitForExpectationsWithTimeout:0.25
  166. handler:^(NSError *_Nullable error) {
  167. XCTAssertNil(error.localizedDescription);
  168. }];
  169. }
  170. - (void)testThatAnAlreadyCancelledOperationFinishesWithoutStarting {
  171. XCTestExpectation *cancelledExpectation =
  172. [self expectationWithDescription:@"Operation finished as cancelled"];
  173. XCTestExpectation *didNotCallPerform =
  174. [self expectationWithDescription:@"Did not call performTokenOperation"];
  175. int64_t tenHoursAgo = FIRMessagingCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000;
  176. FIRMessagingCheckinPreferences *checkinPreferences =
  177. [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo];
  178. FIRMessagingTokenOperationFake *operation =
  179. [[FIRMessagingTokenOperationFake alloc] initWithAction:FIRMessagingTokenActionFetch
  180. forAuthorizedEntity:kAuthorizedEntity
  181. scope:kScope
  182. options:nil
  183. checkinPreferences:checkinPreferences
  184. instanceID:self.instanceID];
  185. operation.performWasCalled = NO;
  186. __weak FIRMessagingTokenOperationFake *weakOperation = operation;
  187. [operation addCompletionHandler:^(FIRMessagingTokenOperationResult result,
  188. NSString *_Nullable token, NSError *_Nullable error) {
  189. if (result == FIRMessagingTokenOperationCancelled) {
  190. [cancelledExpectation fulfill];
  191. }
  192. if (!weakOperation.performWasCalled) {
  193. [didNotCallPerform fulfill];
  194. }
  195. }];
  196. [operation cancel];
  197. [operation start];
  198. [self waitForExpectationsWithTimeout:0.25
  199. handler:^(NSError *_Nullable error) {
  200. XCTAssertNil(error.localizedDescription);
  201. }];
  202. }
  203. - (void)testThatOptionsDictionaryIsIncludedWithFetchRequest {
  204. XCTestExpectation *optionsIncludedExpectation =
  205. [self expectationWithDescription:@"Options keys were included in token URL request"];
  206. int64_t tenHoursAgo = FIRMessagingCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000;
  207. FIRMessagingCheckinPreferences *checkinPreferences =
  208. [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo];
  209. NSData *fakeDeviceToken = [@"fakeAPNSToken" dataUsingEncoding:NSUTF8StringEncoding];
  210. BOOL isSandbox = NO;
  211. NSString *apnsTupleString =
  212. FIRMessagingAPNSTupleStringForTokenAndServerType(fakeDeviceToken, isSandbox);
  213. NSDictionary *options = @{
  214. kFIRMessagingTokenOptionsFirebaseAppIDKey : @"fakeGMPAppID",
  215. kFIRMessagingTokenOptionsAPNSKey : fakeDeviceToken,
  216. kFIRMessagingTokenOptionsAPNSIsSandboxKey : @(isSandbox),
  217. };
  218. FIRMessagingTokenFetchOperation *operation =
  219. [[FIRMessagingTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  220. scope:kScope
  221. options:options
  222. checkinPreferences:checkinPreferences
  223. instanceID:self.instanceID];
  224. NSURL *expectedRequestURL = [NSURL URLWithString:FIRMessagingTokenRegisterServer()];
  225. NSHTTPURLResponse *expectedResponse = [[NSHTTPURLResponse alloc] initWithURL:expectedRequestURL
  226. statusCode:200
  227. HTTPVersion:@"HTTP/1.1"
  228. headerFields:nil];
  229. [FIRURLSessionOCMockStub
  230. stubURLSessionDataTaskWithResponse:expectedResponse
  231. body:[self dataForResponseWithValidToken:YES]
  232. error:nil
  233. URLSessionMock:self.URLSessionMock
  234. requestValidationBlock:^BOOL(NSURLRequest *_Nonnull sentRequest) {
  235. NSString *query = [[NSString alloc] initWithData:sentRequest.HTTPBody
  236. encoding:NSUTF8StringEncoding];
  237. NSString *gmpAppIDQueryTuple = [NSString
  238. stringWithFormat:@"%@=%@", kFIRMessagingTokenOptionsFirebaseAppIDKey,
  239. options[kFIRMessagingTokenOptionsFirebaseAppIDKey]];
  240. NSRange gmpAppIDRange = [query rangeOfString:gmpAppIDQueryTuple];
  241. NSString *apnsQueryTuple =
  242. [NSString stringWithFormat:@"%@=%@", kFIRMessagingTokenOptionsAPNSKey,
  243. apnsTupleString];
  244. NSRange apnsRange = [query rangeOfString:apnsQueryTuple];
  245. if (gmpAppIDRange.location != NSNotFound && apnsRange.location != NSNotFound) {
  246. [optionsIncludedExpectation fulfill];
  247. }
  248. return YES;
  249. }];
  250. [operation start];
  251. [self waitForExpectationsWithTimeout:0.25
  252. handler:^(NSError *_Nullable error) {
  253. XCTAssertNil(error.localizedDescription);
  254. }];
  255. }
  256. - (void)testServerResetCommand {
  257. XCTestExpectation *shouldResetIdentityExpectation =
  258. [self expectationWithDescription:
  259. @"When server sends down RST error, clients should return reset identity error."];
  260. int64_t tenHoursAgo = FIRMessagingCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000;
  261. FIRMessagingCheckinPreferences *checkinPreferences =
  262. [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo];
  263. FIRMessagingTokenFetchOperation *operation =
  264. [[FIRMessagingTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  265. scope:kScope
  266. options:nil
  267. checkinPreferences:checkinPreferences
  268. instanceID:self.instanceID];
  269. NSURL *expectedRequestURL = [NSURL URLWithString:FIRMessagingTokenRegisterServer()];
  270. NSHTTPURLResponse *expectedResponse = [[NSHTTPURLResponse alloc] initWithURL:expectedRequestURL
  271. statusCode:200
  272. HTTPVersion:@"HTTP/1.1"
  273. headerFields:nil];
  274. [FIRURLSessionOCMockStub
  275. stubURLSessionDataTaskWithResponse:expectedResponse
  276. body:[self dataForResponseWithValidToken:NO]
  277. error:nil
  278. URLSessionMock:self.URLSessionMock
  279. requestValidationBlock:^BOOL(NSURLRequest *_Nonnull sentRequest) {
  280. return YES;
  281. }];
  282. [operation addCompletionHandler:^(FIRMessagingTokenOperationResult result,
  283. NSString *_Nullable token, NSError *_Nullable error) {
  284. XCTAssertEqual(result, FIRMessagingTokenOperationError);
  285. XCTAssertNotNil(error);
  286. XCTAssertEqual(error.code, kFIRMessagingErrorCodeInvalidIdentity);
  287. [shouldResetIdentityExpectation fulfill];
  288. }];
  289. [operation start];
  290. [self waitForExpectationsWithTimeout:0.25
  291. handler:^(NSError *_Nullable error) {
  292. XCTAssertNil(error.localizedDescription);
  293. }];
  294. }
  295. - (void)testHTTPAuthHeaderGenerationFromCheckin {
  296. FIRMessagingCheckinPreferences *checkinPreferences =
  297. [[FIRMessagingCheckinPreferences alloc] initWithDeviceID:kDeviceID secretToken:kSecretToken];
  298. NSString *expectedHeader =
  299. [NSString stringWithFormat:@"AidLogin %@:%@", checkinPreferences.deviceID,
  300. checkinPreferences.secretToken];
  301. NSString *generatedHeader =
  302. [FIRMessagingTokenOperation HTTPAuthHeaderFromCheckin:checkinPreferences];
  303. XCTAssertEqualObjects(generatedHeader, expectedHeader);
  304. }
  305. - (void)testTokenFetchOperationFirebaseUserAgentAndHeartbeatHeader {
  306. XCTestExpectation *completionExpectation =
  307. [self expectationWithDescription:@"completionExpectation"];
  308. FIRMessagingCheckinPreferences *checkinPreferences =
  309. [self setCheckinPreferencesWithLastCheckinTime:0];
  310. FIRMessagingTokenFetchOperation *operation =
  311. [[FIRMessagingTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  312. scope:kScope
  313. options:nil
  314. checkinPreferences:checkinPreferences
  315. instanceID:self.instanceID];
  316. NSURL *expectedRequestURL = [NSURL URLWithString:FIRMessagingTokenRegisterServer()];
  317. NSHTTPURLResponse *expectedResponse = [[NSHTTPURLResponse alloc] initWithURL:expectedRequestURL
  318. statusCode:200
  319. HTTPVersion:@"HTTP/1.1"
  320. headerFields:nil];
  321. [FIRURLSessionOCMockStub
  322. stubURLSessionDataTaskWithResponse:expectedResponse
  323. body:[self dataForResponseWithValidToken:NO]
  324. error:nil
  325. URLSessionMock:self.URLSessionMock
  326. requestValidationBlock:^BOOL(NSURLRequest *_Nonnull sentRequest) {
  327. NSString *userAgentValue =
  328. sentRequest.allHTTPHeaderFields[kFIRMessagingFirebaseUserAgentKey];
  329. XCTAssertEqualObjects(userAgentValue, [FIRApp firebaseUserAgent]);
  330. NSString *heartBeatCode =
  331. sentRequest.allHTTPHeaderFields[kFIRMessagingFirebaseHeartbeatKey];
  332. // It is expected that both the SDK and global heartbeat are requested.
  333. XCTAssertEqual(heartBeatCode.integerValue, FIRHeartbeatInfoCodeCombined,
  334. @"Heartbeat storage info needed to be updated but was not.");
  335. [completionExpectation fulfill];
  336. return YES;
  337. }];
  338. [operation start];
  339. [self waitForExpectationsWithTimeout:0.25
  340. handler:^(NSError *_Nullable error) {
  341. XCTAssertNil(error.localizedDescription);
  342. }];
  343. }
  344. #pragma mark - Internal Helpers
  345. - (NSData *)dataForResponseWithValidToken:(BOOL)validToken {
  346. NSString *response;
  347. if (validToken) {
  348. response = [NSString stringWithFormat:@"token=%@", kRegistrationToken];
  349. } else {
  350. response = @"Error=RST";
  351. }
  352. return [response dataUsingEncoding:NSUTF8StringEncoding];
  353. }
  354. - (FIRMessagingCheckinPreferences *)setCheckinPreferencesWithLastCheckinTime:(int64_t)time {
  355. FIRMessagingCheckinPreferences *checkinPreferences =
  356. [[FIRMessagingCheckinPreferences alloc] initWithDeviceID:kDeviceID secretToken:kSecretToken];
  357. NSDictionary *checkinPlistContents = @{
  358. kFIRMessagingDigestStringKey : kDigestString,
  359. kFIRMessagingVersionInfoStringKey : kVersionInfoString,
  360. kFIRMessagingLastCheckinTimeKey : @(time)
  361. };
  362. [checkinPreferences updateWithCheckinPlistContents:checkinPlistContents];
  363. // manually initialize the checkin preferences
  364. self.checkinPreferences = checkinPreferences;
  365. return checkinPreferences;
  366. }
  367. - (void)stubInstallations {
  368. _mockInstallations = OCMClassMock([FIRInstallations class]);
  369. OCMStub([_mockInstallations installations]).andReturn(_mockInstallations);
  370. FIRInstallationsAuthTokenResult *authToken =
  371. [[FIRInstallationsAuthTokenResult alloc] initWithToken:@"fis-auth-token"
  372. expirationDate:[NSDate distantFuture]];
  373. id authTokenWithCompletionArg = [OCMArg invokeBlockWithArgs:authToken, [NSNull null], nil];
  374. OCMStub([_mockInstallations authTokenWithCompletion:authTokenWithCompletionArg]);
  375. }
  376. - (void)stubHeartbeatInfo {
  377. _mockHeartbeatInfo = OCMClassMock([FIRHeartbeatInfo class]);
  378. OCMStub([_mockHeartbeatInfo heartbeatCodeForTag:@"fire-iid"])
  379. .andReturn(FIRHeartbeatInfoCodeCombined);
  380. }
  381. @end