FIRInstanceIDTokenOperationsTest.m 19 KB

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