refactor: migrate GACAppCheckTests off OCMock (Milestone 1) (#107)

This commit is contained in:
Nick Cooke authored and GitHub committed 2026-07-17 13:35:53 -04:00
1 parent 170b466a06
commit 3c6d658bbb
16 files changed
+556 -230

No files matched your search

+107 -221
View File
@@ -16,7 +16,11 @@
#import <XCTest/XCTest.h>
#import <OCMock/OCMock.h>
#import "AppCheckCore/Tests/Unit/Utils/GACAppCheckProviderFake.h"
#import "AppCheckCore/Tests/Unit/Utils/GACAppCheckSettingsFake.h"
#import "AppCheckCore/Tests/Unit/Utils/GACAppCheckStorageFake.h"
#import "AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenDelegateFake.h"
#import "AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenRefresherFake.h"
#import "FBLPromise+Testing.h"
@@ -54,15 +58,13 @@ static NSString *const kAppGroupID = @"app_group_id";
@interface GACAppCheckTests : XCTestCase
@property(nonatomic) OCMockObject<GACAppCheckStorageProtocol> *mockStorage;
@property(nonatomic) OCMockObject<GACAppCheckProvider> *mockAppCheckProvider;
@property(nonatomic) OCMockObject<GACAppCheckTokenRefresherProtocol> *mockTokenRefresher;
@property(nonatomic) OCMockObject<GACAppCheckSettingsProtocol> *mockSettings;
@property(nonatomic) OCMockObject<GACAppCheckTokenDelegate> *mockTokenDelegate;
@property(nonatomic) GACAppCheckStorageFake *fakeStorage;
@property(nonatomic) GACAppCheckProviderFake *fakeAppCheckProvider;
@property(nonatomic) GACAppCheckTokenRefresherFake *fakeTokenRefresher;
@property(nonatomic) GACAppCheckSettingsFake *fakeSettings;
@property(nonatomic) GACAppCheckTokenDelegateFake *fakeTokenDelegate;
@property(nonatomic) GACAppCheck *appCheck;
@property(nonatomic, copy, nullable) GACAppCheckTokenRefreshBlock tokenRefreshHandler;
@end
@implementation GACAppCheckTests
@@ -70,102 +72,33 @@ static NSString *const kAppGroupID = @"app_group_id";
- (void)setUp {
[super setUp];
self.mockStorage = OCMStrictProtocolMock(@protocol(GACAppCheckStorageProtocol));
self.mockAppCheckProvider = OCMStrictProtocolMock(@protocol(GACAppCheckProvider));
self.mockTokenRefresher = OCMStrictProtocolMock(@protocol(GACAppCheckTokenRefresherProtocol));
self.mockSettings = OCMStrictProtocolMock(@protocol(GACAppCheckSettingsProtocol));
self.mockTokenDelegate = OCMStrictProtocolMock(@protocol(GACAppCheckTokenDelegate));
[self stubSetTokenRefreshHandler];
self.fakeStorage = [[GACAppCheckStorageFake alloc] init];
self.fakeAppCheckProvider = [[GACAppCheckProviderFake alloc] init];
self.fakeTokenRefresher = [[GACAppCheckTokenRefresherFake alloc] init];
self.fakeSettings = [[GACAppCheckSettingsFake alloc] init];
self.fakeTokenDelegate = [[GACAppCheckTokenDelegateFake alloc] init];
self.appCheck = [[GACAppCheck alloc] initWithServiceName:kAppName
appCheckProvider:self.mockAppCheckProvider
storage:self.mockStorage
tokenRefresher:self.mockTokenRefresher
settings:self.mockSettings
tokenDelegate:self.mockTokenDelegate];
appCheckProvider:self.fakeAppCheckProvider
storage:self.fakeStorage
tokenRefresher:self.fakeTokenRefresher
settings:self.fakeSettings
tokenDelegate:self.fakeTokenDelegate];
}
- (void)tearDown {
self.appCheck = nil;
[self.mockAppCheckProvider stopMocking];
self.mockAppCheckProvider = nil;
[self.mockStorage stopMocking];
self.mockStorage = nil;
[self.mockTokenRefresher stopMocking];
self.mockTokenRefresher = nil;
self.fakeAppCheckProvider = nil;
self.fakeStorage = nil;
self.fakeTokenRefresher = nil;
self.fakeSettings = nil;
self.fakeTokenDelegate = nil;
[super tearDown];
}
#pragma mark - Public Init
- (void)testAppCheckInit {
NSString *tokenKey =
[NSString stringWithFormat:@"app_check_token.%@.%@", kAppName, kResourceName];
// 1. Stub GACAppCheckTokenRefresher and validate usage.
id mockTokenRefresher = OCMClassMock([GACAppCheckTokenRefresher class]);
OCMExpect([mockTokenRefresher alloc]).andReturn(mockTokenRefresher);
id refresherDateValidator =
[OCMArg checkWithBlock:^BOOL(GACAppCheckTokenRefreshResult *refreshResult) {
XCTAssertEqual(refreshResult.status, GACAppCheckTokenRefreshStatusNever);
XCTAssertEqual(refreshResult.tokenExpirationDate, nil);
XCTAssertEqual(refreshResult.tokenReceivedAtDate, nil);
return YES;
}];
id settingsValidator = [OCMArg checkWithBlock:^BOOL(id obj) {
XCTAssert([obj conformsToProtocol:@protocol(GACAppCheckSettingsProtocol)]);
return YES;
}];
OCMExpect([mockTokenRefresher initWithRefreshResult:refresherDateValidator
settings:settingsValidator])
.andReturn(mockTokenRefresher);
OCMExpect([mockTokenRefresher setTokenRefreshHandler:[OCMArg any]]);
// 2. Stub GACAppCheckStorage and validate usage.
id mockStorage = OCMStrictClassMock([GACAppCheckStorage class]);
OCMExpect([mockStorage alloc]).andReturn(mockStorage);
OCMExpect([mockStorage initWithTokenKey:tokenKey accessGroup:kAppGroupID]).andReturn(mockStorage);
// 3. Stub attestation provider.
OCMockObject<GACAppCheckProvider> *mockProvider =
OCMStrictProtocolMock(@protocol(GACAppCheckProvider));
// 4. Stub GACAppCheckSettingsProtocol.
OCMockObject<GACAppCheckSettingsProtocol> *mockSettings =
OCMStrictProtocolMock(@protocol(GACAppCheckSettingsProtocol));
// 5. Stub GACAppCheckTokenDelegate.
OCMockObject<GACAppCheckTokenDelegate> *mockTokenDelegate =
OCMStrictProtocolMock(@protocol(GACAppCheckTokenDelegate));
// 6. Call init.
GACAppCheck *appCheck = [[GACAppCheck alloc] initWithServiceName:kAppName
resourceName:kResourceName
appCheckProvider:mockProvider
settings:mockSettings
tokenDelegate:mockTokenDelegate
keychainAccessGroup:kAppGroupID];
XCTAssert([appCheck isKindOfClass:[GACAppCheck class]]);
// 7. Verify mocks.
OCMVerifyAll(mockTokenRefresher);
OCMVerifyAll(mockStorage);
OCMVerifyAll(mockProvider);
OCMVerifyAll(mockSettings);
OCMVerifyAll(mockTokenDelegate);
// 8. Stop mocking real class mocks.
[mockTokenRefresher stopMocking];
mockTokenRefresher = nil;
[mockStorage stopMocking];
mockStorage = nil;
}
#pragma mark - Public Get Token
- (void)testGetToken_WhenNoCache_Success {
@@ -185,7 +118,11 @@ static NSString *const kAppGroupID = @"app_group_id";
// 3. Wait for expectations and validate mocks.
[self waitForExpectations:@[ expectation ] timeout:0.5];
[self verifyAllMocks];
XCTAssertEqual(self.fakeAppCheckProvider.getTokenCallCount, 1);
XCTAssertEqualObjects(self.fakeStorage.lastSetToken, expectedToken);
XCTAssertEqual(self.fakeTokenRefresher.updateWithRefreshResultCallCount, 1);
XCTAssertEqualObjects(self.fakeTokenDelegate.lastToken, expectedToken);
}
- (void)testGetToken_WhenCachedTokenIsValid_Success {
@@ -209,7 +146,11 @@ static NSString *const kAppGroupID = @"app_group_id";
// 3. Wait for expectations and validate mocks.
[self waitForExpectations:@[ expectation ] timeout:0.5];
[self verifyAllMocks];
XCTAssertEqual(self.fakeAppCheckProvider.getTokenCallCount, 1);
XCTAssertEqualObjects(self.fakeStorage.lastSetToken, expectedToken);
XCTAssertEqual(self.fakeTokenRefresher.updateWithRefreshResultCallCount, 1);
XCTAssertEqualObjects(self.fakeTokenDelegate.lastToken, expectedToken);
}
- (void)testGetToken_WhenCachedTokenExpired_Success {
@@ -229,7 +170,11 @@ static NSString *const kAppGroupID = @"app_group_id";
// 3. Wait for expectations and validate mocks.
[self waitForExpectations:@[ expectation ] timeout:0.5];
[self verifyAllMocks];
XCTAssertEqual(self.fakeAppCheckProvider.getTokenCallCount, 1);
XCTAssertEqualObjects(self.fakeStorage.lastSetToken, expectedToken);
XCTAssertEqual(self.fakeTokenRefresher.updateWithRefreshResultCallCount, 1);
XCTAssertEqualObjects(self.fakeTokenDelegate.lastToken, expectedToken);
}
- (void)testGetToken_AppCheckProviderError {
@@ -253,70 +198,66 @@ static NSString *const kAppGroupID = @"app_group_id";
// 3. Wait for expectations and validate mocks.
[self waitForExpectations:@[ expectation ] timeout:0.5];
[self verifyAllMocks];
XCTAssertEqual(self.fakeAppCheckProvider.getTokenCallCount, 1);
XCTAssertEqual(self.fakeTokenDelegate.tokenDidUpdateCallCount, 0);
XCTAssertNil(self.fakeStorage.lastSetToken);
XCTAssertEqual(self.fakeTokenRefresher.updateWithRefreshResultCallCount, 0);
}
#pragma mark - Token refresher
- (void)testTokenRefreshTriggeredAndRefreshSuccess {
// 1. Expect token to be requested from storage.
OCMExpect([self.mockStorage getToken]).andReturn([FBLPromise resolvedWith:nil]);
self.fakeStorage.getTokenPromise = [FBLPromise resolvedWith:nil];
// 2. Expect token requested from app check provider.
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:10000];
GACAppCheckToken *tokenToReturn = [[GACAppCheckToken alloc] initWithToken:@"valid"
expirationDate:expirationDate];
id completionArg = [OCMArg invokeBlockWithArgs:tokenToReturn, [NSNull null], nil];
OCMExpect([self.mockAppCheckProvider getTokenWithCompletion:completionArg]);
self.fakeAppCheckProvider.tokenToReturn = tokenToReturn;
// 3. Expect new token to be stored.
OCMExpect([self.mockStorage setToken:tokenToReturn])
.andReturn([FBLPromise resolvedWith:tokenToReturn]);
OCMExpect([self.mockTokenRefresher updateWithRefreshResult:[OCMArg any]]);
self.fakeStorage.setTokenPromise = [FBLPromise resolvedWith:tokenToReturn];
// 4. Expect token update notification to be sent.
OCMExpect([self.mockTokenDelegate tokenDidUpdate:tokenToReturn serviceName:kAppName]);
// 5. Trigger refresh and expect the result.
if (self.tokenRefreshHandler == nil) {
// 4. Trigger refresh and expect the result.
if (self.fakeTokenRefresher.tokenRefreshHandler == nil) {
XCTFail(@"`tokenRefreshHandler` must be not `nil`.");
return;
}
XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completion"];
self.tokenRefreshHandler(^(GACAppCheckTokenRefreshResult *refreshResult) {
self.fakeTokenRefresher.tokenRefreshHandler(^(GACAppCheckTokenRefreshResult *refreshResult) {
[completionExpectation fulfill];
XCTAssertEqualObjects(refreshResult.tokenExpirationDate, expirationDate);
XCTAssertEqual(refreshResult.status, GACAppCheckTokenRefreshStatusSuccess);
});
[self waitForExpectations:@[ completionExpectation ] timeout:0.5];
[self verifyAllMocks];
XCTAssertEqual(self.fakeAppCheckProvider.getTokenCallCount, 1);
XCTAssertEqualObjects(self.fakeStorage.lastSetToken, tokenToReturn);
XCTAssertEqual(self.fakeTokenRefresher.updateWithRefreshResultCallCount, 1);
XCTAssertEqual(self.fakeTokenDelegate.tokenDidUpdateCallCount, 1);
XCTAssertEqualObjects(self.fakeTokenDelegate.lastToken, tokenToReturn);
}
- (void)testTokenRefreshTriggeredAndRefreshError {
// 1. Expect token to be requested from storage.
OCMExpect([self.mockStorage getToken]).andReturn([FBLPromise resolvedWith:nil]);
self.fakeStorage.getTokenPromise = [FBLPromise resolvedWith:nil];
// 2. Expect token requested from app check provider.
NSError *providerError = [self internalError];
id completionArg = [OCMArg invokeBlockWithArgs:[NSNull null], providerError, nil];
OCMExpect([self.mockAppCheckProvider getTokenWithCompletion:completionArg]);
// 3. Don't expect token requested from app check provider.
OCMReject([self.mockAppCheckProvider getTokenWithCompletion:[OCMArg any]]);
// 4. Don't expect token update notification to be sent.
OCMReject([self.mockTokenDelegate tokenDidUpdate:OCMOCK_ANY serviceName:OCMOCK_ANY]);
self.fakeAppCheckProvider.errorToReturn = providerError;
// 5. Trigger refresh and expect the result.
if (self.tokenRefreshHandler == nil) {
if (self.fakeTokenRefresher.tokenRefreshHandler == nil) {
XCTFail(@"`tokenRefreshHandler` must be not `nil`.");
return;
}
XCTestExpectation *completionExpectation = [self expectationWithDescription:@"completion"];
self.tokenRefreshHandler(^(GACAppCheckTokenRefreshResult *refreshResult) {
self.fakeTokenRefresher.tokenRefreshHandler(^(GACAppCheckTokenRefreshResult *refreshResult) {
[completionExpectation fulfill];
XCTAssertEqual(refreshResult.status, GACAppCheckTokenRefreshStatusFailure);
XCTAssertNil(refreshResult.tokenExpirationDate);
@@ -324,23 +265,17 @@ static NSString *const kAppGroupID = @"app_group_id";
});
[self waitForExpectations:@[ completionExpectation ] timeout:0.5];
[self verifyAllMocks];
XCTAssertEqual(self.fakeAppCheckProvider.getTokenCallCount, 1);
XCTAssertEqual(self.fakeTokenDelegate.tokenDidUpdateCallCount, 0);
XCTAssertNil(self.fakeStorage.lastSetToken);
XCTAssertEqual(self.fakeTokenRefresher.updateWithRefreshResultCallCount, 0);
}
- (void)testLimitedUseTokenWithSuccess {
// 1. Don't expect token to be requested from storage.
OCMReject([self.mockStorage getToken]);
// 2. Expect token requested from app check provider.
// 1. Expect token requested from app check provider.
GACAppCheckToken *expectedToken = [self validToken];
id completionArg = [OCMArg invokeBlockWithArgs:expectedToken, [NSNull null], nil];
OCMExpect([self.mockAppCheckProvider getLimitedUseTokenWithCompletion:completionArg]);
// 3. Don't expect token requested from storage.
OCMReject([self.mockStorage setToken:expectedToken]);
// 4. Don't expect token update notification to be sent.
OCMReject([self.mockTokenDelegate tokenDidUpdate:OCMOCK_ANY serviceName:OCMOCK_ANY]);
self.fakeAppCheckProvider.limitedUseTokenToReturn = expectedToken;
// 5. Expect token request to be completed.
XCTestExpectation *getTokenExpectation = [self expectationWithDescription:@"getToken"];
@@ -351,23 +286,16 @@ static NSString *const kAppGroupID = @"app_group_id";
XCTAssertNil(result.error);
}];
[self waitForExpectations:@[ getTokenExpectation ] timeout:0.5];
[self verifyAllMocks];
XCTAssertEqual(self.fakeAppCheckProvider.getLimitedUseTokenCallCount, 1);
XCTAssertEqualObjects(self.fakeStorage.lastSetToken, nil);
XCTAssertEqual(self.fakeTokenDelegate.tokenDidUpdateCallCount, 0);
}
- (void)testLimitedUseToken_WhenTokenGenerationErrors {
// 1. Don't expect token to be requested from storage.
OCMReject([self.mockStorage getToken]);
// 2. Expect error when requesting token from app check provider.
NSError *providerError = [_GACAppCheckErrorUtil keychainErrorWithError:[self internalError]];
id completionArg = [OCMArg invokeBlockWithArgs:[NSNull null], providerError, nil];
OCMExpect([self.mockAppCheckProvider getLimitedUseTokenWithCompletion:completionArg]);
// 3. Don't expect token requested from app check provider.
OCMReject([self.mockAppCheckProvider getTokenWithCompletion:[OCMArg any]]);
// 4. Don't expect token update notification to be sent.
OCMReject([self.mockTokenDelegate tokenDidUpdate:OCMOCK_ANY serviceName:OCMOCK_ANY]);
self.fakeAppCheckProvider.limitedUseErrorToReturn = providerError;
// 5. Expect token request to be completed.
XCTestExpectation *getTokenExpectation = [self expectationWithDescription:@"getToken"];
@@ -381,7 +309,12 @@ static NSString *const kAppGroupID = @"app_group_id";
}];
[self waitForExpectations:@[ getTokenExpectation ] timeout:0.5];
[self verifyAllMocks];
XCTAssertEqual(self.fakeAppCheckProvider.getLimitedUseTokenCallCount, 1);
XCTAssertEqual(self.fakeAppCheckProvider.getTokenCallCount, 0);
XCTAssertNil(self.fakeStorage.lastSetToken);
XCTAssertEqual(self.fakeTokenDelegate.tokenDidUpdateCallCount, 0);
XCTAssertEqual(self.fakeTokenRefresher.updateWithRefreshResultCallCount, 0);
}
#pragma mark - Merging multiple get token requests
@@ -392,10 +325,6 @@ static NSString *const kAppGroupID = @"app_group_id";
[self expectTokenRequestFromAppCheckProvider];
GACAppCheckToken *expectedToken = expectedTokenAndPromise.firstObject;
FBLPromise *storeTokenPromise = expectedTokenAndPromise.lastObject;
OCMExpect([self.mockTokenRefresher updateWithRefreshResult:[OCMArg any]]);
// 2. Expect token update notification to be sent.
OCMExpect([self.mockTokenDelegate tokenDidUpdate:expectedToken serviceName:kAppName]);
// 3. Request token several times.
NSInteger getTokenCallsCount = 10;
@@ -422,7 +351,10 @@ static NSString *const kAppGroupID = @"app_group_id";
// 4. Wait for expectations and validate mocks.
[self waitForExpectations:getTokenCompletionExpectations timeout:0.5];
[self verifyAllMocks];
XCTAssertEqual(self.fakeAppCheckProvider.getTokenCallCount, 1);
XCTAssertEqual(self.fakeTokenRefresher.updateWithRefreshResultCallCount, 1);
XCTAssertEqual(self.fakeTokenDelegate.tokenDidUpdateCallCount, 1);
// 5. Check a get token call after.
[self assertGetToken_WhenCachedTokenIsValid_Success];
@@ -437,9 +369,6 @@ static NSString *const kAppGroupID = @"app_group_id";
// 1.1. Create an expected error to be reject the store token promise with later.
NSError *storageError = [NSError errorWithDomain:self.name code:0 userInfo:nil];
// 2. Don't expect token update notification to be sent.
OCMReject([self.mockTokenDelegate tokenDidUpdate:OCMOCK_ANY serviceName:OCMOCK_ANY]);
// 3. Request token several times.
NSInteger getTokenCallsCount = 10;
NSMutableArray *getTokenCompletionExpectations =
@@ -467,7 +396,12 @@ static NSString *const kAppGroupID = @"app_group_id";
// 4. Wait for expectations and validate mocks.
[self waitForExpectations:getTokenCompletionExpectations timeout:0.5];
[self verifyAllMocks];
// After the first token generation fails and caches the result, the call count will be 1
XCTAssertEqual(self.fakeAppCheckProvider.getTokenCallCount, 1);
XCTAssertEqual(self.fakeTokenDelegate.tokenDidUpdateCallCount, 0); // No updates on error
XCTAssertEqualObjects(self.fakeStorage.lastSetToken, expectedTokenAndPromise.firstObject);
XCTAssertEqual(self.fakeTokenRefresher.updateWithRefreshResultCallCount, 0);
// 5. Check a get token call after.
[self assertGetToken_WhenCachedTokenIsValid_Success];
@@ -489,15 +423,9 @@ static NSString *const kAppGroupID = @"app_group_id";
return [[GACAppCheckToken alloc] initWithToken:@"valid" expirationDate:soonExpiringTokenDate];
}
- (void)stubSetTokenRefreshHandler {
id arg = [OCMArg checkWithBlock:^BOOL(id handler) {
self.tokenRefreshHandler = handler;
return YES;
}];
OCMExpect([self.mockTokenRefresher setTokenRefreshHandler:arg]);
}
- (void)assertGetToken_WhenCachedTokenIsValid_Success {
NSInteger initialCallCount = self.fakeAppCheckProvider.getTokenCallCount;
// 1. Create expected token and configure expectations.
GACAppCheckToken *cachedToken = [self validToken];
@@ -514,25 +442,20 @@ static NSString *const kAppGroupID = @"app_group_id";
// 3. Wait for expectations and validate mocks.
[self waitForExpectations:@[ expectation ] timeout:0.5];
[self verifyAllMocks];
XCTAssertEqual(self.fakeAppCheckProvider.getTokenCallCount, initialCallCount);
}
- (XCTestExpectation *)configuredExpectations_GetTokenWhenNoCache_withExpectedToken:
(GACAppCheckToken *)expectedToken {
// 1. Expect token to be requested from storage.
OCMExpect([self.mockStorage getToken]).andReturn([FBLPromise resolvedWith:nil]);
self.fakeStorage.getTokenPromise = [FBLPromise resolvedWith:nil];
// 2. Expect token requested from app check provider.
id completionArg = [OCMArg invokeBlockWithArgs:expectedToken, [NSNull null], nil];
OCMExpect([self.mockAppCheckProvider getTokenWithCompletion:completionArg]);
self.fakeAppCheckProvider.tokenToReturn = expectedToken;
// 3. Expect new token to be stored.
OCMExpect([self.mockStorage setToken:expectedToken])
.andReturn([FBLPromise resolvedWith:expectedToken]);
OCMExpect([self.mockTokenRefresher updateWithRefreshResult:[OCMArg any]]);
// 4. Expect token update notification to be sent.
OCMExpect([self.mockTokenDelegate tokenDidUpdate:expectedToken serviceName:kAppName]);
self.fakeStorage.setTokenPromise = [FBLPromise resolvedWith:expectedToken];
// 5. Expect token request to be completed.
XCTestExpectation *getTokenExpectation = [self expectationWithDescription:@"getToken"];
@@ -543,13 +466,7 @@ static NSString *const kAppGroupID = @"app_group_id";
- (XCTestExpectation *)configuredExpectation_GetTokenWhenCacheTokenIsValid_withExpectedToken:
(GACAppCheckToken *)expectedToken {
// 1. Expect token to be requested from storage.
OCMExpect([self.mockStorage getToken]).andReturn([FBLPromise resolvedWith:expectedToken]);
// 2. Don't expect token requested from app check provider.
OCMReject([self.mockAppCheckProvider getTokenWithCompletion:[OCMArg any]]);
// 3. Don't expect token update notification to be sent.
OCMReject([self.mockTokenDelegate tokenDidUpdate:OCMOCK_ANY serviceName:OCMOCK_ANY]);
self.fakeStorage.getTokenPromise = [FBLPromise resolvedWith:expectedToken];
// 4. Expect token request to be completed.
return [self expectationWithDescription:@"getToken"];
@@ -558,20 +475,11 @@ static NSString *const kAppGroupID = @"app_group_id";
- (XCTestExpectation *)
configuredExpectations_GetTokenForcingRefreshWhenCacheIsValid_withExpectedToken:
(GACAppCheckToken *)expectedToken {
// 1. Don't expect token to be requested from storage.
OCMReject([self.mockStorage getToken]);
// 2. Expect token requested from app check provider.
id completionArg = [OCMArg invokeBlockWithArgs:expectedToken, [NSNull null], nil];
OCMExpect([self.mockAppCheckProvider getTokenWithCompletion:completionArg]);
self.fakeAppCheckProvider.tokenToReturn = expectedToken;
// 3. Expect new token to be stored.
OCMExpect([self.mockStorage setToken:expectedToken])
.andReturn([FBLPromise resolvedWith:expectedToken]);
OCMExpect([self.mockTokenRefresher updateWithRefreshResult:[OCMArg any]]);
// 4. Expect token update notification to be sent.
OCMExpect([self.mockTokenDelegate tokenDidUpdate:expectedToken serviceName:kAppName]);
self.fakeStorage.setTokenPromise = [FBLPromise resolvedWith:expectedToken];
// 5. Expect token request to be completed.
XCTestExpectation *getTokenExpectation = [self expectationWithDescription:@"getToken"];
@@ -584,19 +492,13 @@ static NSString *const kAppGroupID = @"app_group_id";
// 1. Expect token to be requested from storage.
GACAppCheckToken *cachedToken = [[GACAppCheckToken alloc] initWithToken:@"expired"
expirationDate:[NSDate date]];
OCMExpect([self.mockStorage getToken]).andReturn([FBLPromise resolvedWith:cachedToken]);
self.fakeStorage.getTokenPromise = [FBLPromise resolvedWith:cachedToken];
// 2. Expect token requested from app check provider.
id completionArg = [OCMArg invokeBlockWithArgs:expectedToken, [NSNull null], nil];
OCMExpect([self.mockAppCheckProvider getTokenWithCompletion:completionArg]);
self.fakeAppCheckProvider.tokenToReturn = expectedToken;
// 3. Expect new token to be stored.
OCMExpect([self.mockStorage setToken:expectedToken])
.andReturn([FBLPromise resolvedWith:expectedToken]);
OCMExpect([self.mockTokenRefresher updateWithRefreshResult:[OCMArg any]]);
// 4. Expect token update notification to be sent.
OCMExpect([self.mockTokenDelegate tokenDidUpdate:expectedToken serviceName:kAppName]);
self.fakeStorage.setTokenPromise = [FBLPromise resolvedWith:expectedToken];
// 5. Expect token request to be completed.
XCTestExpectation *getTokenExpectation = [self expectationWithDescription:@"getToken"];
@@ -608,17 +510,10 @@ static NSString *const kAppGroupID = @"app_group_id";
configuredExpectations_GetTokenWhenError_withError:(NSError *_Nonnull)error
andToken:(GACAppCheckToken *_Nullable)token {
// 1. Expect token to be requested from storage.
OCMExpect([self.mockStorage getToken]).andReturn([FBLPromise resolvedWith:token]);
self.fakeStorage.getTokenPromise = [FBLPromise resolvedWith:token];
// 2. Expect token requested from app check provider.
id completionArg = [OCMArg invokeBlockWithArgs:[NSNull null], error, nil];
OCMExpect([self.mockAppCheckProvider getTokenWithCompletion:completionArg]);
// 3. Don't expect token requested from app check provider.
OCMReject([self.mockAppCheckProvider getTokenWithCompletion:[OCMArg any]]);
// 4. Expect token update notification to be sent.
OCMReject([self.mockTokenDelegate tokenDidUpdate:OCMOCK_ANY serviceName:OCMOCK_ANY]);
self.fakeAppCheckProvider.errorToReturn = error;
// 5. Expect token request to be completed.
XCTestExpectation *getTokenExpectation = [self expectationWithDescription:@"getToken"];
@@ -628,28 +523,19 @@ static NSString *const kAppGroupID = @"app_group_id";
- (NSArray *)expectTokenRequestFromAppCheckProvider {
// 1. Expect token to be requested from storage.
OCMExpect([self.mockStorage getToken]).andReturn([FBLPromise resolvedWith:nil]);
self.fakeStorage.getTokenPromise = [FBLPromise resolvedWith:nil];
// 2. Expect token requested from app check provider.
GACAppCheckToken *expectedToken = [self validToken];
id completionArg = [OCMArg invokeBlockWithArgs:expectedToken, [NSNull null], nil];
OCMExpect([self.mockAppCheckProvider getTokenWithCompletion:completionArg]);
self.fakeAppCheckProvider.tokenToReturn = expectedToken;
// 3. Expect new token to be stored.
// 3.1. Create a pending promise to resolve later.
FBLPromise<GACAppCheckToken *> *storeTokenPromise = [FBLPromise pendingPromise];
// 3.2. Stub storage set token method.
OCMExpect([self.mockStorage setToken:expectedToken]).andReturn(storeTokenPromise);
self.fakeStorage.setTokenPromise = storeTokenPromise;
return @[ expectedToken, storeTokenPromise ];
}
- (void)verifyAllMocks {
OCMVerifyAll(self.mockAppCheckProvider);
OCMVerifyAll(self.mockStorage);
OCMVerifyAll(self.mockSettings);
OCMVerifyAll(self.mockTokenDelegate);
OCMVerifyAll(self.mockTokenRefresher);
}
@end
@@ -0,0 +1,35 @@
/*
* Copyright 2026 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 <Foundation/Foundation.h>
#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckProvider.h"
#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h"
NS_ASSUME_NONNULL_BEGIN
@interface GACAppCheckProviderFake : NSObject <GACAppCheckProvider>
@property(atomic, nullable) GACAppCheckToken *tokenToReturn;
@property(atomic, nullable) NSError *errorToReturn;
@property(nonatomic) NSInteger getTokenCallCount;
@property(atomic, nullable) GACAppCheckToken *limitedUseTokenToReturn;
@property(atomic, nullable) NSError *limitedUseErrorToReturn;
@property(nonatomic) NSInteger getLimitedUseTokenCallCount;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,76 @@
/*
* Copyright 2026 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 "AppCheckCore/Tests/Unit/Utils/GACAppCheckProviderFake.h"
@implementation GACAppCheckProviderFake
@synthesize getTokenCallCount = _getTokenCallCount;
@synthesize getLimitedUseTokenCallCount = _getLimitedUseTokenCallCount;
- (NSInteger)getTokenCallCount {
@synchronized(self) {
return _getTokenCallCount;
}
}
- (void)setGetTokenCallCount:(NSInteger)getTokenCallCount {
@synchronized(self) {
_getTokenCallCount = getTokenCallCount;
}
}
- (NSInteger)getLimitedUseTokenCallCount {
@synchronized(self) {
return _getLimitedUseTokenCallCount;
}
}
- (void)setGetLimitedUseTokenCallCount:(NSInteger)getLimitedUseTokenCallCount {
@synchronized(self) {
_getLimitedUseTokenCallCount = getLimitedUseTokenCallCount;
}
}
- (void)getTokenWithCompletion:(void (^)(GACAppCheckToken *_Nullable token,
NSError *_Nullable error))handler {
GACAppCheckToken *token;
NSError *error;
@synchronized(self) {
_getTokenCallCount++;
token = self.tokenToReturn;
error = self.errorToReturn;
}
if (handler) {
handler(token, error);
}
}
- (void)getLimitedUseTokenWithCompletion:(void (^)(GACAppCheckToken *_Nullable token,
NSError *_Nullable error))handler {
GACAppCheckToken *token;
NSError *error;
@synchronized(self) {
_getLimitedUseTokenCallCount++;
token = self.limitedUseTokenToReturn;
error = self.limitedUseErrorToReturn;
}
if (handler) {
handler(token, error);
}
}
@end
@@ -0,0 +1,28 @@
/*
* Copyright 2026 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 <Foundation/Foundation.h>
#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckSettings.h"
NS_ASSUME_NONNULL_BEGIN
@interface GACAppCheckSettingsFake : NSObject <GACAppCheckSettingsProtocol>
@property(nonatomic) BOOL isTokenAutoRefreshEnabled;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,35 @@
/*
* Copyright 2026 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 "AppCheckCore/Tests/Unit/Utils/GACAppCheckSettingsFake.h"
@implementation GACAppCheckSettingsFake
@synthesize isTokenAutoRefreshEnabled = _isTokenAutoRefreshEnabled;
- (BOOL)isTokenAutoRefreshEnabled {
@synchronized(self) {
return _isTokenAutoRefreshEnabled;
}
}
- (void)setIsTokenAutoRefreshEnabled:(BOOL)isTokenAutoRefreshEnabled {
@synchronized(self) {
_isTokenAutoRefreshEnabled = isTokenAutoRefreshEnabled;
}
}
@end
@@ -0,0 +1,31 @@
/*
* Copyright 2026 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 <Foundation/Foundation.h>
#import "AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.h"
NS_ASSUME_NONNULL_BEGIN
@interface GACAppCheckStorageFake : NSObject <GACAppCheckStorageProtocol>
@property(atomic, nullable) FBLPromise<GACAppCheckToken *> *getTokenPromise;
@property(atomic, nullable) FBLPromise<GACAppCheckToken *> *setTokenPromise;
@property(atomic, nullable) GACAppCheckToken *lastSetToken;
@property(atomic, nullable) FBLPromise<NSNull *> *removeTokenPromise;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,37 @@
/*
* Copyright 2026 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 "AppCheckCore/Tests/Unit/Utils/GACAppCheckStorageFake.h"
#import "FBLPromise+Testing.h"
@implementation GACAppCheckStorageFake
- (FBLPromise<GACAppCheckToken *> *)getToken {
return self.getTokenPromise ?: [FBLPromise resolvedWith:nil];
}
- (FBLPromise<GACAppCheckToken *> *)setToken:(GACAppCheckToken *)token {
@synchronized(self) {
self.lastSetToken = token;
}
return self.setTokenPromise ?: [FBLPromise resolvedWith:token];
}
- (FBLPromise<NSNull *> *)removeToken {
return self.removeTokenPromise ?: [FBLPromise resolvedWith:[NSNull null]];
}
@end
@@ -0,0 +1,30 @@
/*
* Copyright 2026 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 <Foundation/Foundation.h>
#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckTokenDelegate.h"
NS_ASSUME_NONNULL_BEGIN
@interface GACAppCheckTokenDelegateFake : NSObject <GACAppCheckTokenDelegate>
@property(atomic) NSInteger tokenDidUpdateCallCount;
@property(atomic, nullable) GACAppCheckToken *lastToken;
@property(atomic, copy, nullable) NSString *lastServiceName;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,29 @@
/*
* Copyright 2026 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 "AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenDelegateFake.h"
@implementation GACAppCheckTokenDelegateFake
- (void)tokenDidUpdate:(GACAppCheckToken *)token serviceName:(NSString *)serviceName {
@synchronized(self) {
self.tokenDidUpdateCallCount++;
self.lastToken = token;
self.lastServiceName = serviceName;
}
}
@end
@@ -0,0 +1,30 @@
/*
* Copyright 2026 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 <Foundation/Foundation.h>
#import "AppCheckCore/Sources/Core/TokenRefresh/GACAppCheckTokenRefresher.h"
NS_ASSUME_NONNULL_BEGIN
@interface GACAppCheckTokenRefresherFake : NSObject <GACAppCheckTokenRefresherProtocol>
@property(nonatomic, copy, nullable) GACAppCheckTokenRefreshBlock tokenRefreshHandler;
@property(atomic) NSInteger updateWithRefreshResultCallCount;
@property(atomic, nullable) GACAppCheckTokenRefreshResult *lastRefreshResult;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,42 @@
/*
* Copyright 2026 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 "AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenRefresherFake.h"
@implementation GACAppCheckTokenRefresherFake
@synthesize tokenRefreshHandler = _tokenRefreshHandler;
- (GACAppCheckTokenRefreshBlock)tokenRefreshHandler {
@synchronized(self) {
return _tokenRefreshHandler;
}
}
- (void)setTokenRefreshHandler:(GACAppCheckTokenRefreshBlock)tokenRefreshHandler {
@synchronized(self) {
_tokenRefreshHandler = [tokenRefreshHandler copy];
}
}
- (void)updateWithRefreshResult:(GACAppCheckTokenRefreshResult *)refreshResult {
@synchronized(self) {
self.updateWithRefreshResultCallCount++;
self.lastRefreshResult = refreshResult;
}
}
@end
+3 -3
View File
@@ -28,12 +28,12 @@ typedef void (^GACFakeTimerCreateHandler)(NSDate *fireDate);
/// `createHandler` is called each time the timer provider returned by `fakeTimerProvider` is asked
/// to create a timer.
@property(nonatomic, copy, nullable) GACFakeTimerCreateHandler createHandler;
@property(atomic, copy, nullable) GACFakeTimerCreateHandler createHandler;
@property(nonatomic, copy, nullable) dispatch_block_t invalidationHandler;
@property(atomic, copy, nullable) dispatch_block_t invalidationHandler;
/// The timer handler passed in the timer provider returned by `fakeTimerProvider` method.
@property(nonatomic, copy, nullable) dispatch_block_t handler;
@property(atomic, copy, nullable) dispatch_block_t handler;
@end
+16 -6
View File
@@ -19,20 +19,30 @@
@implementation GACFakeTimer
- (GACTimerProvider)fakeTimerProvider {
__weak __typeof__(self) weakSelf = self;
return ^id<GACAppCheckTimerProtocol> _Nullable(NSDate *fireDate, dispatch_queue_t queue,
dispatch_block_t handler) {
self.handler = handler;
if (self.createHandler) {
self.createHandler(fireDate);
__typeof__(self) strongSelf = weakSelf;
if (!strongSelf) {
return nil;
}
return self;
@synchronized(strongSelf) {
strongSelf.handler = handler;
void (^createHandler)(NSDate *) = strongSelf.createHandler;
if (createHandler) {
createHandler(fireDate);
}
}
return strongSelf;
};
}
- (void)invalidate {
if (self.invalidationHandler) {
self.invalidationHandler();
void (^invalidationHandler)(void) = self.invalidationHandler;
if (invalidationHandler) {
invalidationHandler();
}
}
@@ -27,6 +27,11 @@
}
}
if (fileURL == nil) {
NSLog(@"Fixture named %@ not found", fileName);
return nil;
}
NSError *error;
NSData *data = [NSData dataWithContentsOfURL:fileURL options:0 error:&error];
+8
View File
@@ -0,0 +1,8 @@
# Review Guidelines
## Concurrency & Thread Safety
When reviewing Objective-C code, particularly test fakes, you MUST strictly enforce the following thread safety rules for concurrent test execution:
1. **State Mutation Synchronization**: All state mutations (e.g., incrementing counters like `counter++`, setting properties, or adding to arrays) MUST be wrapped in a `@synchronized(self)` block to prevent data races and TSAN (Thread Sanitizer) failures.
2. **Proper Use of `atomic` vs `nonatomic`**: The `atomic` property modifier does NOT make read-modify-write operations (like `++`) thread-safe. Properties that require compound operations should be declared `nonatomic` to avoid misleading expectations, and their getters/setters must be manually synchronized in the implementation file (or accessed directly via ivar within a synchronized block).
+44
View File
@@ -210,3 +210,47 @@ Perform self-reflection after completing the task. You MUST update this file
(`agents.md`) with any new learnings, context, or troubleshooting steps that
were needed and will be needed again to refine the process for future agents.
Alternatively, create or update a Knowledge Item.
---
## 🧪 OCMock & Objective-C Testing Migration Guidelines
When migrating Objective-C tests away from `OCMock` (to support future Swift
interoperability), you MUST follow these patterns:
- **Use Protocol-Based Fakes**: Replace `OCMock` with manually implemented
`Fake` classes that conform to the required protocols (e.g.,
`id<GACAppCheckProvider>`).
- **Strict Black-Box Testing (No Test Categories)**: Do NOT use
`@interface TargetClass (Tests)` categories to expose internal properties or
standard `alloc/init` overrides just to verify "wiring". Inject Fakes via
designated initializers and verify behavior, not state.
- **Thread-Safety & Locks**: Avoid redundant recursive locks
(`@synchronized(self)`). If a property uses `@synchronized(self)` in its
getter/setter, use direct instance variable access (e.g. `_ivar`) when
reading/writing it internally from within another `@synchronized(self)` block
to reduce overhead.
- **Concurrent Independent PRs**: When migrating multiple test suites, separate
them into independent branches (e.g. Core AppCheck vs AppAttest) and open
concurrent PRs rather than a single monolithic PR, provided they don't share
Fake implementations.
---
## 🤖 AI Subagent Personas
### Objective Code Verifier
Your role is to act as a strict execution engine. When invoked, you must
strictly run builds and test suites using this repository's specific test
commands (as defined in the Verification section above). You must report binary
pass/fail results. Do not attempt to fix or diagnose the failures yourself;
simply output the error logs and failure details back to the caller.
### Rigorous Code Reviewer
Your role is to perform subjective, rigorous code reviews on proposed changes.
You must focus heavily on concurrency, memory management, and strict adherence
to the project's guidelines. You MUST read and enforce the rules defined in
`REVIEW_GUIDELINES.md` located at the root of this repository. Flag any missing
synchronization or thread-safety violations immediately.