FIRInstanceIDTokenOperationsTest.m 16 KB

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