FIRInstanceIDTokenOperationsTest.m 19 KB

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