Files
app-check/AppCheckCore/Tests/Unit/Core/GACAppCheckTokenRefresherTests.m
T
Andrew Heard 2d2d0e6228 Renamed AppCheck to AppCheckCore (#11581)
Renamed the App Check (Lite) SDK to App Check Core.
2023-07-21 10:17:20 -04:00

504 lines
20 KiB
Objective-C

/*
* 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 <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckSettings.h"
#import "AppCheckCore/Sources/Core/TokenRefresh/GACAppCheckTokenRefreshResult.h"
#import "AppCheckCore/Sources/Core/TokenRefresh/GACAppCheckTokenRefresher.h"
#import "AppCheckCore/Tests/Unit/Utils/GACFakeTimer.h"
#import "AppCheckCore/Tests/Utils/Date/GACDateTestUtils.h"
@interface GACAppCheckTokenRefresherTests : XCTestCase
@property(nonatomic) GACFakeTimer *fakeTimer;
@property(nonatomic) OCMockObject<GACAppCheckSettingsProtocol> *mockSettings;
@property(nonatomic) GACAppCheckTokenRefreshResult *initialTokenRefreshResult;
@end
@implementation GACAppCheckTokenRefresherTests
- (void)setUp {
self.mockSettings = OCMProtocolMock(@protocol(GACAppCheckSettingsProtocol));
self.fakeTimer = [[GACFakeTimer alloc] init];
NSDate *receivedAtDate = [NSDate date];
self.initialTokenRefreshResult = [[GACAppCheckTokenRefreshResult 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 = [[GACAppCheckTokenRefreshResult alloc] initWithStatusNever];
GACAppCheckTokenRefresher *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 GACAppCheckTokenRefreshCompletion initialRefreshCompletion;
XCTestExpectation *initialRefreshExpectation =
[self expectationWithDescription:@"initial refresh"];
refresher.tokenRefreshHandler = ^(GACAppCheckTokenRefreshCompletion _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 = [[GACAppCheckTokenRefreshResult 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 = [[GACAppCheckTokenRefreshResult alloc]
initWithStatusSuccessAndExpirationDate:[expectedRefreshDate dateByAddingTimeInterval:60 * 60]
receivedAtDate:expectedRefreshDate];
XCTestExpectation *nextRefreshExpectation = [self expectationWithDescription:@"next refresh"];
refresher.tokenRefreshHandler = ^(GACAppCheckTokenRefreshCompletion _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.
GACAppCheckTokenRefresher *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 = ^(GACAppCheckTokenRefreshCompletion _Nonnull completion) {
};
[self waitForExpectations:@[ timerCreateExpectation2 ] timeout:0.5];
OCMVerifyAll(self.mockSettings);
}
- (void)testNextRefreshOnRefreshSuccess {
GACAppCheckTokenRefresher *refresher = [self createRefresher];
NSDate *refreshedTokenExpirationDate =
[self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:60 * 60];
__auto_type refreshResult = [[GACAppCheckTokenRefreshResult 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 = ^(GACAppCheckTokenRefreshCompletion _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 {
GACAppCheckTokenRefresher *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 = ^(GACAppCheckTokenRefreshCompletion _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 =
[[GACAppCheckTokenRefreshResult 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 {
GACAppCheckTokenRefresher *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 = [[GACAppCheckTokenRefreshResult alloc]
initWithStatusSuccessAndExpirationDate:[NSDate dateWithTimeIntervalSinceNow:60 * 60]
receivedAtDate:[NSDate date]];
XCTestExpectation *refreshExpectation = [self expectationWithDescription:@"refresh"];
refreshExpectation.inverted = YES;
refresher.tokenRefreshHandler = ^(GACAppCheckTokenRefreshCompletion _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 {
GACAppCheckTokenRefresher *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 = [[GACAppCheckTokenRefreshResult alloc]
initWithStatusSuccessAndExpirationDate:[expectedTimerFireDate
dateByAddingTimeInterval:60 * 60]
receivedAtDate:expectedTimerFireDate];
XCTestExpectation *noRefreshExpectation = [self expectationWithDescription:@"initial refresh"];
noRefreshExpectation.inverted = YES;
refresher.tokenRefreshHandler = ^(GACAppCheckTokenRefreshCompletion _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 {
GACAppCheckTokenRefresher *refresher = [self createRefresher];
NSDate *newExpirationDate =
[self.initialTokenRefreshResult.tokenExpirationDate dateByAddingTimeInterval:10 * 60];
__auto_type newRefreshResult = [[GACAppCheckTokenRefreshResult 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 {
GACAppCheckTokenRefresher *refresher = [self createRefresher];
__auto_type newRefreshResult = [[GACAppCheckTokenRefreshResult 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 {
GACAppCheckTokenRefresher *refresher = [self createRefresher];
NSDate *newExpirationDate = [NSDate dateWithTimeIntervalSinceNow:0.5 * 60];
__auto_type newRefreshResult = [[GACAppCheckTokenRefreshResult 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([GACDateTestUtils 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!");
}
}
- (GACAppCheckTokenRefresher *)createRefresher {
return [[GACAppCheckTokenRefresher 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