FIRMessagingAuthServiceTest.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. /*
  2. * Copyright 2021 Google LLC
  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 <OCMock/OCMock.h>
  17. #import <XCTest/XCTest.h>
  18. #import "FirebaseMessaging/Sources/NSError+FIRMessaging.h"
  19. #import "FirebaseMessaging/Sources/Token/FIRMessagingAuthService.h"
  20. #import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinPreferences.h"
  21. #import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinService.h"
  22. #import "FirebaseMessaging/Sources/Token/FIRMessagingCheckinStore.h"
  23. static NSString *const kDeviceAuthId = @"device-id";
  24. static NSString *const kSecretToken = @"secret-token";
  25. static NSString *const kVersionInfo = @"1.0";
  26. @interface FIRMessagingCheckinService (ExposedForTest)
  27. @property(nonatomic, readwrite, strong) FIRMessagingCheckinPreferences *checkinPreferences;
  28. @end
  29. @interface FIRMessagingAuthService (ExposedForTest)
  30. @property(atomic, readwrite, assign) int64_t lastCheckinTimestampSeconds;
  31. @property(atomic, readwrite, assign) int64_t nextScheduledCheckinIntervalSeconds;
  32. @property(atomic, readwrite, assign) int checkinRetryCount;
  33. @property(nonatomic, readonly, strong)
  34. NSMutableArray<FIRMessagingDeviceCheckinCompletion> *checkinHandlers;
  35. @property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
  36. @property(nonatomic, readwrite, strong) FIRMessagingCheckinStore *checkinStore;
  37. @property(nonatomic, readwrite, strong) FIRMessagingCheckinPreferences *checkinPreferences;
  38. @end
  39. @interface FIRMessagingAuthServiceTest : XCTestCase
  40. @property(nonatomic, readwrite, strong) FIRMessagingAuthService *authService;
  41. @property(nonatomic, readwrite, strong) FIRMessagingCheckinService *checkinService;
  42. @property(nonatomic, readwrite, strong) id mockCheckinService;
  43. @property(nonatomic, readwrite, strong) id mockStore;
  44. @property(nonatomic, readwrite, copy) FIRMessagingDeviceCheckinCompletion checkinCompletion;
  45. @end
  46. @implementation FIRMessagingAuthServiceTest
  47. - (void)setUp {
  48. [super setUp];
  49. _authService = [[FIRMessagingAuthService alloc] init];
  50. _mockStore = OCMPartialMock(_authService.checkinStore);
  51. _mockCheckinService = OCMPartialMock(_authService.checkinService);
  52. // Ensure cached checkin is reset when testing initial checkin call.
  53. FIRMessagingCheckinPreferences *preferences =
  54. [[FIRMessagingCheckinPreferences alloc] initWithDeviceID:@"" secretToken:@""];
  55. _authService.checkinPreferences = preferences;
  56. // The tests here are to focus on checkin interval not locale change, so always set locale as
  57. // non-changed.
  58. [[NSUserDefaults standardUserDefaults] setObject:FIRMessagingCurrentLocale()
  59. forKey:kFIRMessagingInstanceIDUserDefaultsKeyLocale];
  60. }
  61. - (void)tearDown {
  62. [_mockStore stopMocking];
  63. [_mockCheckinService stopMocking];
  64. _checkinCompletion = nil;
  65. [super tearDown];
  66. }
  67. /**
  68. * Test scheduling a checkin which completes successfully. Once the checkin is complete
  69. * we should have the valid checkin preferences in memory.
  70. */
  71. - (void)testScheduleCheckin_initialSuccess {
  72. XCTestExpectation *checkinExpectation =
  73. [self expectationWithDescription:@"Did call checkin service"];
  74. FIRMessagingCheckinPreferences *checkinPreferences = [self validCheckinPreferences];
  75. OCMStub([self.mockCheckinService
  76. checkinWithExistingCheckin:[OCMArg any]
  77. completion:([OCMArg checkWithBlock:^BOOL(id obj) {
  78. [checkinExpectation fulfill];
  79. self.checkinCompletion = obj;
  80. return obj != nil;
  81. }])])
  82. .andDo(^(NSInvocation *invocation) {
  83. self.checkinCompletion(checkinPreferences, nil);
  84. });
  85. // Always return YES for whether we succeeded in persisting the checkin
  86. OCMStub([self.mockStore
  87. saveCheckinPreferences:checkinPreferences
  88. handler:([OCMArg invokeBlockWithArgs:[NSNull null], nil])]);
  89. [self.authService scheduleCheckin:YES];
  90. XCTAssertTrue([self.authService hasValidCheckinInfo]);
  91. XCTAssertEqual([self.authService checkinRetryCount], 1);
  92. [self waitForExpectationsWithTimeout:2.0 handler:NULL];
  93. }
  94. /**
  95. * Test scheduling a checkin which completes successfully, but fails to save, due to Keychain
  96. * errors.
  97. */
  98. - (void)testScheduleCheckin_successButFailureInSaving {
  99. XCTestExpectation *checkinFailureExpectation =
  100. [self expectationWithDescription:@"Did receive error after checkin"];
  101. FIRMessagingCheckinPreferences *checkinPreferences = [self validCheckinPreferences];
  102. OCMStub([self.mockCheckinService checkinWithExistingCheckin:[OCMArg any]
  103. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  104. [checkinFailureExpectation fulfill];
  105. self.checkinCompletion = obj;
  106. return obj != nil;
  107. }]]);
  108. // Always return NO for whether we succeeded in persisting the checkin, to simulate Keychain error
  109. OCMStub([self.mockStore saveCheckinPreferences:checkinPreferences
  110. handler:([OCMArg invokeBlockWithArgs:[OCMArg any], nil])]);
  111. [self.authService
  112. fetchCheckinInfoWithHandler:^(FIRMessagingCheckinPreferences *checkin, NSError *error) {
  113. [checkinFailureExpectation fulfill];
  114. }];
  115. [self waitForExpectationsWithTimeout:2.0 handler:NULL];
  116. XCTAssertFalse([self.authService hasValidCheckinInfo]);
  117. }
  118. /**
  119. * Test scheduling multiple checkins to complete immediately. Each successive checkin should
  120. * be triggered immediately.
  121. */
  122. - (void)testMultipleScheduleCheckin_immediately {
  123. XCTestExpectation *checkinExpectation =
  124. [self expectationWithDescription:@"Did call checkin service"];
  125. __block int checkinHandlerInvocationCount = 0;
  126. FIRMessagingCheckinPreferences *checkinPreferences = [self validCheckinPreferences];
  127. OCMStub([self.mockCheckinService checkinWithExistingCheckin:[OCMArg any]
  128. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  129. self.checkinCompletion = obj;
  130. return obj != nil;
  131. }]])
  132. .andDo(^(NSInvocation *invocation) {
  133. checkinHandlerInvocationCount++;
  134. // Mock successful Checkin after delay.
  135. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
  136. dispatch_get_main_queue(), ^{
  137. [checkinExpectation fulfill];
  138. self.checkinCompletion(checkinPreferences, nil);
  139. });
  140. });
  141. // Always return YES for whether we succeeded in persisting the checkin
  142. OCMStub([self.mockStore
  143. saveCheckinPreferences:checkinPreferences
  144. handler:([OCMArg invokeBlockWithArgs:[NSNull null], nil])]);
  145. [self.authService scheduleCheckin:YES];
  146. // Schedule an immediate checkin again.
  147. // This should just return because the previous checkin isn't over yet.
  148. [self.authService scheduleCheckin:YES];
  149. [self waitForExpectationsWithTimeout:5.0 handler:NULL];
  150. XCTAssertTrue([self.authService hasValidCheckinInfo]);
  151. XCTAssertEqual([self.authService checkinRetryCount], 2);
  152. // Checkin handler should only be invoked once since the second checkin request should
  153. // return immediately.
  154. XCTAssertEqual(checkinHandlerInvocationCount, 1);
  155. }
  156. /**
  157. * Test multiple checkins scheduled. The second checkin should be scheduled after some
  158. * delay before the first checkin has returned. Since the latter checkin is not immediate
  159. * we should not run it since the first checkin is already scheduled to be executed later.
  160. */
  161. - (void)testMultipleScheduleCheckin_notImmediately {
  162. XCTestExpectation *checkinExpectation =
  163. [self expectationWithDescription:@"Did call checkin service"];
  164. FIRMessagingCheckinPreferences *checkinPreferences = [self validCheckinPreferences];
  165. OCMStub([self.mockCheckinService checkinWithExistingCheckin:[OCMArg any]
  166. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  167. self.checkinCompletion = obj;
  168. return obj != nil;
  169. }]])
  170. .andDo(^(NSInvocation *invocation) {
  171. // Mock successful Checkin after delay.
  172. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
  173. dispatch_get_main_queue(), ^{
  174. [checkinExpectation fulfill];
  175. self.checkinCompletion(checkinPreferences, nil);
  176. });
  177. });
  178. // Always return YES for whether we succeeded in persisting the checkin
  179. OCMStub([self.mockStore
  180. saveCheckinPreferences:checkinPreferences
  181. handler:([OCMArg invokeBlockWithArgs:[NSNull null], nil])]);
  182. [self.authService scheduleCheckin:YES];
  183. // Schedule another checkin after some delay while the first checkin has not yet returned
  184. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)),
  185. dispatch_get_main_queue(), ^{
  186. [self.authService scheduleCheckin:NO];
  187. });
  188. [self waitForExpectationsWithTimeout:5.0 handler:NULL];
  189. XCTAssertTrue([self.authService hasValidCheckinInfo]);
  190. XCTAssertEqual([self.authService checkinRetryCount], 1);
  191. }
  192. /**
  193. * Test initial checkin failure which schedules another checkin which should succeed.
  194. */
  195. - (void)testInitialCheckinFailure_retrySuccess {
  196. XCTestExpectation *checkinExpectation =
  197. [self expectationWithDescription:@"Did call checkin service"];
  198. __block int checkinHandlerInvocationCount = 0;
  199. OCMStub([self.mockCheckinService checkinWithExistingCheckin:[OCMArg any]
  200. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  201. self.checkinCompletion = obj;
  202. return obj != nil;
  203. }]])
  204. .andDo(^(NSInvocation *invocation) {
  205. checkinHandlerInvocationCount++;
  206. if (checkinHandlerInvocationCount == 1) {
  207. // Mock failure on first try
  208. NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeUnknown
  209. failureReason:@"Timeout"];
  210. self.checkinCompletion(nil, error);
  211. } else if (checkinHandlerInvocationCount == 2) {
  212. // Mock success on second try
  213. [checkinExpectation fulfill];
  214. self.checkinCompletion([self validCheckinPreferences], nil);
  215. } else {
  216. // We should not retry for a third time again.
  217. XCTFail(@"Invoking checkin handler invalid number of times.");
  218. }
  219. });
  220. // Always return YES for whether we succeeded in persisting the checkin
  221. OCMStub([self.mockStore
  222. saveCheckinPreferences:[OCMArg any]
  223. handler:([OCMArg invokeBlockWithArgs:[NSNull null], nil])]);
  224. [self.authService scheduleCheckin:YES];
  225. // Schedule another checkin after some delay while the first checkin has not yet returned
  226. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
  227. dispatch_get_main_queue(), ^{
  228. [self.authService scheduleCheckin:YES];
  229. XCTAssertTrue([self.authService hasValidCheckinInfo]);
  230. XCTAssertEqual([self.authService checkinRetryCount], 2);
  231. XCTAssertEqual(checkinHandlerInvocationCount, 2);
  232. });
  233. [self waitForExpectationsWithTimeout:5.0 handler:NULL];
  234. }
  235. /**
  236. * Test initial checkin failure which schedules another checkin which should succeed. If
  237. * a new checkin request comes after that we should not schedule a checkin as we have
  238. * already have valid checkin credentials.
  239. */
  240. - (void)testInitialCheckinFailure_multipleRetrySuccess {
  241. XCTestExpectation *checkinExpectation =
  242. [self expectationWithDescription:@"Did call checkin service"];
  243. __block int checkinHandlerInvocationCount = 0;
  244. OCMStub([self.mockCheckinService checkinWithExistingCheckin:[OCMArg any]
  245. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  246. self.checkinCompletion = obj;
  247. return obj != nil;
  248. }]])
  249. .andDo(^(NSInvocation *invocation) {
  250. checkinHandlerInvocationCount++;
  251. if (checkinHandlerInvocationCount <= 2) {
  252. // Mock failure on first try
  253. NSError *error = [NSError messagingErrorWithCode:kFIRMessagingErrorCodeUnknown
  254. failureReason:@"Timeout"];
  255. self.checkinCompletion(nil, error);
  256. } else if (checkinHandlerInvocationCount == 3) {
  257. // Mock success on second try
  258. [checkinExpectation fulfill];
  259. self.checkinCompletion([self validCheckinPreferences], nil);
  260. } else {
  261. // We should not retry for a third time again.
  262. XCTFail(@"Invoking checkin handler invalid number of times.");
  263. }
  264. });
  265. // Always return YES for whether we succeeded in persisting the checkin
  266. OCMStub([self.mockStore
  267. saveCheckinPreferences:[OCMArg any]
  268. handler:([OCMArg invokeBlockWithArgs:[NSNull null], nil])]);
  269. [self.authService scheduleCheckin:YES];
  270. [self waitForExpectationsWithTimeout:10.0 handler:NULL];
  271. XCTAssertTrue([self.authService hasValidCheckinInfo]);
  272. XCTAssertEqual([self.authService checkinRetryCount], 3);
  273. }
  274. /**
  275. * Performing multiple checkin requests should result in multiple handlers being
  276. * called back, but with only a single actual checkin fetch.
  277. */
  278. - (void)testMultipleCheckinHandlersWithSuccessfulCheckin {
  279. XCTestExpectation *allHandlersCalledExpectation =
  280. [self expectationWithDescription:@"All checkin handlers were called"];
  281. __block NSInteger checkinHandlerCallbackCount = 0;
  282. __block NSInteger checkinServiceInvocationCount = 0;
  283. // Always return a successful checkin, and count the number of times CheckinService is called
  284. OCMStub([self.mockCheckinService checkinWithExistingCheckin:[OCMArg any]
  285. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  286. self.checkinCompletion = obj;
  287. return obj != nil;
  288. }]])
  289. .andDo(^(NSInvocation *invocation) {
  290. checkinServiceInvocationCount++;
  291. self.checkinCompletion([self validCheckinPreferences], nil);
  292. });
  293. // Always return YES for whether we succeeded in persisting the checkin
  294. OCMStub([self.mockStore
  295. saveCheckinPreferences:[OCMArg any]
  296. handler:([OCMArg invokeBlockWithArgs:[NSNull null], nil])]);
  297. NSInteger numHandlers = 10;
  298. for (NSInteger i = 0; i < numHandlers; i++) {
  299. [self.authService
  300. fetchCheckinInfoWithHandler:^(FIRMessagingCheckinPreferences *checkin, NSError *error) {
  301. checkinHandlerCallbackCount++;
  302. if (checkinHandlerCallbackCount == numHandlers) {
  303. [allHandlersCalledExpectation fulfill];
  304. }
  305. }];
  306. }
  307. [self waitForExpectationsWithTimeout:1.0 handler:nil];
  308. XCTAssertEqual(checkinServiceInvocationCount, 1);
  309. XCTAssertEqual(checkinHandlerCallbackCount, numHandlers);
  310. }
  311. /**
  312. * Performing a scheduled checkin *and* simultaneous checkin request should result in
  313. * the number of pending checkin handlers to be 2 (one for the scheduled checkin, one for
  314. * the direct fetch).
  315. */
  316. - (void)testScheduledAndImmediateCheckinsWithMultipleHandler {
  317. XCTestExpectation *fetchHandlerCalledExpectation =
  318. [self expectationWithDescription:@"Direct checkin handler was called"];
  319. __block NSInteger checkinServiceInvocationCount = 0;
  320. // Always return a successful checkin, and count the number of times CheckinService is called
  321. OCMStub([self.mockCheckinService checkinWithExistingCheckin:[OCMArg any]
  322. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  323. self.checkinCompletion = obj;
  324. return obj != nil;
  325. }]])
  326. .andDo(^(NSInvocation *invocation) {
  327. checkinServiceInvocationCount++;
  328. // Give the checkin service some time to complete the request
  329. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)),
  330. dispatch_get_main_queue(), ^{
  331. self.checkinCompletion([self validCheckinPreferences], nil);
  332. });
  333. });
  334. // Always return YES for whether we succeeded in persisting the checkin
  335. OCMStub([self.mockStore
  336. saveCheckinPreferences:[OCMArg any]
  337. handler:([OCMArg invokeBlockWithArgs:[NSNull null], nil])]);
  338. // Start a scheduled (though immediate) checkin
  339. [self.authService scheduleCheckin:YES];
  340. // Request a direct checkin fetch
  341. [self.authService
  342. fetchCheckinInfoWithHandler:^(FIRMessagingCheckinPreferences *checkin, NSError *error) {
  343. [fetchHandlerCalledExpectation fulfill];
  344. }];
  345. // At this point we should have checkinHandlers, one for scheduled, one for the direct fetch
  346. XCTAssertEqual(self.authService.checkinHandlers.count, 2);
  347. [self waitForExpectationsWithTimeout:0.5 handler:nil];
  348. // Make sure only one checkin fetch was performed
  349. XCTAssertEqual(checkinServiceInvocationCount, 1);
  350. }
  351. #pragma mark - Helper Methods
  352. - (FIRMessagingCheckinPreferences *)validCheckinPreferences {
  353. NSDictionary *gservicesData = @{
  354. kFIRMessagingVersionInfoStringKey : kVersionInfo,
  355. kFIRMessagingLastCheckinTimeKey : @(FIRMessagingCurrentTimestampInMilliseconds())
  356. };
  357. FIRMessagingCheckinPreferences *checkinPreferences =
  358. [[FIRMessagingCheckinPreferences alloc] initWithDeviceID:kDeviceAuthId
  359. secretToken:kSecretToken];
  360. [checkinPreferences updateWithCheckinPlistContents:gservicesData];
  361. return checkinPreferences;
  362. }
  363. @end