/* * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #import #import #import "FirebaseAppCheck/Sources/Core/FIRAppCheckSettings.h" #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefreshResult.h" #import "FirebaseAppCheck/Sources/Core/TokenRefresh/FIRAppCheckTokenRefresher.h" #import "FirebaseAppCheck/Tests/Unit/Utils/FIRFakeTimer.h" #import "SharedTestUtilities/Date/FIRDateTestUtils.h" @interface FIRAppCheckTokenRefresherTests : XCTestCase @property(nonatomic) FIRFakeTimer *fakeTimer; @property(nonatomic) OCMockObject *mockSettings; @property(nonatomic) FIRAppCheckTokenRefreshResult *initialTokenRefreshResult; @end @implementation FIRAppCheckTokenRefresherTests - (void)setUp { self.mockSettings = OCMProtocolMock(@protocol(FIRAppCheckSettingsProtocol)); self.fakeTimer = [[FIRFakeTimer alloc] init]; NSDate *receivedAtDate = [NSDate date]; self.initialTokenRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[receivedAtDate dateByAddingTimeInterval:1000] receivedAtDate:receivedAtDate]; } - (void)tearDown { self.fakeTimer = nil; [self.mockSettings stopMocking]; self.mockSettings = nil; } #pragma mark - Auto refresh is allowed - (void)testInitialRefreshWhenAutoRefreshAllowed { __auto_type weakSelf = self; self.initialTokenRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusNever]; FIRAppCheckTokenRefresher *refresher = [self createRefresher]; // 1. Expect checking if auto-refresh allowed before scheduling the initial refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 2. Don't expect the timer to be scheduled for the first refresh as the refresh should be // triggered straight away. XCTestExpectation *initialTimerCreatedExpectation = [self expectationWithDescription:@"initial refresh timer created"]; initialTimerCreatedExpectation.inverted = YES; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { weakSelf.fakeTimer.createHandler = nil; [initialTimerCreatedExpectation fulfill]; }; // 3. Expect checking if auto-refresh allowed before triggering the initial refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 4. Expect initial refresh handler to be called. __block FIRAppCheckTokenRefreshCompletion initialRefreshCompletion; XCTestExpectation *initialRefreshExpectation = [self expectationWithDescription:@"initial refresh"]; refresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) { // Save completion to be called later. initialRefreshCompletion = completion; [initialRefreshExpectation fulfill]; }; NSDate *initialTokenExpirationDate = [NSDate dateWithTimeIntervalSinceNow:60 * 60]; NSDate *initialTokenReceivedDate = [NSDate date]; __auto_type initialRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:initialTokenExpirationDate receivedAtDate:initialTokenReceivedDate]; [self waitForExpectations:@[ initialTimerCreatedExpectation, initialRefreshExpectation ] timeout:1]; // 5. Expect checking if auto-refresh allowed before scheduling next refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 6. Expect a next refresh timer to be scheduled on initial refresh completion. NSDate *expectedRefreshDate = [self expectedRefreshDateWithReceivedDate:initialTokenReceivedDate expirationDate:initialTokenExpirationDate]; XCTestExpectation *nextTimerCreateExpectation = [self expectationWithDescription:@"next refresh create timer"]; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { weakSelf.fakeTimer.createHandler = nil; XCTAssertEqualObjects(fireDate, expectedRefreshDate); [nextTimerCreateExpectation fulfill]; }; // 7. Call initial refresh completion and wait for next refresh timer to be scheduled. initialRefreshCompletion(initialRefreshResult); [self waitForExpectations:@[ nextTimerCreateExpectation ] timeout:0.5]; // 8. Expect checking if auto-refresh allowed before triggering the next refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 9. Expect refresh handler to be called for the next refresh. __auto_type nextRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[expectedRefreshDate dateByAddingTimeInterval:60 * 60] receivedAtDate:expectedRefreshDate]; XCTestExpectation *nextRefreshExpectation = [self expectationWithDescription:@"next refresh"]; refresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) { [nextRefreshExpectation fulfill]; // Call completion. completion(nextRefreshResult); }; // 10. Fire the timer. [self fireTimer]; // 11. Wait for the next refresh handler to be called. [self waitForExpectations:@[ nextRefreshExpectation ] timeout:1]; OCMVerifyAll(self.mockSettings); } - (void)testNoTimeScheduledUntilHandlerSet { // 1. Don't expect timer to be scheduled. XCTestExpectation *timerCreateExpectation1 = [self expectationWithDescription:@"create timer 1"]; timerCreateExpectation1.inverted = YES; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { [timerCreateExpectation1 fulfill]; }; // 2. Create a publisher. FIRAppCheckTokenRefresher *refresher = [self createRefresher]; XCTAssertNotNil(refresher); [self waitForExpectations:@[ timerCreateExpectation1 ] timeout:0.5]; // 3. Expect timer to be created after the handler has been set. // 3.1. Expect checking if auto-refresh allowed one more time when timer fires. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 3.2. Expect timer to fire. XCTestExpectation *timerCreateExpectation2 = [self expectationWithDescription:@"create timer 2"]; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { [timerCreateExpectation2 fulfill]; }; // 3.3. Set handler. refresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) { }; [self waitForExpectations:@[ timerCreateExpectation2 ] timeout:0.5]; OCMVerifyAll(self.mockSettings); } - (void)testNextRefreshOnRefreshSuccess { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; NSDate *refreshedTokenExpirationDate = [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:60 * 60]; __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:refreshedTokenExpirationDate receivedAtDate:self.initialTokenRefreshResult.tokenExpirationDate]; // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 2. Expect checking if auto-refresh allowed before calling the refresh handler. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 3. Expect refresh handler. XCTestExpectation *initialRefreshExpectation = [self expectationWithDescription:@"initial refresh"]; refresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) { [initialRefreshExpectation fulfill]; // Call completion in a while. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ completion(refreshResult); }); }; // 4. Expect for new timer to be created. NSDate *expectedFireDate = [self expectedRefreshDateWithReceivedDate:refreshResult.tokenReceivedAtDate expirationDate:refreshResult.tokenExpirationDate]; XCTestExpectation *createTimerExpectation = [self expectationWithDescription:@"create timer"]; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { [createTimerExpectation fulfill]; XCTAssertEqualObjects(fireDate, expectedFireDate); }; // 5. Expect checking if auto-refresh allowed before refreshing. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 6. Fire initial timer and wait for expectations. [self fireTimer]; [self waitForExpectations:@[ initialRefreshExpectation, createTimerExpectation ] timeout:1 enforceOrder:YES]; OCMVerifyAll(self.mockSettings); } - (void)testBackoff { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; // Initial backoff interval. NSTimeInterval expectedBackoffTime = 0; NSTimeInterval maximumBackoffTime = 16 * 60; // 16 min. // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; for (NSInteger i = 0; i < 10; i++) { // 2. Expect checking if auto-refresh allowed before calling the refresh handler. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 3. Expect refresh handler. XCTestExpectation *initialRefreshExpectation = [self expectationWithDescription:@"initial refresh"]; refresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) { [initialRefreshExpectation fulfill]; // Call completion in a while. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ __auto_type refreshFailure = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusFailure]; completion(refreshFailure); }); }; // 4. Expect for new timer to be created. // No backoff initially, 1st backoff 30sec, double backoff on each next attempt until 16min. expectedBackoffTime = expectedBackoffTime == 0 ? 30 : expectedBackoffTime * 2; expectedBackoffTime = MIN(expectedBackoffTime, maximumBackoffTime); NSDate *expectedFireDate = [[NSDate date] dateByAddingTimeInterval:expectedBackoffTime]; XCTestExpectation *createTimerExpectation = [self expectationWithDescription:@"create timer"]; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { [createTimerExpectation fulfill]; // Check expected and actual fire date are not too different (account for the random part // and request attempt delay). XCTAssertLessThan(ABS([expectedFireDate timeIntervalSinceDate:fireDate]), 2); }; // 5. Expect checking if auto-refresh allowed before refreshing. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 6. Fire initial timer and wait for expectations. [self fireTimer]; [self waitForExpectations:@[ initialRefreshExpectation, createTimerExpectation ] timeout:1 enforceOrder:YES]; } OCMVerifyAll(self.mockSettings); } #pragma mark - Auto refresh is not allowed - (void)testNoInitialRefreshWhenAutoRefreshIsNotAllowed { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. [[[self.mockSettings expect] andReturnValue:@(NO)] isTokenAutoRefreshEnabled]; // 2. Don't expect timer to be scheduled. XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; timerCreateExpectation.inverted = YES; __auto_type weakSelf = self; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { weakSelf.fakeTimer.createHandler = nil; [timerCreateExpectation fulfill]; }; // 3. Don't expect refresh handler to be called. __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[NSDate dateWithTimeIntervalSinceNow:60 * 60] receivedAtDate:[NSDate date]]; XCTestExpectation *refreshExpectation = [self expectationWithDescription:@"refresh"]; refreshExpectation.inverted = YES; refresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) { [refreshExpectation fulfill]; // Call completion. completion(refreshResult); }; // 4. Check if the handler is not fired before the timer. [self waitForExpectations:@[ timerCreateExpectation, refreshExpectation ] timeout:1]; OCMVerifyAll(self.mockSettings); } - (void)testNoRefreshWhenAutoRefreshWasDisabledAfterInit { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 2. Expect timer to be scheduled. NSDate *expectedTimerFireDate = [self expectedRefreshDateWithReceivedDate:self.initialTokenRefreshResult.tokenReceivedAtDate expirationDate:self.initialTokenRefreshResult.tokenExpirationDate]; XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; __auto_type weakSelf = self; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { weakSelf.fakeTimer.createHandler = nil; XCTAssertEqualObjects(fireDate, expectedTimerFireDate); [timerCreateExpectation fulfill]; }; // 3. Expect refresh handler to be called. __auto_type refreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[expectedTimerFireDate dateByAddingTimeInterval:60 * 60] receivedAtDate:expectedTimerFireDate]; XCTestExpectation *noRefreshExpectation = [self expectationWithDescription:@"initial refresh"]; noRefreshExpectation.inverted = YES; refresher.tokenRefreshHandler = ^(FIRAppCheckTokenRefreshCompletion _Nonnull completion) { [noRefreshExpectation fulfill]; // Call completion. completion(refreshResult); }; // 4. Check if the handler is not fired before the timer. [self waitForExpectations:@[ timerCreateExpectation ] timeout:1]; // 5. Expect checking if auto-refresh allowed before refreshing. [[[self.mockSettings expect] andReturnValue:@(NO)] isTokenAutoRefreshEnabled]; // 6. Fire the timer and wait for completion. [self fireTimer]; [self waitForExpectations:@[ noRefreshExpectation ] timeout:1]; OCMVerifyAll(self.mockSettings); } #pragma mark - Update token expiration - (void)testUpdateWithRefreshResultWhenAutoRefreshIsAllowed { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; NSDate *newExpirationDate = [self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:10 * 60]; __auto_type newRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:newExpirationDate receivedAtDate:self.initialTokenRefreshResult.tokenExpirationDate]; // 1. Expect checking if auto-refresh allowed before scheduling refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 2. Expect timer to be scheduled. NSDate *expectedTimerFireDate = [self expectedRefreshDateWithReceivedDate:newRefreshResult.tokenReceivedAtDate expirationDate:newRefreshResult.tokenExpirationDate]; XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; __auto_type weakSelf = self; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { weakSelf.fakeTimer.createHandler = nil; XCTAssertEqualObjects(fireDate, expectedTimerFireDate); [timerCreateExpectation fulfill]; }; // 3. Update token expiration date. [refresher updateWithRefreshResult:newRefreshResult]; // 4. Wait for timer to be created. [self waitForExpectations:@[ timerCreateExpectation ] timeout:1]; OCMVerifyAll(self.mockSettings); } - (void)testUpdateWithRefreshResultWhenAutoRefreshIsNotAllowed { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; __auto_type newRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:[NSDate dateWithTimeIntervalSinceNow:60 * 60] receivedAtDate:self.initialTokenRefreshResult.tokenExpirationDate]; // 1. Expect checking if auto-refresh allowed before scheduling initial refresh. [[[self.mockSettings expect] andReturnValue:@(NO)] isTokenAutoRefreshEnabled]; // 2. Don't expect timer to be scheduled. XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; timerCreateExpectation.inverted = YES; __auto_type weakSelf = self; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { weakSelf.fakeTimer.createHandler = nil; [timerCreateExpectation fulfill]; }; // 3. Update token expiration date. [refresher updateWithRefreshResult:newRefreshResult]; // 4. Wait for timer to be created. [self waitForExpectations:@[ timerCreateExpectation ] timeout:1]; OCMVerifyAll(self.mockSettings); } - (void)testUpdateWithRefreshResult_WhenTokenExpiresLessThanIn1Minute { FIRAppCheckTokenRefresher *refresher = [self createRefresher]; NSDate *newExpirationDate = [NSDate dateWithTimeIntervalSinceNow:0.5 * 60]; __auto_type newRefreshResult = [[FIRAppCheckTokenRefreshResult alloc] initWithStatusSuccessAndExpirationDate:newExpirationDate receivedAtDate:[NSDate date]]; // 1. Expect checking if auto-refresh allowed before scheduling refresh. [[[self.mockSettings expect] andReturnValue:@(YES)] isTokenAutoRefreshEnabled]; // 2. Expect timer to be scheduled in at least 1 minute. XCTestExpectation *timerCreateExpectation = [self expectationWithDescription:@"create timer"]; __auto_type weakSelf = self; self.fakeTimer.createHandler = ^(NSDate *_Nonnull fireDate) { weakSelf.fakeTimer.createHandler = nil; // 1 minute is the minimal interval between successful refreshes. XCTAssert([FIRDateTestUtils isDate:fireDate approximatelyEqualCurrentPlusTimeInterval:60 precision:1]); [timerCreateExpectation fulfill]; }; // 3. Update token expiration date. [refresher updateWithRefreshResult:newRefreshResult]; // 4. Wait for timer to be created. [self waitForExpectations:@[ timerCreateExpectation ] timeout:1]; OCMVerifyAll(self.mockSettings); } #pragma mark - Helpers - (void)fireTimer { if (self.fakeTimer.handler) { self.fakeTimer.handler(); } else { XCTFail(@"handler must not be nil!"); } } - (FIRAppCheckTokenRefresher *)createRefresher { return [[FIRAppCheckTokenRefresher alloc] initWithRefreshResult:self.initialTokenRefreshResult timerProvider:[self.fakeTimer fakeTimerProvider] settings:self.mockSettings]; } - (NSDate *)expectedRefreshDateWithReceivedDate:(NSDate *)receivedDate expirationDate:(NSDate *)expirationDate { NSTimeInterval timeToLive = [expirationDate timeIntervalSinceDate:receivedDate]; XCTAssertGreaterThanOrEqual(timeToLive, 0); NSTimeInterval timeToRefresh = timeToLive / 2 + 5 * 60; // 50% of TTL + 5 min NSTimeInterval minimalAutoRefreshInterval = 60; // 1 min timeToRefresh = MAX(timeToRefresh, minimalAutoRefreshInterval); NSDate *refreshDate = [receivedDate dateByAddingTimeInterval:timeToRefresh]; NSDate *now = [NSDate date]; return [refreshDate laterDate:now]; } @end