FIRInstanceIDTokenOperationsTest.m 18 KB

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