FIRInstanceIDAuthServiceTest.m 17 KB

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