FIRInstanceIDTokenOperationsTest.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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. __weak FIRInstanceIDTokenOperationFake *weakOperation = operation;
  178. [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
  179. NSString *_Nullable token, NSError *_Nullable error) {
  180. if (result == FIRInstanceIDTokenOperationCancelled) {
  181. [cancelledExpectation fulfill];
  182. }
  183. if (!weakOperation.performWasCalled) {
  184. [didNotCallPerform fulfill];
  185. }
  186. }];
  187. [operation cancel];
  188. [operation start];
  189. [self waitForExpectationsWithTimeout:0.25
  190. handler:^(NSError *_Nullable error) {
  191. XCTAssertNil(error.localizedDescription);
  192. }];
  193. }
  194. - (void)testThatOptionsDictionaryIsIncludedWithFetchRequest {
  195. XCTestExpectation *optionsIncludedExpectation =
  196. [self expectationWithDescription:@"Options keys were included in token URL request"];
  197. int64_t tenHoursAgo = FIRInstanceIDCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000;
  198. FIRInstanceIDCheckinPreferences *checkinPreferences =
  199. [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo];
  200. NSData *fakeDeviceToken = [@"fakeAPNSToken" dataUsingEncoding:NSUTF8StringEncoding];
  201. BOOL isSandbox = NO;
  202. NSString *apnsTupleString =
  203. FIRInstanceIDAPNSTupleStringForTokenAndServerType(fakeDeviceToken, isSandbox);
  204. NSDictionary *options = @{
  205. kFIRInstanceIDTokenOptionsFirebaseAppIDKey : @"fakeGMPAppID",
  206. kFIRInstanceIDTokenOptionsAPNSKey : fakeDeviceToken,
  207. kFIRInstanceIDTokenOptionsAPNSIsSandboxKey : @(isSandbox),
  208. };
  209. FIRInstanceIDTokenFetchOperation *operation =
  210. [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  211. scope:kScope
  212. options:options
  213. checkinPreferences:checkinPreferences
  214. instanceID:self.instanceID];
  215. operation.testBlock =
  216. ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock response) {
  217. NSString *query = [[NSString alloc] initWithData:request.HTTPBody
  218. encoding:NSUTF8StringEncoding];
  219. NSString *gmpAppIDQueryTuple =
  220. [NSString stringWithFormat:@"%@=%@", kFIRInstanceIDTokenOptionsFirebaseAppIDKey,
  221. options[kFIRInstanceIDTokenOptionsFirebaseAppIDKey]];
  222. NSRange gmpAppIDRange = [query rangeOfString:gmpAppIDQueryTuple];
  223. NSString *apnsQueryTuple = [NSString
  224. stringWithFormat:@"%@=%@", kFIRInstanceIDTokenOptionsAPNSKey, apnsTupleString];
  225. NSRange apnsRange = [query rangeOfString:apnsQueryTuple];
  226. if (gmpAppIDRange.location != NSNotFound && apnsRange.location != NSNotFound) {
  227. [optionsIncludedExpectation fulfill];
  228. }
  229. // Return a response (doesnt matter what the response is)
  230. NSData *responseBody = [self dataForFetchRequest:request returnValidToken:YES];
  231. NSHTTPURLResponse *responseObject = [[NSHTTPURLResponse alloc] initWithURL:request.URL
  232. statusCode:200
  233. HTTPVersion:@"HTTP/1.1"
  234. headerFields:nil];
  235. response(responseBody, responseObject, nil);
  236. };
  237. [operation start];
  238. [self waitForExpectationsWithTimeout:0.25
  239. handler:^(NSError *_Nullable error) {
  240. XCTAssertNil(error.localizedDescription);
  241. }];
  242. }
  243. - (void)testServerResetCommand {
  244. XCTestExpectation *shouldResetIdentityExpectation =
  245. [self expectationWithDescription:
  246. @"When server sends down RST error, clients should return reset identity error."];
  247. int64_t tenHoursAgo = FIRInstanceIDCurrentTimestampInMilliseconds() - 10 * 60 * 60 * 1000;
  248. FIRInstanceIDCheckinPreferences *checkinPreferences =
  249. [self setCheckinPreferencesWithLastCheckinTime:tenHoursAgo];
  250. FIRInstanceIDTokenFetchOperation *operation =
  251. [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  252. scope:kScope
  253. options:nil
  254. checkinPreferences:checkinPreferences
  255. instanceID:self.instanceID];
  256. operation.testBlock =
  257. ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock response) {
  258. // Return a response with Error=RST
  259. NSData *responseBody = [self dataForFetchRequest:request returnValidToken:NO];
  260. NSHTTPURLResponse *responseObject = [[NSHTTPURLResponse alloc] initWithURL:request.URL
  261. statusCode:200
  262. HTTPVersion:@"HTTP/1.1"
  263. headerFields:nil];
  264. response(responseBody, responseObject, nil);
  265. };
  266. [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
  267. NSString *_Nullable token, NSError *_Nullable error) {
  268. XCTAssertEqual(result, FIRInstanceIDTokenOperationError);
  269. XCTAssertNotNil(error);
  270. XCTAssertEqual(error.code, kFIRInstanceIDErrorCodeInvalidIdentity);
  271. [shouldResetIdentityExpectation fulfill];
  272. }];
  273. [operation start];
  274. [self waitForExpectationsWithTimeout:0.25
  275. handler:^(NSError *_Nullable error) {
  276. XCTAssertNil(error.localizedDescription);
  277. }];
  278. }
  279. - (void)testHTTPAuthHeaderGenerationFromCheckin {
  280. FIRInstanceIDCheckinPreferences *checkinPreferences =
  281. [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceID secretToken:kSecretToken];
  282. NSString *expectedHeader =
  283. [NSString stringWithFormat:@"AidLogin %@:%@", checkinPreferences.deviceID,
  284. checkinPreferences.secretToken];
  285. NSString *generatedHeader =
  286. [FIRInstanceIDTokenOperation HTTPAuthHeaderFromCheckin:checkinPreferences];
  287. XCTAssertEqualObjects(generatedHeader, expectedHeader);
  288. }
  289. - (void)testTokenFetchOperationFirebaseUserAgentAndHeartbeatHeader {
  290. XCTestExpectation *completionExpectation =
  291. [self expectationWithDescription:@"completionExpectation"];
  292. FIRInstanceIDCheckinPreferences *checkinPreferences =
  293. [self setCheckinPreferencesWithLastCheckinTime:0];
  294. FIRInstanceIDTokenFetchOperation *operation =
  295. [[FIRInstanceIDTokenFetchOperation alloc] initWithAuthorizedEntity:kAuthorizedEntity
  296. scope:kScope
  297. options:nil
  298. checkinPreferences:checkinPreferences
  299. instanceID:self.instanceID];
  300. operation.testBlock =
  301. ^(NSURLRequest *request, FIRInstanceIDURLRequestTestResponseBlock response) {
  302. NSString *userAgentValue = request.allHTTPHeaderFields[kFIRInstanceIDFirebaseUserAgentKey];
  303. XCTAssertEqualObjects(userAgentValue, [FIRApp firebaseUserAgent]);
  304. NSString *heartBeatCode = request.allHTTPHeaderFields[kFIRInstanceIDFirebaseHeartbeatKey];
  305. XCTAssertEqualObjects(heartBeatCode, @"3");
  306. // Return a response with Error=RST
  307. NSData *responseBody = [self dataForFetchRequest:request returnValidToken:NO];
  308. NSHTTPURLResponse *responseObject = [[NSHTTPURLResponse alloc] initWithURL:request.URL
  309. statusCode:200
  310. HTTPVersion:@"HTTP/1.1"
  311. headerFields:nil];
  312. response(responseBody, responseObject, nil);
  313. };
  314. [operation addCompletionHandler:^(FIRInstanceIDTokenOperationResult result,
  315. NSString *_Nullable token, NSError *_Nullable error) {
  316. [completionExpectation fulfill];
  317. }];
  318. [operation start];
  319. [self waitForExpectationsWithTimeout:0.25
  320. handler:^(NSError *_Nullable error) {
  321. XCTAssertNil(error.localizedDescription);
  322. }];
  323. }
  324. #pragma mark - Internal Helpers
  325. - (NSData *)dataForFetchRequest:(NSURLRequest *)request returnValidToken:(BOOL)returnValidToken {
  326. NSString *response;
  327. if (returnValidToken) {
  328. response = [NSString stringWithFormat:@"token=%@", kRegistrationToken];
  329. } else {
  330. response = @"Error=RST";
  331. }
  332. return [response dataUsingEncoding:NSUTF8StringEncoding];
  333. }
  334. - (FIRInstanceIDCheckinPreferences *)setCheckinPreferencesWithLastCheckinTime:(int64_t)time {
  335. FIRInstanceIDCheckinPreferences *checkinPreferences =
  336. [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceID secretToken:kSecretToken];
  337. NSDictionary *checkinPlistContents = @{
  338. kFIRInstanceIDDigestStringKey : kDigestString,
  339. kFIRInstanceIDVersionInfoStringKey : kVersionInfoString,
  340. kFIRInstanceIDLastCheckinTimeKey : @(time)
  341. };
  342. [checkinPreferences updateWithCheckinPlistContents:checkinPlistContents];
  343. // manually initialize the checkin preferences
  344. self.checkinPreferences = checkinPreferences;
  345. return checkinPreferences;
  346. }
  347. - (void)stubInstallations {
  348. _mockInstallations = OCMClassMock([FIRInstallations class]);
  349. OCMStub([_mockInstallations installations]).andReturn(_mockInstallations);
  350. FIRInstallationsAuthTokenResult *authToken =
  351. [[FIRInstallationsAuthTokenResult alloc] initWithToken:@"fis-auth-token"
  352. expirationDate:[NSDate distantFuture]];
  353. id authTokenWithCompletionArg = [OCMArg invokeBlockWithArgs:authToken, [NSNull null], nil];
  354. OCMStub([_mockInstallations authTokenWithCompletion:authTokenWithCompletionArg]);
  355. }
  356. @end