FIRInstallationsAPIServiceTests.m 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  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 FirebaseCoreInternal;
  19. #import "FBLPromise+Testing.h"
  20. #import "FirebaseInstallations/Source/Tests/Utils/FIRInstallationsItem+Tests.h"
  21. #import "FirebaseInstallations/Source/Library/Errors/FIRInstallationsErrorUtil.h"
  22. #import "FirebaseInstallations/Source/Library/Errors/FIRInstallationsHTTPError.h"
  23. #import "FirebaseInstallations/Source/Library/InstallationsAPI/FIRInstallationsAPIService.h"
  24. #import "FirebaseInstallations/Source/Library/InstallationsStore/FIRInstallationsStoredAuthToken.h"
  25. #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
  26. typedef FBLPromise * (^FIRInstallationsAPIServiceTask)(void);
  27. #pragma mark - Fakes
  28. /// A fake heartbeat logger used for dependency injection during testing.
  29. @interface FIRHeartbeatLoggerFake : NSObject <FIRHeartbeatLoggerProtocol>
  30. @property(nonatomic, copy, nullable) FIRHeartbeatsPayload * (^onFlushHeartbeatsIntoPayloadHandler)
  31. (void);
  32. @property(nonatomic, copy, nullable) FIRDailyHeartbeatCode (^onHeartbeatCodeForTodayHandler)(void);
  33. @end
  34. @implementation FIRHeartbeatLoggerFake
  35. - (nonnull FIRHeartbeatsPayload *)flushHeartbeatsIntoPayload {
  36. if (self.onFlushHeartbeatsIntoPayloadHandler) {
  37. return self.onFlushHeartbeatsIntoPayloadHandler();
  38. } else {
  39. return nil;
  40. }
  41. }
  42. - (FIRDailyHeartbeatCode)heartbeatCodeForToday {
  43. // This API should not be used by the below tests because the Installations
  44. // SDK uses only the V2 heartbeat API (`flushHeartbeatsIntoPayload`) for
  45. // getting heartbeats.
  46. [self doesNotRecognizeSelector:_cmd];
  47. return FIRDailyHeartbeatCodeNone;
  48. }
  49. - (void)log {
  50. // This API should not be used by the below tests because the Installations
  51. // SDK does not log heartbeats in it's networking context.
  52. [self doesNotRecognizeSelector:_cmd];
  53. }
  54. @end
  55. #pragma mark - FIRInstallationsAPIService + Internal
  56. @interface FIRInstallationsAPIService (Internal)
  57. - (instancetype)initWithURLSession:(NSURLSession *)URLSession
  58. APIKey:(NSString *)APIKey
  59. projectID:(NSString *)projectID
  60. heartbeatLogger:(id<FIRHeartbeatLoggerProtocol>)heartbeatLogger;
  61. @end
  62. #pragma mark - FIRInstallationsAPIServiceTests
  63. @interface FIRInstallationsAPIServiceTests : XCTestCase
  64. @property(nonatomic) FIRInstallationsAPIService *service;
  65. @property(nonatomic) FIRHeartbeatLoggerFake *heartbeatLoggerFake;
  66. @property(nonatomic) NSString *APIKey;
  67. @property(nonatomic) NSString *projectID;
  68. @property(nonatomic) id mockURLSession;
  69. @end
  70. @implementation FIRInstallationsAPIServiceTests
  71. - (void)setUp {
  72. self.APIKey = @"api-key";
  73. self.projectID = @"project-id";
  74. self.mockURLSession = OCMClassMock([NSURLSession class]);
  75. self.heartbeatLoggerFake = [[FIRHeartbeatLoggerFake alloc] init];
  76. self.service = [[FIRInstallationsAPIService alloc] initWithURLSession:self.mockURLSession
  77. APIKey:self.APIKey
  78. projectID:self.projectID
  79. heartbeatLogger:self.heartbeatLoggerFake];
  80. }
  81. - (void)tearDown {
  82. self.service = nil;
  83. self.heartbeatLoggerFake = nil;
  84. self.mockURLSession = nil;
  85. self.projectID = nil;
  86. self.APIKey = nil;
  87. // Wait for any pending promises to complete.
  88. XCTAssert(FBLWaitForPromisesWithTimeout(2));
  89. }
  90. - (void)testRegisterInstallationSuccessWhenHeartbeatsNeedSending {
  91. // Given
  92. NSString *fixtureName = @"APIRegisterInstallationResponseSuccess.json";
  93. FIRHeartbeatsPayload *nonEmptyHeartbeatsPayload =
  94. [FIRHeartbeatLoggingTestUtils nonEmptyHeartbeatsPayload];
  95. // When
  96. self.heartbeatLoggerFake.onFlushHeartbeatsIntoPayloadHandler = ^FIRHeartbeatsPayload * {
  97. return nonEmptyHeartbeatsPayload;
  98. };
  99. // Then
  100. [self assertRegisterInstallationSuccessWithResponseFixtureName:fixtureName
  101. responseCode:201
  102. expectedFIDOverride:@"aaaaaaaaaaaaaaaaaaaaaa"
  103. heartbeatsPayload:nonEmptyHeartbeatsPayload];
  104. }
  105. - (void)testRegisterInstallationSuccessWhenNoHeartbeatsNeedSending {
  106. // Given
  107. NSString *fixtureName = @"APIRegisterInstallationResponseSuccess.json";
  108. FIRHeartbeatsPayload *emptyHeartbeatsPayload =
  109. [FIRHeartbeatLoggingTestUtils emptyHeartbeatsPayload];
  110. // When
  111. self.heartbeatLoggerFake.onFlushHeartbeatsIntoPayloadHandler = ^FIRHeartbeatsPayload * {
  112. return emptyHeartbeatsPayload;
  113. };
  114. // Then
  115. [self assertRegisterInstallationSuccessWithResponseFixtureName:fixtureName
  116. responseCode:201
  117. expectedFIDOverride:@"aaaaaaaaaaaaaaaaaaaaaa"
  118. heartbeatsPayload:emptyHeartbeatsPayload];
  119. }
  120. - (void)testRegisterInstallationSuccess_NoFIDInResponse {
  121. NSString *fixtureName = @"APIRegisterInstallationResponseSuccessNoFID.json";
  122. [self assertRegisterInstallationSuccessWithResponseFixtureName:fixtureName
  123. responseCode:201
  124. expectedFIDOverride:nil
  125. heartbeatsPayload:nil];
  126. }
  127. - (void)testRegisterInstallationSuccess_InvalidInstallation {
  128. FIRInstallationsItem *installation = [FIRInstallationsItem createUnregisteredInstallationItem];
  129. installation.firebaseInstallationID = nil;
  130. __auto_type promise = [self.service registerInstallation:installation];
  131. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  132. XCTAssertTrue(promise.isRejected);
  133. XCTAssertNil(promise.value);
  134. XCTAssertNotNil(promise.error);
  135. }
  136. - (void)testRefreshAuthTokenSuccessWhenHeartbeatsNeedSending {
  137. // Given
  138. FIRHeartbeatsPayload *nonEmptyHeartbeatsPayload =
  139. [FIRHeartbeatLoggingTestUtils nonEmptyHeartbeatsPayload];
  140. // When
  141. self.heartbeatLoggerFake.onFlushHeartbeatsIntoPayloadHandler = ^FIRHeartbeatsPayload * {
  142. return nonEmptyHeartbeatsPayload;
  143. };
  144. // Then
  145. [self assertRefreshAuthTokenSuccessWhenSendingHeartbeatsPayload:nonEmptyHeartbeatsPayload];
  146. }
  147. - (void)testRefreshAuthTokenSuccessWhenNoHeartbeatsNeedSending {
  148. // Given
  149. FIRHeartbeatsPayload *emptyHeartbeatsPayload =
  150. [FIRHeartbeatLoggingTestUtils emptyHeartbeatsPayload];
  151. // When
  152. self.heartbeatLoggerFake.onFlushHeartbeatsIntoPayloadHandler = ^FIRHeartbeatsPayload * {
  153. return emptyHeartbeatsPayload;
  154. };
  155. // Then
  156. [self assertRefreshAuthTokenSuccessWhenSendingHeartbeatsPayload:emptyHeartbeatsPayload];
  157. }
  158. - (void)testRefreshAuthTokenAPIError {
  159. FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
  160. installation.firebaseInstallationID = @"qwertyuiopasdfghjklzxcvbnm";
  161. // 1. Stub URL session:
  162. // 1.1. URL request validation.
  163. id URLRequestValidation = [self refreshTokenRequestValidationArgWithInstallation:installation
  164. heartbeatsPayload:nil];
  165. // 1.2. Capture completion to call it later.
  166. __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  167. id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
  168. taskCompletion = obj;
  169. return YES;
  170. }];
  171. // 1.3. Create a data task mock.
  172. id mockDataTask = OCMClassMock([NSURLSessionDataTask class]);
  173. OCMExpect([(NSURLSessionDataTask *)mockDataTask resume]);
  174. // 1.4. Expect `dataTaskWithRequest` to be called.
  175. OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
  176. completionHandler:completionArg])
  177. .andReturn(mockDataTask);
  178. // 1.5. Prepare server response data.
  179. NSData *errorResponseData =
  180. [self loadFixtureNamed:@"APIGenerateTokenResponseInvalidRefreshToken.json"];
  181. // 2. Call
  182. FBLPromise<FIRInstallationsItem *> *promise =
  183. [self.service refreshAuthTokenForInstallation:installation];
  184. // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  185. OCMVerifyAllWithDelay(self.mockURLSession, 0.5);
  186. // 4. Wait for the data task `resume` to be called.
  187. OCMVerifyAllWithDelay(mockDataTask, 0.5);
  188. // 5. Call the data task completion.
  189. taskCompletion(errorResponseData, [self responseWithStatusCode:401], nil);
  190. // 6. Check result.
  191. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  192. XCTAssertTrue([FIRInstallationsErrorUtil isAPIError:promise.error withHTTPCode:401]);
  193. XCTAssertNil(promise.value);
  194. }
  195. - (void)testRefreshAuthToken_WhenAPIError500_ThenRetriesOnce {
  196. FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
  197. installation.firebaseInstallationID = @"qwertyuiopasdfghjklzxcvbnm";
  198. // 1. Stub URL session:
  199. // 1.1. URL request validation.
  200. id URLRequestValidation = [self refreshTokenRequestValidationArgWithInstallation:installation
  201. heartbeatsPayload:nil];
  202. // 1.2. Capture completion to call it later.
  203. __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  204. id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
  205. taskCompletion = obj;
  206. return YES;
  207. }];
  208. // 1.3. Create a data task mock.
  209. id mockDataTask1 = OCMClassMock([NSURLSessionDataTask class]);
  210. OCMExpect([(NSURLSessionDataTask *)mockDataTask1 resume]);
  211. // 1.4. Expect `dataTaskWithRequest` to be called.
  212. OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
  213. completionHandler:completionArg])
  214. .andReturn(mockDataTask1);
  215. // 1.5. Prepare server response data.
  216. NSData *errorResponseData =
  217. [self loadFixtureNamed:@"APIGenerateTokenResponseInvalidRefreshToken.json"];
  218. // 2. Call
  219. FBLPromise<FIRInstallationsItem *> *promise =
  220. [self.service refreshAuthTokenForInstallation:installation];
  221. // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  222. OCMVerifyAllWithDelay(self.mockURLSession, 0.5);
  223. // 4. Wait for the data task `resume` to be called.
  224. OCMVerifyAllWithDelay(mockDataTask1, 0.5);
  225. // 5. Call the data task completion.
  226. taskCompletion(errorResponseData,
  227. [self responseWithStatusCode:FIRInstallationsHTTPCodesServerInternalError], nil);
  228. // 6. Retry:
  229. // 6.1. Expect another API request to be sent.
  230. id mockDataTask2 = OCMClassMock([NSURLSessionDataTask class]);
  231. OCMExpect([(NSURLSessionDataTask *)mockDataTask2 resume]);
  232. OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
  233. completionHandler:completionArg])
  234. .andReturn(mockDataTask2);
  235. OCMVerifyAllWithDelay(self.mockURLSession, 1.5);
  236. OCMVerifyAllWithDelay(mockDataTask2, 1.5);
  237. // 6.2. Send the API response again.
  238. taskCompletion(errorResponseData,
  239. [self responseWithStatusCode:FIRInstallationsHTTPCodesServerInternalError], nil);
  240. // 6. Check result.
  241. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  242. XCTAssertTrue([FIRInstallationsErrorUtil
  243. isAPIError:promise.error
  244. withHTTPCode:FIRInstallationsHTTPCodesServerInternalError]);
  245. XCTAssertNil(promise.value);
  246. }
  247. - (void)testRefreshAuthTokenDataNil {
  248. FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
  249. installation.firebaseInstallationID = @"qwertyuiopasdfghjklzxcvbnm";
  250. // 1. Stub URL session:
  251. // 1.1. URL request validation.
  252. id URLRequestValidation = [self refreshTokenRequestValidationArgWithInstallation:installation
  253. heartbeatsPayload:nil];
  254. // 1.2. Capture completion to call it later.
  255. __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  256. id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
  257. taskCompletion = obj;
  258. return YES;
  259. }];
  260. // 1.3. Create a data task mock.
  261. id mockDataTask = OCMClassMock([NSURLSessionDataTask class]);
  262. OCMExpect([(NSURLSessionDataTask *)mockDataTask resume]);
  263. // 1.4. Expect `dataTaskWithRequest` to be called.
  264. OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
  265. completionHandler:completionArg])
  266. .andReturn(mockDataTask);
  267. // 2. Call
  268. FBLPromise<FIRInstallationsItem *> *promise =
  269. [self.service refreshAuthTokenForInstallation:installation];
  270. // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  271. OCMVerifyAllWithDelay(self.mockURLSession, 0.5);
  272. // 4. Wait for the data task `resume` to be called.
  273. OCMVerifyAllWithDelay(mockDataTask, 0.5);
  274. // 5. Call the data task completion.
  275. // HTTP 200 but no data (a potential server failure).
  276. taskCompletion(nil, [self responseWithStatusCode:200], nil);
  277. // 6. Check result.
  278. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  279. XCTAssertEqualObjects(promise.error.userInfo[NSLocalizedFailureReasonErrorKey],
  280. @"Failed to serialize JSON data.");
  281. XCTAssertNil(promise.value);
  282. }
  283. - (void)testDeleteInstallationSuccessWhenHeartbeatsNeedSending {
  284. // Given
  285. FIRHeartbeatsPayload *nonEmptyHeartbeatsPayload =
  286. [FIRHeartbeatLoggingTestUtils nonEmptyHeartbeatsPayload];
  287. // When
  288. self.heartbeatLoggerFake.onFlushHeartbeatsIntoPayloadHandler = ^FIRHeartbeatsPayload * {
  289. return nonEmptyHeartbeatsPayload;
  290. };
  291. // Then
  292. [self assertDeleteInstallationSuccessWhenSendingHeartbeatsPayload:nonEmptyHeartbeatsPayload];
  293. }
  294. - (void)testDeleteInstallationSuccessWhenNoHeartbeatsNeedSending {
  295. // Given
  296. FIRHeartbeatsPayload *emptyHeartbeatsPayload =
  297. [FIRHeartbeatLoggingTestUtils emptyHeartbeatsPayload];
  298. // When
  299. self.heartbeatLoggerFake.onFlushHeartbeatsIntoPayloadHandler = ^FIRHeartbeatsPayload * {
  300. return emptyHeartbeatsPayload;
  301. };
  302. // Then
  303. [self assertDeleteInstallationSuccessWhenSendingHeartbeatsPayload:emptyHeartbeatsPayload];
  304. }
  305. - (void)testDeleteInstallationErrorNotFound {
  306. FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
  307. // 1. Stub URL session:
  308. // 1.1. URL request validation.
  309. id URLRequestValidation = [self deleteInstallationRequestValidationWithInstallation:installation
  310. heartbeatsPayload:nil];
  311. // 1.2. Capture completion to call it later.
  312. __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  313. id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
  314. taskCompletion = obj;
  315. return YES;
  316. }];
  317. // 1.3. Create a data task mock.
  318. id mockDataTask = OCMClassMock([NSURLSessionDataTask class]);
  319. OCMExpect([(NSURLSessionDataTask *)mockDataTask resume]);
  320. // 1.4. Expect `dataTaskWithRequest` to be called.
  321. OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
  322. completionHandler:completionArg])
  323. .andReturn(mockDataTask);
  324. // 2. Call
  325. FBLPromise<FIRInstallationsItem *> *promise = [self.service deleteInstallation:installation];
  326. // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  327. OCMVerifyAllWithDelay(self.mockURLSession, 0.5);
  328. // 4. Wait for the data task `resume` to be called.
  329. OCMVerifyAllWithDelay(mockDataTask, 0.5);
  330. // 5. Call the data task completion.
  331. // HTTP 200 but no data (a potential server failure).
  332. taskCompletion(nil, [self responseWithStatusCode:404], nil);
  333. // 6. Check result.
  334. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  335. XCTAssertTrue([FIRInstallationsErrorUtil isAPIError:promise.error withHTTPCode:404]);
  336. XCTAssertNil(promise.value);
  337. }
  338. - (void)testDeleteInstallation_WhenAPIError500_ThenRetriesOnce {
  339. FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
  340. // 1. Stub URL session:
  341. // 1.1. URL request validation.
  342. id URLRequestValidation = [self deleteInstallationRequestValidationWithInstallation:installation
  343. heartbeatsPayload:nil];
  344. // 1.2. Capture completion to call it later.
  345. __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  346. id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
  347. taskCompletion = obj;
  348. return YES;
  349. }];
  350. // 1.3. Create a data task mock.
  351. id mockDataTask1 = OCMClassMock([NSURLSessionDataTask class]);
  352. OCMExpect([(NSURLSessionDataTask *)mockDataTask1 resume]);
  353. // 1.4. Expect `dataTaskWithRequest` to be called.
  354. OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
  355. completionHandler:completionArg])
  356. .andReturn(mockDataTask1);
  357. // 2. Call
  358. FBLPromise<FIRInstallationsItem *> *promise = [self.service deleteInstallation:installation];
  359. // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  360. OCMVerifyAllWithDelay(self.mockURLSession, 0.5);
  361. // 4. Wait for the data task `resume` to be called.
  362. OCMVerifyAllWithDelay(mockDataTask1, 0.5);
  363. // 5. Call the data task completion.
  364. // HTTP 200 but no data (a potential server failure).
  365. taskCompletion(nil, [self responseWithStatusCode:FIRInstallationsHTTPCodesServerInternalError],
  366. nil);
  367. // 6. Retry:
  368. // 6.1. Wait for the API request to be sent again.
  369. id mockDataTask2 = OCMClassMock([NSURLSessionDataTask class]);
  370. OCMExpect([(NSURLSessionDataTask *)mockDataTask2 resume]);
  371. OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
  372. completionHandler:completionArg])
  373. .andReturn(mockDataTask2);
  374. OCMVerifyAllWithDelay(self.mockURLSession, 1.5);
  375. OCMVerifyAllWithDelay(mockDataTask1, 1.5);
  376. // 6.1. Send another response.
  377. taskCompletion(nil, [self responseWithStatusCode:FIRInstallationsHTTPCodesServerInternalError],
  378. nil);
  379. // 7. Check result.
  380. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  381. XCTAssertTrue([FIRInstallationsErrorUtil
  382. isAPIError:promise.error
  383. withHTTPCode:FIRInstallationsHTTPCodesServerInternalError]);
  384. XCTAssertNil(promise.value);
  385. }
  386. #pragma mark - Helpers
  387. - (void)assertRefreshAuthTokenSuccessWhenSendingHeartbeatsPayload:
  388. (FIRHeartbeatsPayload *)heartbeatsPayload {
  389. FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
  390. installation.firebaseInstallationID = @"qwertyuiopasdfghjklzxcvbnm";
  391. // 1. Stub URL session:
  392. // 1.1. URL request validation.
  393. id URLRequestValidation =
  394. [self refreshTokenRequestValidationArgWithInstallation:installation
  395. heartbeatsPayload:heartbeatsPayload];
  396. // 1.2. Capture completion to call it later.
  397. __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  398. id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
  399. taskCompletion = obj;
  400. return YES;
  401. }];
  402. // 1.3. Create a data task mock.
  403. id mockDataTask = OCMClassMock([NSURLSessionDataTask class]);
  404. OCMExpect([(NSURLSessionDataTask *)mockDataTask resume]);
  405. // 1.4. Expect `dataTaskWithRequest` to be called.
  406. OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
  407. completionHandler:completionArg])
  408. .andReturn(mockDataTask);
  409. // 1.5. Prepare server response data.
  410. NSData *successResponseData = [self loadFixtureNamed:@"APIGenerateTokenResponseSuccess.json"];
  411. // 2. Call
  412. FBLPromise<FIRInstallationsItem *> *promise =
  413. [self.service refreshAuthTokenForInstallation:installation];
  414. // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  415. OCMVerifyAllWithDelay(self.mockURLSession, 0.5);
  416. // 4. Wait for the data task `resume` to be called.
  417. OCMVerifyAllWithDelay(mockDataTask, 0.5);
  418. // 5. Call the data task completion.
  419. taskCompletion(successResponseData, [self responseWithStatusCode:200], nil);
  420. // 6. Check result.
  421. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  422. XCTAssertNil(promise.error);
  423. XCTAssertNotNil(promise.value);
  424. XCTAssertNotEqual(promise.value, installation);
  425. XCTAssertEqualObjects(promise.value.appID, installation.appID);
  426. XCTAssertEqualObjects(promise.value.firebaseAppName, installation.firebaseAppName);
  427. XCTAssertEqualObjects(promise.value.firebaseInstallationID, installation.firebaseInstallationID);
  428. XCTAssertEqualObjects(promise.value.refreshToken, installation.refreshToken);
  429. XCTAssertEqualObjects(promise.value.authToken.token,
  430. @"aaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbb.cccccccccccccccccccccccc");
  431. [self assertDate:promise.value.authToken.expirationDate
  432. isApproximatelyEqualCurrentPlusTimeInterval:3987465];
  433. }
  434. - (void)assertDeleteInstallationSuccessWhenSendingHeartbeatsPayload:
  435. (FIRHeartbeatsPayload *)heartbeatsPayload {
  436. FIRInstallationsItem *installation = [FIRInstallationsItem createRegisteredInstallationItem];
  437. // 1. Stub URL session:
  438. // 1.1. URL request validation.
  439. id URLRequestValidation =
  440. [self deleteInstallationRequestValidationWithInstallation:installation
  441. heartbeatsPayload:heartbeatsPayload];
  442. // 1.2. Capture completion to call it later.
  443. __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  444. id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
  445. taskCompletion = obj;
  446. return YES;
  447. }];
  448. // 1.3. Create a data task mock.
  449. id mockDataTask = OCMClassMock([NSURLSessionDataTask class]);
  450. OCMExpect([(NSURLSessionDataTask *)mockDataTask resume]);
  451. // 1.4. Expect `dataTaskWithRequest` to be called.
  452. OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
  453. completionHandler:completionArg])
  454. .andReturn(mockDataTask);
  455. // 2. Call
  456. FBLPromise<FIRInstallationsItem *> *promise = [self.service deleteInstallation:installation];
  457. // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  458. OCMVerifyAllWithDelay(self.mockURLSession, 0.5);
  459. // 4. Wait for the data task `resume` to be called.
  460. OCMVerifyAllWithDelay(mockDataTask, 0.5);
  461. // 5. Call the data task completion.
  462. // HTTP 200 but no data (a potential server failure).
  463. NSData *successResponseData = [@"{}" dataUsingEncoding:NSUTF8StringEncoding];
  464. taskCompletion(successResponseData, [self responseWithStatusCode:200], nil);
  465. // 6. Check result.
  466. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  467. XCTAssertNil(promise.error);
  468. XCTAssertEqual(promise.value, installation);
  469. }
  470. - (NSData *)loadFixtureNamed:(NSString *)fileName {
  471. NSURL *fileURL = [[NSBundle bundleForClass:[self class]] URLForResource:fileName
  472. withExtension:nil];
  473. XCTAssertNotNil(fileURL);
  474. NSError *error;
  475. NSData *data = [NSData dataWithContentsOfURL:fileURL options:0 error:&error];
  476. XCTAssertNotNil(data, @"File name: %@ Error: %@", fileName, error);
  477. return data;
  478. }
  479. - (NSURLResponse *)responseWithStatusCode:(NSUInteger)statusCode {
  480. NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[NSURL fileURLWithPath:@"/"]
  481. statusCode:statusCode
  482. HTTPVersion:nil
  483. headerFields:nil];
  484. return response;
  485. }
  486. - (void)assertDate:(NSDate *)date
  487. isApproximatelyEqualCurrentPlusTimeInterval:(NSTimeInterval)timeInterval {
  488. NSDate *expectedDate = [NSDate dateWithTimeIntervalSinceNow:timeInterval];
  489. NSTimeInterval precision = 10;
  490. XCTAssert(ABS([date timeIntervalSinceDate:expectedDate]) <= precision,
  491. @"date: %@ is not equal to expected %@ with precision %f - %@", date, expectedDate,
  492. precision, self.name);
  493. }
  494. - (id)refreshTokenRequestValidationArgWithInstallation:(FIRInstallationsItem *)installation
  495. heartbeatsPayload:(FIRHeartbeatsPayload *)heartbeatsPayload {
  496. return [OCMArg checkWithBlock:^BOOL(NSURLRequest *request) {
  497. XCTAssertEqualObjects(request.HTTPMethod, @"POST");
  498. XCTAssertEqualObjects(request.URL.absoluteString,
  499. @"https://firebaseinstallations.googleapis.com/v1/projects/project-id/"
  500. @"installations/qwertyuiopasdfghjklzxcvbnm/authTokens:generate");
  501. NSMutableDictionary<NSString *, NSString *> *expectedHTTPHeaderFields = @{
  502. @"Content-Type" : @"application/json",
  503. @"X-Goog-Api-Key" : self.APIKey,
  504. @"X-Ios-Bundle-Identifier" : [[NSBundle mainBundle] bundleIdentifier],
  505. @"Authorization" : [NSString stringWithFormat:@"FIS_v2 %@", installation.refreshToken]
  506. }
  507. .mutableCopy;
  508. NSString *_Nullable heartbeatHeaderValue =
  509. FIRHeaderValueFromHeartbeatsPayload(heartbeatsPayload);
  510. if (heartbeatHeaderValue) {
  511. expectedHTTPHeaderFields[@"X-firebase-client"] = heartbeatHeaderValue;
  512. }
  513. XCTAssertEqualObjects([request allHTTPHeaderFields], expectedHTTPHeaderFields);
  514. NSError *error;
  515. NSDictionary *body = [NSJSONSerialization JSONObjectWithData:request.HTTPBody
  516. options:0
  517. error:&error];
  518. XCTAssertNotNil(body, @"Error: %@, test: %@", error, self.name);
  519. XCTAssertEqualObjects(
  520. body,
  521. @{@"installation" : @{@"sdkVersion" : [self SDKVersion]}}, @"%@", self.name);
  522. return YES;
  523. }];
  524. }
  525. - (id)deleteInstallationRequestValidationWithInstallation:(FIRInstallationsItem *)installation
  526. heartbeatsPayload:
  527. (FIRHeartbeatsPayload *)heartbeatsPayload {
  528. return [OCMArg checkWithBlock:^BOOL(NSURLRequest *request) {
  529. XCTAssert([request isKindOfClass:[NSURLRequest class]], @"Unexpected class: %@",
  530. [request class]);
  531. XCTAssertEqualObjects(request.HTTPMethod, @"DELETE");
  532. NSString *expectedURL = [NSString
  533. stringWithFormat:
  534. @"https://firebaseinstallations.googleapis.com/v1/projects/%@/installations/%@/",
  535. self.projectID, installation.firebaseInstallationID];
  536. XCTAssertEqualObjects(request.URL.absoluteString, expectedURL);
  537. NSMutableDictionary<NSString *, NSString *> *expectedHTTPHeaderFields = @{
  538. @"Content-Type" : @"application/json",
  539. @"X-Goog-Api-Key" : self.APIKey,
  540. @"X-Ios-Bundle-Identifier" : [[NSBundle mainBundle] bundleIdentifier],
  541. @"Authorization" : [NSString stringWithFormat:@"FIS_v2 %@", installation.refreshToken],
  542. }
  543. .mutableCopy;
  544. NSString *_Nullable heartbeatHeaderValue =
  545. FIRHeaderValueFromHeartbeatsPayload(heartbeatsPayload);
  546. if (heartbeatHeaderValue) {
  547. expectedHTTPHeaderFields[@"X-firebase-client"] = heartbeatHeaderValue;
  548. }
  549. XCTAssertEqualObjects([request allHTTPHeaderFields], expectedHTTPHeaderFields);
  550. NSString *expectedAuthHeader =
  551. [NSString stringWithFormat:@"FIS_v2 %@", installation.refreshToken];
  552. XCTAssertEqualObjects(request.allHTTPHeaderFields[@"Authorization"], expectedAuthHeader, @"%@",
  553. self.name);
  554. NSError *error;
  555. NSDictionary *JSONBody = [NSJSONSerialization JSONObjectWithData:request.HTTPBody
  556. options:0
  557. error:&error];
  558. XCTAssertNotNil(JSONBody, @"Error: %@", error);
  559. XCTAssertEqualObjects(JSONBody, @{});
  560. return YES;
  561. }];
  562. }
  563. - (void)assertRegisterInstallationSuccessWithResponseFixtureName:(NSString *)fixtureName
  564. responseCode:(NSInteger)responseCode
  565. expectedFIDOverride:(nullable NSString *)overrideFID
  566. heartbeatsPayload:
  567. (FIRHeartbeatsPayload *)heartbeatsPayload {
  568. FIRInstallationsItem *installation = [FIRInstallationsItem createUnregisteredInstallationItem];
  569. installation.IIDDefaultToken = @"iid-auth-token";
  570. NSString *expectedFID = overrideFID ?: installation.firebaseInstallationID;
  571. // 1. Stub URL session:
  572. // 1.1. URL request validation.
  573. id URLRequestValidation = [OCMArg checkWithBlock:^BOOL(NSURLRequest *request) {
  574. XCTAssertEqualObjects(request.HTTPMethod, @"POST");
  575. XCTAssertEqualObjects(
  576. request.URL.absoluteString,
  577. @"https://firebaseinstallations.googleapis.com/v1/projects/project-id/installations/");
  578. NSMutableDictionary<NSString *, NSString *> *expectedHTTPHeaderFields = @{
  579. @"Content-Type" : @"application/json",
  580. @"X-Goog-Api-Key" : self.APIKey,
  581. @"X-Ios-Bundle-Identifier" : [[NSBundle mainBundle] bundleIdentifier],
  582. }
  583. .mutableCopy;
  584. NSString *_Nullable heartbeatHeaderValue =
  585. FIRHeaderValueFromHeartbeatsPayload(heartbeatsPayload);
  586. if (heartbeatHeaderValue) {
  587. expectedHTTPHeaderFields[@"X-firebase-client"] = heartbeatHeaderValue;
  588. }
  589. [expectedHTTPHeaderFields addEntriesFromDictionary:@{
  590. @"x-goog-fis-ios-iid-migration-auth" : installation.IIDDefaultToken
  591. }];
  592. XCTAssertEqualObjects([request allHTTPHeaderFields], expectedHTTPHeaderFields);
  593. NSError *error;
  594. NSDictionary *body = [NSJSONSerialization JSONObjectWithData:request.HTTPBody
  595. options:0
  596. error:&error];
  597. XCTAssertNotNil(body, @"Error: %@", error);
  598. XCTAssertEqualObjects(body[@"fid"], installation.firebaseInstallationID);
  599. XCTAssertEqualObjects(body[@"authVersion"], @"FIS_v2");
  600. XCTAssertEqualObjects(body[@"appId"], installation.appID);
  601. XCTAssertEqualObjects(body[@"sdkVersion"], [self SDKVersion]);
  602. return YES;
  603. }];
  604. // 1.2. Capture completion to call it later.
  605. __block void (^taskCompletion)(NSData *, NSURLResponse *, NSError *);
  606. id completionArg = [OCMArg checkWithBlock:^BOOL(id obj) {
  607. taskCompletion = obj;
  608. return YES;
  609. }];
  610. // 1.3. Create a data task mock.
  611. id mockDataTask = OCMClassMock([NSURLSessionDataTask class]);
  612. OCMExpect([(NSURLSessionDataTask *)mockDataTask resume]);
  613. // 1.4. Expect `dataTaskWithRequest` to be called.
  614. OCMExpect([self.mockURLSession dataTaskWithRequest:URLRequestValidation
  615. completionHandler:completionArg])
  616. .andReturn(mockDataTask);
  617. // 2. Call
  618. FBLPromise<FIRInstallationsItem *> *promise = [self.service registerInstallation:installation];
  619. // 3. Wait for `[NSURLSession dataTaskWithRequest...]` to be called
  620. OCMVerifyAllWithDelay(self.mockURLSession, 0.5);
  621. // 4. Wait for the data task `resume` to be called.
  622. OCMVerifyAllWithDelay(mockDataTask, 0.5);
  623. // 5. Call the data task completion.
  624. NSData *responseData = [self loadFixtureNamed:fixtureName];
  625. taskCompletion(responseData, [self responseWithStatusCode:responseCode], nil);
  626. // 6. Check result.
  627. XCTAssert(FBLWaitForPromisesWithTimeout(0.5));
  628. XCTAssertNil(promise.error);
  629. XCTAssertNotNil(promise.value);
  630. XCTAssertNotEqual(promise.value, installation);
  631. XCTAssertEqualObjects(promise.value.appID, installation.appID);
  632. XCTAssertEqualObjects(promise.value.firebaseAppName, installation.firebaseAppName);
  633. // Server may respond with a different FID if the sent FID cannot be accepted.
  634. XCTAssertEqualObjects(promise.value.firebaseInstallationID, expectedFID);
  635. XCTAssertEqualObjects(promise.value.refreshToken, @"aaaaaaabbbbbbbbcccccccccdddddddd00000000");
  636. XCTAssertEqualObjects(promise.value.authToken.token,
  637. @"aaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbb.cccccccccccccccccccccccc");
  638. [self assertDate:promise.value.authToken.expirationDate
  639. isApproximatelyEqualCurrentPlusTimeInterval:604800];
  640. }
  641. - (NSString *)SDKVersion {
  642. return [NSString stringWithFormat:@"i:%@", FIRFirebaseVersion()];
  643. }
  644. @end