FIRInstanceIDAuthServiceTest.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. /*
  2. * Copyright 2019 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <XCTest/XCTest.h>
  17. #import <OCMock/OCMock.h>
  18. #import "Firebase/InstanceID/FIRInstanceIDAuthService.h"
  19. #import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h"
  20. #import "Firebase/InstanceID/FIRInstanceIDCheckinService.h"
  21. #import "Firebase/InstanceID/FIRInstanceIDStore.h"
  22. #import "Firebase/InstanceID/NSError+FIRInstanceID.h"
  23. static NSString *const kDeviceAuthId = @"device-id";
  24. static NSString *const kSecretToken = @"secret-token";
  25. static NSString *const kVersionInfo = @"1.0";
  26. @interface FIRInstanceIDCheckinService ()
  27. @property(nonatomic, readwrite, strong) FIRInstanceIDCheckinPreferences *checkinPreferences;
  28. @end
  29. @interface FIRInstanceIDAuthService ()
  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<FIRInstanceIDDeviceCheckinCompletion> *checkinHandlers;
  35. @end
  36. @interface FIRInstanceIDAuthServiceTest : XCTestCase
  37. @property(nonatomic, readwrite, strong) FIRInstanceIDAuthService *authService;
  38. @property(nonatomic, readwrite, strong) FIRInstanceIDCheckinService *checkinService;
  39. @property(nonatomic, readwrite, strong) id mockCheckinService;
  40. @property(nonatomic, readwrite, strong) id mockStore;
  41. @property(nonatomic, readwrite, copy) FIRInstanceIDDeviceCheckinCompletion checkinCompletion;
  42. @end
  43. @implementation FIRInstanceIDAuthServiceTest
  44. - (void)setUp {
  45. [super setUp];
  46. _mockStore = OCMClassMock([FIRInstanceIDStore class]);
  47. _checkinService = [[FIRInstanceIDCheckinService alloc] init];
  48. _mockCheckinService = OCMPartialMock(_checkinService);
  49. _authService = [[FIRInstanceIDAuthService alloc] initWithCheckinService:_mockCheckinService
  50. store:_mockStore];
  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:FIRInstanceIDCurrentLocale()
  54. forKey:kFIRInstanceIDUserDefaultsKeyLocale];
  55. }
  56. - (void)tearDown {
  57. _checkinCompletion = nil;
  58. [super tearDown];
  59. }
  60. /**
  61. * Test scheduling a checkin which completes successfully. Once the checkin is complete
  62. * we should have the valid checkin preferences in memory.
  63. */
  64. - (void)testScheduleCheckin_initialSuccess {
  65. XCTestExpectation *checkinExpectation =
  66. [self expectationWithDescription:@"Did call checkin service"];
  67. FIRInstanceIDCheckinPreferences *checkinPreferences = [self validCheckinPreferences];
  68. [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
  69. self.checkinCompletion(checkinPreferences, nil);
  70. }] checkinWithExistingCheckin:[OCMArg any]
  71. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  72. [checkinExpectation fulfill];
  73. self.checkinCompletion = obj;
  74. return obj != nil;
  75. }]];
  76. // Always return YES for whether we succeeded in persisting the checkin
  77. [[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
  78. handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]];
  79. [self.authService scheduleCheckin:YES];
  80. XCTAssertTrue([self.authService hasValidCheckinInfo]);
  81. XCTAssertEqual([self.authService checkinRetryCount], 1);
  82. [self waitForExpectationsWithTimeout:2.0 handler:NULL];
  83. }
  84. /**
  85. * Test scheduling a checkin which completes successfully, but fails to save, due to Keychain
  86. * errors.
  87. */
  88. - (void)testScheduleCheckin_successButFailureInSaving {
  89. XCTestExpectation *checkinFailureExpectation =
  90. [self expectationWithDescription:@"Did receive error after checkin"];
  91. FIRInstanceIDCheckinPreferences *checkinPreferences = [self validCheckinPreferences];
  92. [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
  93. self.checkinCompletion(checkinPreferences, nil);
  94. }] checkinWithExistingCheckin:[OCMArg any]
  95. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  96. self.checkinCompletion = obj;
  97. return obj != nil;
  98. }]];
  99. // Always return NO for whether we succeeded in persisting the checkin, to simulate Keychain error
  100. [[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
  101. handler:[OCMArg invokeBlockWithArgs:[OCMArg any], nil]];
  102. [self.authService
  103. fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *checkin, NSError *error) {
  104. [checkinFailureExpectation fulfill];
  105. }];
  106. [self waitForExpectationsWithTimeout:2.0 handler:NULL];
  107. XCTAssertFalse([self.authService hasValidCheckinInfo]);
  108. }
  109. /**
  110. * Test scheduling multiple checkins to complete immediately. Each successive checkin should
  111. * be triggered immediately.
  112. */
  113. - (void)testMultipleScheduleCheckin_immediately {
  114. XCTestExpectation *checkinExpectation =
  115. [self expectationWithDescription:@"Did call checkin service"];
  116. __block int checkinHandlerInvocationCount = 0;
  117. FIRInstanceIDCheckinPreferences *checkinPreferences = [self validCheckinPreferences];
  118. [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
  119. checkinHandlerInvocationCount++;
  120. // Mock successful Checkin after delay.
  121. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
  122. dispatch_get_main_queue(), ^{
  123. [checkinExpectation fulfill];
  124. self.checkinCompletion(checkinPreferences, nil);
  125. });
  126. }] checkinWithExistingCheckin:[OCMArg any]
  127. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  128. self.checkinCompletion = obj;
  129. return obj != nil;
  130. }]];
  131. // Always return YES for whether we succeeded in persisting the checkin
  132. [[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
  133. handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]];
  134. [self.authService scheduleCheckin:YES];
  135. // Schedule an immediate checkin again.
  136. // This should just return because the previous checkin isn't over yet.
  137. [self.authService scheduleCheckin:YES];
  138. [self waitForExpectationsWithTimeout:5.0 handler:NULL];
  139. XCTAssertTrue([self.authService hasValidCheckinInfo]);
  140. XCTAssertEqual([self.authService checkinRetryCount], 2);
  141. // Checkin handler should only be invoked once since the second checkin request should
  142. // return immediately.
  143. XCTAssertEqual(checkinHandlerInvocationCount, 1);
  144. }
  145. /**
  146. * Test multiple checkins scheduled. The second checkin should be scheduled after some
  147. * delay before the first checkin has returned. Since the latter checkin is not immediate
  148. * we should not run it since the first checkin is already scheduled to be executed later.
  149. */
  150. - (void)testMultipleScheduleCheckin_notImmediately {
  151. XCTestExpectation *checkinExpectation =
  152. [self expectationWithDescription:@"Did call checkin service"];
  153. FIRInstanceIDCheckinPreferences *checkinPreferences = [self validCheckinPreferences];
  154. [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
  155. // Mock successful Checkin after delay.
  156. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
  157. dispatch_get_main_queue(), ^{
  158. [checkinExpectation fulfill];
  159. self.checkinCompletion(checkinPreferences, nil);
  160. });
  161. }] checkinWithExistingCheckin:[OCMArg any]
  162. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  163. self.checkinCompletion = obj;
  164. return obj != nil;
  165. }]];
  166. // Always return YES for whether we succeeded in persisting the checkin
  167. [[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
  168. handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]];
  169. [self.authService scheduleCheckin:YES];
  170. // Schedule another checkin after some delay while the first checkin has not yet returned
  171. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)),
  172. dispatch_get_main_queue(), ^{
  173. [self.authService scheduleCheckin:NO];
  174. });
  175. [self waitForExpectationsWithTimeout:5.0 handler:NULL];
  176. XCTAssertTrue([self.authService hasValidCheckinInfo]);
  177. XCTAssertEqual([self.authService checkinRetryCount], 1);
  178. }
  179. /**
  180. * Test initial checkin failure which schedules another checkin which should succeed.
  181. */
  182. - (void)testInitialCheckinFailure_retrySuccess {
  183. XCTestExpectation *checkinExpectation =
  184. [self expectationWithDescription:@"Did call checkin service"];
  185. __block int checkinHandlerInvocationCount = 0;
  186. [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
  187. checkinHandlerInvocationCount++;
  188. if (checkinHandlerInvocationCount == 1) {
  189. // Mock failure on first try
  190. NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeTimeout];
  191. self.checkinCompletion(nil, error);
  192. } else if (checkinHandlerInvocationCount == 2) {
  193. // Mock success on second try
  194. [checkinExpectation fulfill];
  195. self.checkinCompletion([self validCheckinPreferences], nil);
  196. } else {
  197. // We should not retry for a third time again.
  198. XCTFail(@"Invoking checkin handler invalid number of times.");
  199. }
  200. }] checkinWithExistingCheckin:[OCMArg any]
  201. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  202. self.checkinCompletion = obj;
  203. return obj != nil;
  204. }]];
  205. // Always return YES for whether we succeeded in persisting the checkin
  206. [[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
  207. handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]];
  208. [self.authService scheduleCheckin:YES];
  209. // Schedule another checkin after some delay while the first checkin has not yet returned
  210. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
  211. dispatch_get_main_queue(), ^{
  212. [self.authService scheduleCheckin:YES];
  213. XCTAssertTrue([self.authService hasValidCheckinInfo]);
  214. XCTAssertEqual([self.authService checkinRetryCount], 2);
  215. XCTAssertEqual(checkinHandlerInvocationCount, 2);
  216. });
  217. [self waitForExpectationsWithTimeout:5.0 handler:NULL];
  218. }
  219. /**
  220. * Test initial checkin failure which schedules another checkin which should succeed. If
  221. * a new checkin request comes after that we should not schedule a checkin as we have
  222. * already have valid checkin credentials.
  223. */
  224. - (void)testInitialCheckinFailure_multipleRetrySuccess {
  225. XCTestExpectation *checkinExpectation =
  226. [self expectationWithDescription:@"Did call checkin service"];
  227. __block int checkinHandlerInvocationCount = 0;
  228. [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
  229. checkinHandlerInvocationCount++;
  230. if (checkinHandlerInvocationCount <= 2) {
  231. // Mock failure on first try
  232. NSError *error = [NSError errorWithFIRInstanceIDErrorCode:kFIRInstanceIDErrorCodeTimeout];
  233. self.checkinCompletion(nil, error);
  234. } else if (checkinHandlerInvocationCount == 3) {
  235. // Mock success on second try
  236. [checkinExpectation fulfill];
  237. self.checkinCompletion([self validCheckinPreferences], nil);
  238. } else {
  239. // We should not retry for a third time again.
  240. XCTFail(@"Invoking checkin handler invalid number of times.");
  241. }
  242. }] checkinWithExistingCheckin:[OCMArg any]
  243. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  244. self.checkinCompletion = obj;
  245. return obj != nil;
  246. }]];
  247. // Always return YES for whether we succeeded in persisting the checkin
  248. [[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
  249. handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]];
  250. [self.authService scheduleCheckin:YES];
  251. [self waitForExpectationsWithTimeout:10.0 handler:NULL];
  252. XCTAssertTrue([self.authService hasValidCheckinInfo]);
  253. XCTAssertEqual([self.authService checkinRetryCount], 3);
  254. }
  255. /**
  256. * Performing multiple checkin requests should result in multiple handlers being
  257. * called back, but with only a single actual checkin fetch.
  258. */
  259. - (void)testMultipleCheckinHandlersWithSuccessfulCheckin {
  260. XCTestExpectation *allHandlersCalledExpectation =
  261. [self expectationWithDescription:@"All checkin handlers were called"];
  262. __block NSInteger checkinHandlerCallbackCount = 0;
  263. __block NSInteger checkinServiceInvocationCount = 0;
  264. // Always return a successful checkin, and count the number of times CheckinService is called
  265. [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
  266. checkinServiceInvocationCount++;
  267. self.checkinCompletion([self validCheckinPreferences], nil);
  268. }] checkinWithExistingCheckin:[OCMArg any]
  269. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  270. self.checkinCompletion = obj;
  271. return obj != nil;
  272. }]];
  273. // Always return YES for whether we succeeded in persisting the checkin
  274. [[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
  275. handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]];
  276. NSInteger numHandlers = 10;
  277. for (NSInteger i = 0; i < numHandlers; i++) {
  278. [self.authService
  279. fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *checkin, NSError *error) {
  280. checkinHandlerCallbackCount++;
  281. if (checkinHandlerCallbackCount == numHandlers) {
  282. [allHandlersCalledExpectation fulfill];
  283. }
  284. }];
  285. }
  286. [self waitForExpectationsWithTimeout:1.0 handler:nil];
  287. XCTAssertEqual(checkinServiceInvocationCount, 1);
  288. XCTAssertEqual(checkinHandlerCallbackCount, numHandlers);
  289. }
  290. /**
  291. * Performing a scheduled checkin *and* simultaneous checkin request should result in
  292. * the number of pending checkin handlers to be 2 (one for the scheduled checkin, one for
  293. * the direct fetch).
  294. */
  295. - (void)testScheduledAndImmediateCheckinsWithMultipleHandler {
  296. XCTestExpectation *fetchHandlerCalledExpectation =
  297. [self expectationWithDescription:@"Direct checkin handler was called"];
  298. __block NSInteger checkinServiceInvocationCount = 0;
  299. // Always return a successful checkin, and count the number of times CheckinService is called
  300. [[[self.mockCheckinService stub] andDo:^(NSInvocation *invocation) {
  301. checkinServiceInvocationCount++;
  302. // Give the checkin service some time to complete the request
  303. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)),
  304. dispatch_get_main_queue(), ^{
  305. self.checkinCompletion([self validCheckinPreferences], nil);
  306. });
  307. }] checkinWithExistingCheckin:[OCMArg any]
  308. completion:[OCMArg checkWithBlock:^BOOL(id obj) {
  309. self.checkinCompletion = obj;
  310. return obj != nil;
  311. }]];
  312. // Always return YES for whether we succeeded in persisting the checkin
  313. [[self.mockStore stub] saveCheckinPreferences:[OCMArg any]
  314. handler:[OCMArg invokeBlockWithArgs:[NSNull null], nil]];
  315. // Start a scheduled (though immediate) checkin
  316. [self.authService scheduleCheckin:YES];
  317. // Request a direct checkin fetch
  318. [self.authService
  319. fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *checkin, NSError *error) {
  320. [fetchHandlerCalledExpectation fulfill];
  321. }];
  322. // At this point we should have checkinHandlers, one for scheduled, one for the direct fetch
  323. XCTAssertEqual(self.authService.checkinHandlers.count, 2);
  324. [self waitForExpectationsWithTimeout:0.5 handler:nil];
  325. // Make sure only one checkin fetch was performed
  326. XCTAssertEqual(checkinServiceInvocationCount, 1);
  327. }
  328. #pragma mark - Helper Methods
  329. - (FIRInstanceIDCheckinPreferences *)validCheckinPreferences {
  330. NSDictionary *gservicesData = @{
  331. kFIRInstanceIDVersionInfoStringKey : kVersionInfo,
  332. kFIRInstanceIDLastCheckinTimeKey : @(FIRInstanceIDCurrentTimestampInMilliseconds())
  333. };
  334. FIRInstanceIDCheckinPreferences *checkinPreferences =
  335. [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:kDeviceAuthId
  336. secretToken:kSecretToken];
  337. [checkinPreferences updateWithCheckinPlistContents:gservicesData];
  338. return checkinPreferences;
  339. }
  340. @end