FIRInstanceIDTokenOperationsTest.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  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 "Firebase/InstanceID/Public/FIRInstanceID.h"
  18. #import <OCMock/OCMock.h>
  19. #import "Firebase/InstanceID/FIRInstanceIDAuthService.h"
  20. #import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h"
  21. #import "Firebase/InstanceID/FIRInstanceIDCheckinService.h"
  22. #import "Firebase/InstanceID/FIRInstanceIDConstants.h"
  23. #import "Firebase/InstanceID/FIRInstanceIDKeychain.h"
  24. #import "Firebase/InstanceID/FIRInstanceIDStore.h"
  25. #import "Firebase/InstanceID/FIRInstanceIDTokenDeleteOperation.h"
  26. #import "Firebase/InstanceID/FIRInstanceIDTokenFetchOperation.h"
  27. #import "Firebase/InstanceID/FIRInstanceIDTokenOperation+Private.h"
  28. #import "Firebase/InstanceID/FIRInstanceIDTokenOperation.h"
  29. #import "Firebase/InstanceID/NSError+FIRInstanceID.h"
  30. #import <GoogleUtilities/GULHeartbeatDateStorage.h>
  31. #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
  32. #import "FirebaseInstallations/Source/Library/Private/FirebaseInstallationsInternal.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 FIRInstanceIDTokenOperation (ExposedForTest)
  41. - (void)performTokenOperation;
  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 FIRInstanceIDTokenOperationFake : FIRInstanceIDTokenOperation
  52. @property(nonatomic, assign) BOOL performWasCalled;
  53. @end
  54. @implementation FIRInstanceIDTokenOperationFake
  55. - (void)performTokenOperation {
  56. self.performWasCalled = YES;
  57. }
  58. @end
  59. @interface FIRInstanceIDTokenOperationsTest : XCTestCase
  60. @property(strong, readonly, nonatomic) FIRInstanceIDAuthService *authService;
  61. @property(strong, readonly, nonatomic) id mockAuthService;
  62. @property(strong, readonly, nonatomic) id mockStore;
  63. @property(strong, readonly, nonatomic) FIRInstanceIDCheckinService *checkinService;
  64. @property(strong, readonly, nonatomic) id mockCheckinService;
  65. @property(strong, readonly, nonatomic) id mockInstallations;
  66. @property(strong, readonly, nonatomic) NSString *instanceID;
  67. @property(nonatomic, readwrite, strong) FIRInstanceIDCheckinPreferences *checkinPreferences;
  68. @end
  69. @implementation FIRInstanceIDTokenOperationsTest
  70. - (void)setUp {
  71. [super setUp];
  72. _mockStore = OCMClassMock([FIRInstanceIDStore class]);
  73. _checkinService = [[FIRInstanceIDCheckinService alloc] init];
  74. _mockCheckinService = OCMPartialMock(_checkinService);
  75. _authService = [[FIRInstanceIDAuthService alloc] initWithCheckinService:_mockCheckinService
  76. store:_mockStore];
  77. _instanceID = @"instanceID";
  78. // `FIRInstanceIDTokenOperation` uses `FIRInstallations` under the hood to get FIS auth token.
  79. // Stub `FIRInstallations` to avoid using a real object.
  80. [self stubInstallations];
  81. NSString *const kHeartbeatStorageFile = @"HEARTBEAT_INFO_STORAGE";
  82. GULHeartbeatDateStorage *dataStorage =
  83. [[GULHeartbeatDateStorage alloc] initWithFileName:kHeartbeatStorageFile];
  84. [[NSFileManager defaultManager] removeItemAtURL:[dataStorage fileURL] error:nil];
  85. }
  86. - (void)tearDown {
  87. [_mockInstallations stopMocking];
  88. _mockInstallations = nil;
  89. _authService = nil;
  90. [_mockCheckinService stopMocking];
  91. _mockCheckinService = nil;
  92. _checkinService = nil;
  93. _mockStore = nil;
  94. }
  95. - (void)testThatTokenOperationsAuthHeaderStringMatchesCheckin {
  96. int64_t tenHoursAgo = FIRInstanceIDCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000;
  97. FIRInstanceIDCheckinPreferences *checkin =
  98. [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo];
  99. NSString *expectedAuthHeader = [FIRInstanceIDTokenOperation HTTPAuthHeaderFromCheckin:checkin];
  100. XCTestExpectation *authHeaderMatchesCheckinExpectation =
  101. [self expectationWithDescription:@"Auth header string in request matches checkin info"];
  102. FIRInstanceIDTokenFetchOperation *operation =
  103. [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  104. scope:kScope
  105. options:nil
  106. checkinPreferences:checkin
  107. instanceID:self.instanceID];
  108. operation.testBlock =
  109. ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock response) {
  110. NSDictionary<NSString *, NSString *> *headers = request.allHTTPHeaderFields;
  111. NSString *authHeader = headers[@"Authorization"];
  112. if ([authHeader isEqualToString:expectedAuthHeader]) {
  113. [authHeaderMatchesCheckinExpectation fulfill];
  114. }
  115. // Return a response (doesnt matter what the response is)
  116. NSData *responseBody = [self dataForFetchRequest:request returnValidToken:YES];
  117. NSHTTPURLResponse *responseObject = [[NSHTTPURLResponse alloc] initWithURL:request.URL
  118. statusCode:200
  119. HTTPVersion:@"HTTP/1.1"
  120. headerFields:nil];
  121. response(responseBody, responseObject, nil);
  122. };
  123. [operation start];
  124. [self waitForExpectationsWithTimeout:0.25
  125. handler:^(NSError *_Nullable error) {
  126. XCTAssertNil(error.localizedDescription);
  127. }];
  128. }
  129. - (void)testThatTokenOperationWithoutCheckInFails {
  130. // If asserts are enabled, test for the assert to be thrown, otherwise check for the resulting
  131. // error in the completion handler.
  132. XCTestExpectation *failedExpectation =
  133. [self expectationWithDescription:@"Operation failed without checkin info"];
  134. // This will return hasCheckinInfo == NO
  135. FIRInstanceIDCheckinPreferences *emptyCheckinPreferences =
  136. [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:@"" secretToken:@""];
  137. FIRInstanceIDTokenOperation *operation =
  138. [[FIRInstanceIDTokenOperation alloc] initWithAction:FIRInstanceIDTokenActionFetch
  139. forAuthorizedEntity:kAuthorizedEntity
  140. scope:kScope
  141. options:nil
  142. checkinPreferences:emptyCheckinPreferences
  143. instanceID:self.instanceID];
  144. [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
  145. NSString *_Nullable token, NSError *_Nullable error) {
  146. [failedExpectation fulfill];
  147. }];
  148. @try {
  149. [operation start];
  150. } @catch (NSException *exception) {
  151. if (exception.name == NSInternalInconsistencyException) {
  152. [failedExpectation fulfill];
  153. }
  154. } @finally {
  155. }
  156. [self waitForExpectationsWithTimeout:0.25
  157. handler:^(NSError *_Nullable error) {
  158. XCTAssertNil(error.localizedDescription);
  159. }];
  160. }
  161. - (void)testThatAnAlreadyCancelledOperationFinishesWithoutStarting {
  162. XCTestExpectation *cancelledExpectation =
  163. [self expectationWithDescription:@"Operation finished as cancelled"];
  164. XCTestExpectation *didNotCallPerform =
  165. [self expectationWithDescription:@"Did not call performTokenOperation"];
  166. int64_t tenHoursAgo = FIRInstanceIDCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000;
  167. FIRInstanceIDCheckinPreferences *checkinPreferences =
  168. [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo];
  169. FIRInstanceIDTokenOperationFake *operation =
  170. [[FIRInstanceIDTokenOperationFake alloc] initWithAction:FIRInstanceIDTokenActionFetch
  171. forAuthorizedEntity:kAuthorizedEntity
  172. scope:kScope
  173. options:nil
  174. checkinPreferences:checkinPreferences
  175. instanceID:self.instanceID];
  176. operation.performWasCalled = NO;
  177. [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
  178. NSString *_Nullable token, NSError *_Nullable error) {
  179. if (result == FIRInstanceIDTokenOperationCancelled) {
  180. [cancelledExpectation fulfill];
  181. }
  182. if (!operation.performWasCalled) {
  183. [didNotCallPerform fulfill];
  184. }
  185. }];
  186. [operation cancel];
  187. [operation start];
  188. [self waitForExpectationsWithTimeout:0.25
  189. handler:^(NSError *_Nullable error) {
  190. XCTAssertNil(error.localizedDescription);
  191. }];
  192. }
  193. - (void)testThatOptionsDictionaryIsIncludedWithFetchRequest {
  194. XCTestExpectation *optionsIncludedExpectation =
  195. [self expectationWithDescription:@"Options keys were included in token URL request"];
  196. int64_t tenHoursAgo = FIRInstanceIDCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000;
  197. FIRInstanceIDCheckinPreferences *checkinPreferences =
  198. [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo];
  199. NSData *fakeDeviceToken = [@"fakeAPNSToken" dataUsingEncoding:NSUTF8StringEncoding];
  200. BOOL isSandbox = NO;
  201. NSString *apnsTupleString =
  202. FIRInstanceIDAPNSTupleStringForTokenAndServerType(fakeDeviceToken, isSandbox);
  203. NSDictionary *options = @{
  204. kFIRInstanceIDTokenOptionsFirebaseAppIDKey : @"fakeGMPAppID",
  205. kFIRInstanceIDTokenOptionsAPNSKey : fakeDeviceToken,
  206. kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(isSandbox),
  207. };
  208. FIRInstanceIDTokenFetchOperation *operation =
  209. [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  210. scope:kScope
  211. options:options
  212. checkinPreferences:checkinPreferences
  213. instanceID:self.instanceID];
  214. operation.testBlock =
  215. ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock response) {
  216. NSString *query = [[NSString alloc] initWithData:request.HTTPBody
  217. encoding:NSUTF8StringEncoding];
  218. NSString *gmpAppIDQueryTuple =
  219. [NSString stringWithFormat:@"%@=%@", kFIRInstanceIDTokenOptionsFirebaseAppIDKey,
  220. options[kFIRInstanceIDTokenOptionsFirebaseAppIDKey]];
  221. NSRange gmpAppIDRange = [query rangeOfString:gmpAppIDQueryTuple];
  222. NSString *apnsQueryTuple = [NSString
  223. stringWithFormat:@"%@=%@", kFIRInstanceIDTokenOptionsAPNSKey, apnsTupleString];
  224. NSRange apnsRange = [query rangeOfString:apnsQueryTuple];
  225. if (gmpAppIDRange.location != NSNotFound && apnsRange.location != NSNotFound) {
  226. [optionsIncludedExpectation fulfill];
  227. }
  228. // Return a response (doesnt matter what the response is)
  229. NSData *responseBody = [self dataForFetchRequest:request returnValidToken:YES];
  230. NSHTTPURLResponse *responseObject = [[NSHTTPURLResponse alloc] initWithURL:request.URL
  231. statusCode:200
  232. HTTPVersion:@"HTTP/1.1"
  233. headerFields:nil];
  234. response(responseBody, responseObject, nil);
  235. };
  236. [operation start];
  237. [self waitForExpectationsWithTimeout:0.25
  238. handler:^(NSError *_Nullable error) {
  239. XCTAssertNil(error.localizedDescription);
  240. }];
  241. }
  242. - (void)testServerResetCommand {
  243. XCTestExpectation *shouldResetIdentityExpectation =
  244. [self expectationWithDescription:
  245. @"When server sends down RST error, clients should return reset identity error."];
  246. int64_t tenHoursAgo = FIRInstanceIDCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000;
  247. FIRInstanceIDCheckinPreferences *checkinPreferences =
  248. [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo];
  249. FIRInstanceIDTokenFetchOperation *operation =
  250. [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  251. scope:kScope
  252. options:nil
  253. checkinPreferences:checkinPreferences
  254. instanceID:self.instanceID];
  255. operation.testBlock =
  256. ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock response) {
  257. // Return a response with Error=RST
  258. NSData *responseBody = [self dataForFetchRequest:request returnValidToken:NO];
  259. NSHTTPURLResponse *responseObject = [[NSHTTPURLResponse alloc] initWithURL:request.URL
  260. statusCode:200
  261. HTTPVersion:@"HTTP/1.1"
  262. headerFields:nil];
  263. response(responseBody, responseObject, nil);
  264. };
  265. [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
  266. NSString *_Nullable token, NSError *_Nullable error) {
  267. XCTAssertEqual(result, FIRInstanceIDTokenOperationError);
  268. XCTAssertNotNil(error);
  269. XCTAssertEqual(error.code, kFIRInstanceIDErrorCodeInvalidIdentity);
  270. [shouldResetIdentityExpectation fulfill];
  271. }];
  272. [operation start];
  273. [self waitForExpectationsWithTimeout:0.25
  274. handler:^(NSError *_Nullable error) {
  275. XCTAssertNil(error.localizedDescription);
  276. }];
  277. }
  278. - (void)testHTTPAuthHeaderGenerationFromCheckin {
  279. FIRInstanceIDCheckinPreferences *checkinPreferences =
  280. [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceID secretToken:kSecretToken];
  281. NSString *expectedHeader =
  282. [NSString stringWithFormat:@"AidLogin %@:%@", checkinPreferences.deviceID,
  283. checkinPreferences.secretToken];
  284. NSString *generatedHeader =
  285. [FIRInstanceIDTokenOperation HTTPAuthHeaderFromCheckin:checkinPreferences];
  286. XCTAssertEqualObjects(generatedHeader, expectedHeader);
  287. }
  288. - (void)testTokenFetchOperationFirebaseUserAgentAndHeartbeatHeader {
  289. XCTestExpectation *completionExpectation =
  290. [self expectationWithDescription:@"completionExpectation"];
  291. FIRInstanceIDCheckinPreferences *checkinPreferences =
  292. [self setCheckinPreferencesWithLastCheckinTime:0];
  293. FIRInstanceIDTokenFetchOperation *operation =
  294. [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  295. scope:kScope
  296. options:nil
  297. checkinPreferences:checkinPreferences
  298. instanceID:self.instanceID];
  299. operation.testBlock =
  300. ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock response) {
  301. NSString *userAgentValue = request.allHTTPHeaderFields[kFIRInstanceIDFirebaseUserAgentKey];
  302. XCTAssertEqualObjects(userAgentValue, [FIRApp firebaseUserAgent]);
  303. NSString *heartBeatCode = request.allHTTPHeaderFields[kFIRInstanceIDFirebaseHeartbeatKey];
  304. XCTAssertEqualObjects(heartBeatCode, @"3");
  305. // Return a response with Error=RST
  306. NSData *responseBody = [self dataForFetchRequest:request returnValidToken:NO];
  307. NSHTTPURLResponse *responseObject = [[NSHTTPURLResponse alloc] initWithURL:request.URL
  308. statusCode:200
  309. HTTPVersion:@"HTTP/1.1"
  310. headerFields:nil];
  311. response(responseBody, responseObject, nil);
  312. };
  313. [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
  314. NSString *_Nullable token, NSError *_Nullable error) {
  315. [completionExpectation fulfill];
  316. }];
  317. [operation start];
  318. [self waitForExpectationsWithTimeout:0.25
  319. handler:^(NSError *_Nullable error) {
  320. XCTAssertNil(error.localizedDescription);
  321. }];
  322. }
  323. #pragma mark - Internal Helpers
  324. - (NSData *)dataForFetchRequest:(NSURLRequest *)request returnValidToken:(BOOL)returnValidToken {
  325. NSString *response;
  326. if (returnValidToken) {
  327. response = [NSString stringWithFormat:@"token=%@", kRegistrationToken];
  328. } else {
  329. response = @"Error=RST";
  330. }
  331. return [response dataUsingEncoding:NSUTF8StringEncoding];
  332. }
  333. - (FIRInstanceIDCheckinPreferences *)setCheckinPreferencesWithLastCheckinTime:(int64_t)time {
  334. FIRInstanceIDCheckinPreferences *checkinPreferences =
  335. [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceID secretToken:kSecretToken];
  336. NSDictionary *checkinPlistContents = @{
  337. kFIRInstanceIDDigestStringKey : kDigestString,
  338. kFIRInstanceIDVersionInfoStringKey : kVersionInfoString,
  339. kFIRInstanceIDLastCheckinTimeKey : @(time)
  340. };
  341. [checkinPreferences updateWithCheckinPlistContents:checkinPlistContents];
  342. // manually initialize the checkin preferences
  343. self.checkinPreferences = checkinPreferences;
  344. return checkinPreferences;
  345. }
  346. - (void)stubInstallations {
  347. _mockInstallations = OCMClassMock([FIRInstallations class]);
  348. OCMStub([_mockInstallations installations]).andReturn(_mockInstallations);
  349. FIRInstallationsAuthTokenResult *authToken =
  350. [[FIRInstallationsAuthTokenResult alloc] initWithToken:@"fis-auth-token"
  351. expirationDate:[NSDate distantFuture]];
  352. id authTokenWithCompletionArg = [OCMArg invokeBlockWithArgs:authToken, [NSNull null], nil];
  353. OCMStub([_mockInstallations authTokenWithCompletion:authTokenWithCompletionArg]);
  354. }
  355. @end