FIRMessagingTokenOperationsTest.m 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  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] init];
  83. _instanceID = @"instanceID";
  84. // `FIRMessagingTokenOperation` uses `FIRInstallations` under the hood to get FIS auth token.
  85. // Stub `FIRInstallations` to avoid using a real object.
  86. [self stubInstallations];
  87. // `FIRMessagingTokenFetchOperation` uses `FIRHeartbeatInfo` to retrieve a heartbeat code.
  88. // Stub `FIRHeartbeatInfo` to avoid using a real object.
  89. [self stubHeartbeatInfo];
  90. }
  91. - (void)tearDown {
  92. _authService = nil;
  93. [_mockCheckinService stopMocking];
  94. _mockCheckinService = nil;
  95. _checkinService = nil;
  96. _mockTokenStore = nil;
  97. [_mockInstallations stopMocking];
  98. [_mockHeartbeatInfo stopMocking];
  99. }
  100. - (void)testThatTokenOperationsAuthHeaderStringMatchesCheckin {
  101. int64_t tenHoursAgo = FIRMessagingCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000;
  102. FIRMessagingCheckinPreferences *checkin =
  103. [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo];
  104. NSString *expectedAuthHeader = [FIRMessagingTokenOperation HTTPAuthHeaderFromCheckin:checkin];
  105. XCTestExpectation *authHeaderMatchesCheckinExpectation =
  106. [self expectationWithDescription:@"Auth header string in request matches checkin info"];
  107. FIRMessagingTokenFetchOperation *operation =
  108. [[FIRMessagingTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  109. scope:kScope
  110. options:nil
  111. checkinPreferences:checkin
  112. instanceID:self.instanceID];
  113. NSURL *expectedRequestURL = [NSURL URLWithString:FIRMessagingTokenRegisterServer()];
  114. NSHTTPURLResponse *expectedResponse = [[NSHTTPURLResponse alloc] initWithURL:expectedRequestURL
  115. statusCode:200
  116. HTTPVersion:@"HTTP/1.1"
  117. headerFields:nil];
  118. [FIRURLSessionOCMockStub
  119. stubURLSessionDataTaskWithResponse:expectedResponse
  120. body:[self dataForResponseWithValidToken:YES]
  121. error:nil
  122. URLSessionMock:self.URLSessionMock
  123. requestValidationBlock:^BOOL(NSURLRequest *_Nonnull sentRequest) {
  124. NSDictionary<NSString *, NSString *> *headers = sentRequest.allHTTPHeaderFields;
  125. NSString *authHeader = headers[@"Authorization"];
  126. if ([authHeader isEqualToString:expectedAuthHeader]) {
  127. [authHeaderMatchesCheckinExpectation fulfill];
  128. }
  129. return YES;
  130. }];
  131. [operation start];
  132. [self waitForExpectationsWithTimeout:0.25
  133. handler:^(NSError *_Nullable error) {
  134. XCTAssertNil(error.localizedDescription);
  135. }];
  136. }
  137. - (void)testThatTokenOperationWithoutCheckInFails {
  138. // If asserts are enabled, test for the assert to be thrown, otherwise check for the resulting
  139. // error in the completion handler.
  140. XCTestExpectation *failedExpectation =
  141. [self expectationWithDescription:@"Operation failed without checkin info"];
  142. // This will return hasCheckinInfo == NO
  143. FIRMessagingCheckinPreferences *emptyCheckinPreferences =
  144. [[FIRMessagingCheckinPreferences alloc] initWithDeviceID:@"" secretToken:@""];
  145. FIRMessagingTokenOperation *operation =
  146. [[FIRMessagingTokenOperation alloc] initWithAction:FIRMessagingTokenActionFetch
  147. forAuthorizedEntity:kAuthorizedEntity
  148. scope:kScope
  149. options:nil
  150. checkinPreferences:emptyCheckinPreferences
  151. instanceID:self.instanceID];
  152. [operation addCompletionHandler:^(FIRMessagingTokenOperationResult result,
  153. NSString *_Nullable token, NSError *_Nullable error) {
  154. [failedExpectation fulfill];
  155. }];
  156. @try {
  157. [operation start];
  158. } @catch (NSException *exception) {
  159. if (exception.name == NSInternalInconsistencyException) {
  160. [failedExpectation fulfill];
  161. }
  162. } @finally {
  163. }
  164. [self waitForExpectationsWithTimeout:0.25
  165. handler:^(NSError *_Nullable error) {
  166. XCTAssertNil(error.localizedDescription);
  167. }];
  168. }
  169. - (void)testThatAnAlreadyCancelledOperationFinishesWithoutStarting {
  170. XCTestExpectation *cancelledExpectation =
  171. [self expectationWithDescription:@"Operation finished as cancelled"];
  172. XCTestExpectation *didNotCallPerform =
  173. [self expectationWithDescription:@"Did not call performTokenOperation"];
  174. int64_t tenHoursAgo = FIRMessagingCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000;
  175. FIRMessagingCheckinPreferences *checkinPreferences =
  176. [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo];
  177. FIRMessagingTokenOperationFake *operation =
  178. [[FIRMessagingTokenOperationFake alloc] initWithAction:FIRMessagingTokenActionFetch
  179. forAuthorizedEntity:kAuthorizedEntity
  180. scope:kScope
  181. options:nil
  182. checkinPreferences:checkinPreferences
  183. instanceID:self.instanceID];
  184. operation.performWasCalled = NO;
  185. __weak FIRMessagingTokenOperationFake *weakOperation = operation;
  186. [operation addCompletionHandler:^(FIRMessagingTokenOperationResult result,
  187. NSString *_Nullable token, NSError *_Nullable error) {
  188. if (result == FIRMessagingTokenOperationCancelled) {
  189. [cancelledExpectation fulfill];
  190. }
  191. if (!weakOperation.performWasCalled) {
  192. [didNotCallPerform fulfill];
  193. }
  194. }];
  195. [operation cancel];
  196. [operation start];
  197. [self waitForExpectationsWithTimeout:0.25
  198. handler:^(NSError *_Nullable error) {
  199. XCTAssertNil(error.localizedDescription);
  200. }];
  201. }
  202. - (void)testThatOptionsDictionaryIsIncludedWithFetchRequest {
  203. XCTestExpectation *optionsIncludedExpectation =
  204. [self expectationWithDescription:@"Options keys were included in token URL request"];
  205. int64_t tenHoursAgo = FIRMessagingCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000;
  206. FIRMessagingCheckinPreferences *checkinPreferences =
  207. [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo];
  208. NSData *fakeDeviceToken = [@"fakeAPNSToken" dataUsingEncoding:NSUTF8StringEncoding];
  209. BOOL isSandbox = NO;
  210. NSString *apnsTupleString =
  211. FIRMessagingAPNSTupleStringForTokenAndServerType(fakeDeviceToken, isSandbox);
  212. NSDictionary *options = @{
  213. kFIRMessagingTokenOptionsFirebaseAppIDKey : @"fakeGMPAppID",
  214. kFIRMessagingTokenOptionsAPNSKey : fakeDeviceToken,
  215. kFIRMessagingTokenOptionsAPNSIsSandboxKey : @(isSandbox),
  216. };
  217. FIRMessagingTokenFetchOperation *operation =
  218. [[FIRMessagingTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  219. scope:kScope
  220. options:options
  221. checkinPreferences:checkinPreferences
  222. instanceID:self.instanceID];
  223. NSURL *expectedRequestURL = [NSURL URLWithString:FIRMessagingTokenRegisterServer()];
  224. NSHTTPURLResponse *expectedResponse = [[NSHTTPURLResponse alloc] initWithURL:expectedRequestURL
  225. statusCode:200
  226. HTTPVersion:@"HTTP/1.1"
  227. headerFields:nil];
  228. [FIRURLSessionOCMockStub
  229. stubURLSessionDataTaskWithResponse:expectedResponse
  230. body:[self dataForResponseWithValidToken:YES]
  231. error:nil
  232. URLSessionMock:self.URLSessionMock
  233. requestValidationBlock:^BOOL(NSURLRequest *_Nonnull sentRequest) {
  234. NSString *query = [[NSString alloc] initWithData:sentRequest.HTTPBody
  235. encoding:NSUTF8StringEncoding];
  236. NSString *gmpAppIDQueryTuple = [NSString
  237. stringWithFormat:@"%@=%@", kFIRMessagingTokenOptionsFirebaseAppIDKey,
  238. options[kFIRMessagingTokenOptionsFirebaseAppIDKey]];
  239. NSRange gmpAppIDRange = [query rangeOfString:gmpAppIDQueryTuple];
  240. NSString *apnsQueryTuple =
  241. [NSString stringWithFormat:@"%@=%@", kFIRMessagingTokenOptionsAPNSKey,
  242. apnsTupleString];
  243. NSRange apnsRange = [query rangeOfString:apnsQueryTuple];
  244. if (gmpAppIDRange.location != NSNotFound && apnsRange.location != NSNotFound) {
  245. [optionsIncludedExpectation fulfill];
  246. }
  247. return YES;
  248. }];
  249. [operation start];
  250. [self waitForExpectationsWithTimeout:0.25
  251. handler:^(NSError *_Nullable error) {
  252. XCTAssertNil(error.localizedDescription);
  253. }];
  254. }
  255. - (void)testServerResetCommand {
  256. XCTestExpectation *shouldResetIdentityExpectation =
  257. [self expectationWithDescription:
  258. @"When server sends down RST error, clients should return reset identity error."];
  259. int64_t tenHoursAgo = FIRMessagingCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000;
  260. FIRMessagingCheckinPreferences *checkinPreferences =
  261. [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo];
  262. FIRMessagingTokenFetchOperation *operation =
  263. [[FIRMessagingTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  264. scope:kScope
  265. options:nil
  266. checkinPreferences:checkinPreferences
  267. instanceID:self.instanceID];
  268. NSURL *expectedRequestURL = [NSURL URLWithString:FIRMessagingTokenRegisterServer()];
  269. NSHTTPURLResponse *expectedResponse = [[NSHTTPURLResponse alloc] initWithURL:expectedRequestURL
  270. statusCode:200
  271. HTTPVersion:@"HTTP/1.1"
  272. headerFields:nil];
  273. [FIRURLSessionOCMockStub
  274. stubURLSessionDataTaskWithResponse:expectedResponse
  275. body:[self dataForResponseWithValidToken:NO]
  276. error:nil
  277. URLSessionMock:self.URLSessionMock
  278. requestValidationBlock:^BOOL(NSURLRequest *_Nonnull sentRequest) {
  279. return YES;
  280. }];
  281. [operation addCompletionHandler:^(FIRMessagingTokenOperationResult result,
  282. NSString *_Nullable token, NSError *_Nullable error) {
  283. XCTAssertEqual(result, FIRMessagingTokenOperationError);
  284. XCTAssertNotNil(error);
  285. XCTAssertEqual(error.code, kFIRMessagingErrorCodeInvalidIdentity);
  286. [shouldResetIdentityExpectation fulfill];
  287. }];
  288. [operation start];
  289. [self waitForExpectationsWithTimeout:0.25
  290. handler:^(NSError *_Nullable error) {
  291. XCTAssertNil(error.localizedDescription);
  292. }];
  293. }
  294. - (void)testHTTPAuthHeaderGenerationFromCheckin {
  295. FIRMessagingCheckinPreferences *checkinPreferences =
  296. [[FIRMessagingCheckinPreferences alloc] initWithDeviceID:kDeviceID secretToken:kSecretToken];
  297. NSString *expectedHeader =
  298. [NSString stringWithFormat:@"AidLogin %@:%@", checkinPreferences.deviceID,
  299. checkinPreferences.secretToken];
  300. NSString *generatedHeader =
  301. [FIRMessagingTokenOperation HTTPAuthHeaderFromCheckin:checkinPreferences];
  302. XCTAssertEqualObjects(generatedHeader, expectedHeader);
  303. }
  304. - (void)testTokenFetchOperationFirebaseUserAgentAndHeartbeatHeader {
  305. XCTestExpectation *completionExpectation =
  306. [self expectationWithDescription:@"completionExpectation"];
  307. FIRMessagingCheckinPreferences *checkinPreferences =
  308. [self setCheckinPreferencesWithLastCheckinTime:0];
  309. FIRMessagingTokenFetchOperation *operation =
  310. [[FIRMessagingTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  311. scope:kScope
  312. options:nil
  313. checkinPreferences:checkinPreferences
  314. instanceID:self.instanceID];
  315. NSURL *expectedRequestURL = [NSURL URLWithString:FIRMessagingTokenRegisterServer()];
  316. NSHTTPURLResponse *expectedResponse = [[NSHTTPURLResponse alloc] initWithURL:expectedRequestURL
  317. statusCode:200
  318. HTTPVersion:@"HTTP/1.1"
  319. headerFields:nil];
  320. [FIRURLSessionOCMockStub
  321. stubURLSessionDataTaskWithResponse:expectedResponse
  322. body:[self dataForResponseWithValidToken:NO]
  323. error:nil
  324. URLSessionMock:self.URLSessionMock
  325. requestValidationBlock:^BOOL(NSURLRequest *_Nonnull sentRequest) {
  326. NSString *userAgentValue =
  327. sentRequest.allHTTPHeaderFields[kFIRMessagingFirebaseUserAgentKey];
  328. XCTAssertEqualObjects(userAgentValue, [FIRApp firebaseUserAgent]);
  329. NSString *heartBeatCode =
  330. sentRequest.allHTTPHeaderFields[kFIRMessagingFirebaseHeartbeatKey];
  331. // It is expected that both the SDK and global heartbeat are requested.
  332. XCTAssertEqual(heartBeatCode.integerValue, FIRHeartbeatInfoCodeCombined,
  333. @"Heartbeat storage info needed to be updated but was not.");
  334. [completionExpectation fulfill];
  335. return YES;
  336. }];
  337. [operation start];
  338. [self waitForExpectationsWithTimeout:0.25
  339. handler:^(NSError *_Nullable error) {
  340. XCTAssertNil(error.localizedDescription);
  341. }];
  342. }
  343. #pragma mark - Internal Helpers
  344. - (NSData *)dataForResponseWithValidToken:(BOOL)validToken {
  345. NSString *response;
  346. if (validToken) {
  347. response = [NSString stringWithFormat:@"token=%@", kRegistrationToken];
  348. } else {
  349. response = @"Error=RST";
  350. }
  351. return [response dataUsingEncoding:NSUTF8StringEncoding];
  352. }
  353. - (FIRMessagingCheckinPreferences *)setCheckinPreferencesWithLastCheckinTime:(int64_t)time {
  354. FIRMessagingCheckinPreferences *checkinPreferences =
  355. [[FIRMessagingCheckinPreferences alloc] initWithDeviceID:kDeviceID secretToken:kSecretToken];
  356. NSDictionary *checkinPlistContents = @{
  357. kFIRMessagingDigestStringKey : kDigestString,
  358. kFIRMessagingVersionInfoStringKey : kVersionInfoString,
  359. kFIRMessagingLastCheckinTimeKey : @(time)
  360. };
  361. [checkinPreferences updateWithCheckinPlistContents:checkinPlistContents];
  362. // manually initialize the checkin preferences
  363. self.checkinPreferences = checkinPreferences;
  364. return checkinPreferences;
  365. }
  366. - (void)stubInstallations {
  367. _mockInstallations = OCMClassMock([FIRInstallations class]);
  368. OCMStub([_mockInstallations installations]).andReturn(_mockInstallations);
  369. FIRInstallationsAuthTokenResult *authToken =
  370. [[FIRInstallationsAuthTokenResult alloc] initWithToken:@"fis-auth-token"
  371. expirationDate:[NSDate distantFuture]];
  372. id authTokenWithCompletionArg = [OCMArg invokeBlockWithArgs:authToken, [NSNull null], nil];
  373. OCMStub([_mockInstallations authTokenWithCompletion:authTokenWithCompletionArg]);
  374. }
  375. - (void)stubHeartbeatInfo {
  376. _mockHeartbeatInfo = OCMClassMock([FIRHeartbeatInfo class]);
  377. OCMStub([_mockHeartbeatInfo heartbeatCodeForTag:@"fire-iid"])
  378. .andReturn(FIRHeartbeatInfoCodeCombined);
  379. }
  380. @end