FIRMessagingAuthServiceTest.m 19 KB

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