diff --git a/AppCheckCore/Tests/Unit/Core/GACAppCheckTests.m b/AppCheckCore/Tests/Unit/Core/GACAppCheckTests.m index 7710b05..e6a9297 100644 --- a/AppCheckCore/Tests/Unit/Core/GACAppCheckTests.m +++ b/AppCheckCore/Tests/Unit/Core/GACAppCheckTests.m @@ -16,7 +16,11 @@ #import -#import +#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 *mockStorage; -@property(nonatomic) OCMockObject *mockAppCheckProvider; -@property(nonatomic) OCMockObject *mockTokenRefresher; -@property(nonatomic) OCMockObject *mockSettings; -@property(nonatomic) OCMockObject *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 *mockProvider = - OCMStrictProtocolMock(@protocol(GACAppCheckProvider)); - - // 4. Stub GACAppCheckSettingsProtocol. - OCMockObject *mockSettings = - OCMStrictProtocolMock(@protocol(GACAppCheckSettingsProtocol)); - - // 5. Stub GACAppCheckTokenDelegate. - OCMockObject *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 *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 diff --git a/AppCheckCore/Tests/Unit/Utils/GACAppCheckProviderFake.h b/AppCheckCore/Tests/Unit/Utils/GACAppCheckProviderFake.h new file mode 100644 index 0000000..c10ba96 --- /dev/null +++ b/AppCheckCore/Tests/Unit/Utils/GACAppCheckProviderFake.h @@ -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 +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckProvider.h" +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckToken.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface GACAppCheckProviderFake : NSObject + +@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 diff --git a/AppCheckCore/Tests/Unit/Utils/GACAppCheckProviderFake.m b/AppCheckCore/Tests/Unit/Utils/GACAppCheckProviderFake.m new file mode 100644 index 0000000..739c6cd --- /dev/null +++ b/AppCheckCore/Tests/Unit/Utils/GACAppCheckProviderFake.m @@ -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 diff --git a/AppCheckCore/Tests/Unit/Utils/GACAppCheckSettingsFake.h b/AppCheckCore/Tests/Unit/Utils/GACAppCheckSettingsFake.h new file mode 100644 index 0000000..6b46197 --- /dev/null +++ b/AppCheckCore/Tests/Unit/Utils/GACAppCheckSettingsFake.h @@ -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 +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckSettings.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface GACAppCheckSettingsFake : NSObject + +@property(nonatomic) BOOL isTokenAutoRefreshEnabled; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AppCheckCore/Tests/Unit/Utils/GACAppCheckSettingsFake.m b/AppCheckCore/Tests/Unit/Utils/GACAppCheckSettingsFake.m new file mode 100644 index 0000000..1c43649 --- /dev/null +++ b/AppCheckCore/Tests/Unit/Utils/GACAppCheckSettingsFake.m @@ -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 diff --git a/AppCheckCore/Tests/Unit/Utils/GACAppCheckStorageFake.h b/AppCheckCore/Tests/Unit/Utils/GACAppCheckStorageFake.h new file mode 100644 index 0000000..9ad6c7e --- /dev/null +++ b/AppCheckCore/Tests/Unit/Utils/GACAppCheckStorageFake.h @@ -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 +#import "AppCheckCore/Sources/Core/Storage/GACAppCheckStorage.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface GACAppCheckStorageFake : NSObject + +@property(atomic, nullable) FBLPromise *getTokenPromise; +@property(atomic, nullable) FBLPromise *setTokenPromise; +@property(atomic, nullable) GACAppCheckToken *lastSetToken; +@property(atomic, nullable) FBLPromise *removeTokenPromise; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AppCheckCore/Tests/Unit/Utils/GACAppCheckStorageFake.m b/AppCheckCore/Tests/Unit/Utils/GACAppCheckStorageFake.m new file mode 100644 index 0000000..ded19c1 --- /dev/null +++ b/AppCheckCore/Tests/Unit/Utils/GACAppCheckStorageFake.m @@ -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 *)getToken { + return self.getTokenPromise ?: [FBLPromise resolvedWith:nil]; +} + +- (FBLPromise *)setToken:(GACAppCheckToken *)token { + @synchronized(self) { + self.lastSetToken = token; + } + return self.setTokenPromise ?: [FBLPromise resolvedWith:token]; +} + +- (FBLPromise *)removeToken { + return self.removeTokenPromise ?: [FBLPromise resolvedWith:[NSNull null]]; +} + +@end diff --git a/AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenDelegateFake.h b/AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenDelegateFake.h new file mode 100644 index 0000000..10a48ed --- /dev/null +++ b/AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenDelegateFake.h @@ -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 +#import "AppCheckCore/Sources/Public/AppCheckCore/GACAppCheckTokenDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface GACAppCheckTokenDelegateFake : NSObject + +@property(atomic) NSInteger tokenDidUpdateCallCount; +@property(atomic, nullable) GACAppCheckToken *lastToken; +@property(atomic, copy, nullable) NSString *lastServiceName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenDelegateFake.m b/AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenDelegateFake.m new file mode 100644 index 0000000..d0e8d44 --- /dev/null +++ b/AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenDelegateFake.m @@ -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 diff --git a/AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenRefresherFake.h b/AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenRefresherFake.h new file mode 100644 index 0000000..e1b08e3 --- /dev/null +++ b/AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenRefresherFake.h @@ -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 +#import "AppCheckCore/Sources/Core/TokenRefresh/GACAppCheckTokenRefresher.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface GACAppCheckTokenRefresherFake : NSObject + +@property(nonatomic, copy, nullable) GACAppCheckTokenRefreshBlock tokenRefreshHandler; +@property(atomic) NSInteger updateWithRefreshResultCallCount; +@property(atomic, nullable) GACAppCheckTokenRefreshResult *lastRefreshResult; + +@end + +NS_ASSUME_NONNULL_END diff --git a/AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenRefresherFake.m b/AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenRefresherFake.m new file mode 100644 index 0000000..529fd41 --- /dev/null +++ b/AppCheckCore/Tests/Unit/Utils/GACAppCheckTokenRefresherFake.m @@ -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 diff --git a/AppCheckCore/Tests/Unit/Utils/GACFakeTimer.h b/AppCheckCore/Tests/Unit/Utils/GACFakeTimer.h index 0b2a886..3e247e9 100644 --- a/AppCheckCore/Tests/Unit/Utils/GACFakeTimer.h +++ b/AppCheckCore/Tests/Unit/Utils/GACFakeTimer.h @@ -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 diff --git a/AppCheckCore/Tests/Unit/Utils/GACFakeTimer.m b/AppCheckCore/Tests/Unit/Utils/GACFakeTimer.m index ae0884d..5386838 100644 --- a/AppCheckCore/Tests/Unit/Utils/GACFakeTimer.m +++ b/AppCheckCore/Tests/Unit/Utils/GACFakeTimer.m @@ -19,20 +19,30 @@ @implementation GACFakeTimer - (GACTimerProvider)fakeTimerProvider { + __weak __typeof__(self) weakSelf = self; return ^id _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(); } } diff --git a/AppCheckCore/Tests/Unit/Utils/GACFixtureLoader.m b/AppCheckCore/Tests/Unit/Utils/GACFixtureLoader.m index ae60347..4f1d37c 100644 --- a/AppCheckCore/Tests/Unit/Utils/GACFixtureLoader.m +++ b/AppCheckCore/Tests/Unit/Utils/GACFixtureLoader.m @@ -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]; diff --git a/REVIEW_GUIDELINES.md b/REVIEW_GUIDELINES.md new file mode 100644 index 0000000..04451fa --- /dev/null +++ b/REVIEW_GUIDELINES.md @@ -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). diff --git a/agents.md b/agents.md index 9cce38e..476ec2c 100644 --- a/agents.md +++ b/agents.md @@ -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`). +- **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. + + + +