FIRMessagingTokenOperationsTest.m 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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.h"
  18. #import <GoogleUtilities/GULHeartbeatDateStorage.h>
  19. #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
  20. #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.h"
  21. #import "FirebaseMessaging/Sources/FIRMessagingConstants.h"
  22. #import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
  23. #import "FirebaseMessaging/Sources/Token/FIRMessagingAuthService.h"
  24. #import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinPreferences.h"
  25. #import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinService.h"
  26. #import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinStore.h"
  27. #import "FirebaseMessaging/Sources/Token/FIRMessagingKeychain.h"
  28. #import "FirebaseMessaging/Sources/Token/FIRMessagingTokenDeleteOperation.h"
  29. #import "FirebaseMessaging/Sources/Token/FIRMessagingTokenFetchOperation.h"
  30. #import "FirebaseMessaging/Sources/Token/FIRMessagingTokenOperation.h"
  31. #import "FirebaseMessaging/Sources/Token/FIRMessagingTokenStore.h"
  32. #import "SharedTestUtilities/URLSession/FIRURLSessionOCMockStub.h"
  33. static NSString *kDeviceID = @"fakeDeviceID";
  34. static NSString *kSecretToken = @"fakeSecretToken";
  35. static NSString *kDigestString = @"test-digest";
  36. static NSString *kVersionInfoString = @"version_info-1.0.0";
  37. static NSString *kAuthorizedEntity = @"sender-1234567";
  38. static NSString *kScope = @"fcm";
  39. static NSString *kRegistrationToken = @"token-12345";
  40. @interface FIRMessagingTokenOperation (ExposedForTest)
  41. - (void)performTokenOperation;
  42. + (NSString *)HTTPAuthHeaderFromCheckin:(FIRMessagingCheckinPreferences *)checkin;
  43. @end
  44. @interface FIRInstallationsAuthTokenResult (Tests)
  45. - (instancetype)initWithToken:(NSString *)token expirationDate:(NSDate *)expirationDate;
  46. @end
  47. // A Fake operation that allows us to check that perform was called.
  48. // We are not using mocks here because we have no way of forcing NSOperationQueues to release
  49. // their operations, and this means that there is always going to be a race condition between
  50. // when we "stop" our partial mock vs when NSOperationQueue attempts to access the mock object on a
  51. // separate thread. We had mocks previously.
  52. @interface FIRMessagingTokenOperationFake : FIRMessagingTokenOperation
  53. @property(nonatomic, assign) BOOL performWasCalled;
  54. @end
  55. @implementation FIRMessagingTokenOperationFake
  56. - (void)performTokenOperation {
  57. self.performWasCalled = YES;
  58. }
  59. @end
  60. @interface FIRMessagingTokenOperationsTest : XCTestCase
  61. @property(nonatomic) id URLSessionMock;
  62. @property(strong, readonly, nonatomic) FIRMessagingAuthService *authService;
  63. @property(strong, readonly, nonatomic) id mockAuthService;
  64. @property(strong, readonly, nonatomic) id mockTokenStore;
  65. @property(strong, readonly, nonatomic) FIRMessagingCheckinService *checkinService;
  66. @property(strong, readonly, nonatomic) id mockCheckinService;
  67. @property(strong, readonly, nonatomic) id mockInstallations;
  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. NSString *const kHeartbeatStorageFile = @"HEARTBEAT_INFO_STORAGE";
  89. GULHeartbeatDateStorage *dataStorage =
  90. [[GULHeartbeatDateStorage alloc] initWithFileName:kHeartbeatStorageFile];
  91. [[NSFileManager defaultManager] removeItemAtURL:[dataStorage fileURL] error:nil];
  92. }
  93. - (void)tearDown {
  94. _authService = nil;
  95. [_mockCheckinService stopMocking];
  96. _mockCheckinService = nil;
  97. _checkinService = nil;
  98. _mockTokenStore = nil;
  99. [_mockInstallations 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. XCTAssertEqualObjects(heartBeatCode, @"3");
  333. [completionExpectation fulfill];
  334. return YES;
  335. }];
  336. [operation start];
  337. [self waitForExpectationsWithTimeout:0.25
  338. handler:^(NSError *_Nullable error) {
  339. XCTAssertNil(error.localizedDescription);
  340. }];
  341. }
  342. #pragma mark - Internal Helpers
  343. - (NSData *)dataForResponseWithValidToken:(BOOL)validToken {
  344. NSString *response;
  345. if (validToken) {
  346. response = [NSString stringWithFormat:@"token=%@", kRegistrationToken];
  347. } else {
  348. response = @"Error=RST";
  349. }
  350. return [response dataUsingEncoding:NSUTF8StringEncoding];
  351. }
  352. - (FIRMessagingCheckinPreferences *)setCheckinPreferencesWithLastCheckinTime:(int64_t)time {
  353. FIRMessagingCheckinPreferences *checkinPreferences =
  354. [[FIRMessagingCheckinPreferences alloc] initWithDeviceID:kDeviceID secretToken:kSecretToken];
  355. NSDictionary *checkinPlistContents = @{
  356. kFIRMessagingDigestStringKey : kDigestString,
  357. kFIRMessagingVersionInfoStringKey : kVersionInfoString,
  358. kFIRMessagingLastCheckinTimeKey : @(time)
  359. };
  360. [checkinPreferences updateWithCheckinPlistContents:checkinPlistContents];
  361. // manually initialize the checkin preferences
  362. self.checkinPreferences = checkinPreferences;
  363. return checkinPreferences;
  364. }
  365. - (void)stubInstallations {
  366. _mockInstallations = OCMClassMock([FIRInstallations class]);
  367. OCMStub([_mockInstallations installations]).andReturn(_mockInstallations);
  368. FIRInstallationsAuthTokenResult *authToken =
  369. [[FIRInstallationsAuthTokenResult alloc] initWithToken:@"fis-auth-token"
  370. expirationDate:[NSDate distantFuture]];
  371. id authTokenWithCompletionArg = [OCMArg invokeBlockWithArgs:authToken, [NSNull null], nil];
  372. OCMStub([_mockInstallations authTokenWithCompletion:authTokenWithCompletionArg]);
  373. }
  374. @end