FIRMessagingAuthServiceTest.m 18 KB

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