From 6518b3a42a5ee9352ff112aaaa9912b7616c6f86 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 17 May 2024 16:11:19 -0400 Subject: [PATCH 1/2] [local_auth] Remove use of OCMock Adjusts the iOS implementation of local_auth to use a protocol wrapping the relevant parts of `LAContext` instead of using `LAContext` directly, allowing the tests to use a stub implementation instead of OCMock. The goal is to unblock converting the unit tests to Swift, but even by itself this actually makes the tests much shorter and easier to understand. Part of https://github.com/flutter/flutter/issues/119104 --- .../local_auth/local_auth_darwin/CHANGELOG.md | 4 + .../darwin/Tests/FLALocalAuthPluginTests.m | 359 +++++++----------- .../local_auth_darwin/FLALocalAuthPlugin.m | 13 +- .../FLALocalAuthPlugin_Test.h | 22 +- .../local_auth_darwin/example/ios/Podfile | 2 - .../local_auth/local_auth_darwin/pubspec.yaml | 2 +- 6 files changed, 171 insertions(+), 231 deletions(-) diff --git a/packages/local_auth/local_auth_darwin/CHANGELOG.md b/packages/local_auth/local_auth_darwin/CHANGELOG.md index f6df9023d3f..28d789ca0f6 100644 --- a/packages/local_auth/local_auth_darwin/CHANGELOG.md +++ b/packages/local_auth/local_auth_darwin/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.3.1 + +* Adjusts implementation for improved testability, and removes use of OCMock. + ## 1.3.0 * Adds Swift Package Manager compatibility. diff --git a/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.m b/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.m index 786316835d7..fd0ff531e59 100644 --- a/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.m +++ b/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.m @@ -6,8 +6,6 @@ @import XCTest; @import local_auth_darwin; -#import - // Set a long timeout to avoid flake due to slow CI. static const NSTimeInterval kTimeout = 30.0; @@ -15,13 +13,13 @@ * A context factory that returns preset contexts. */ @interface StubAuthContextFactory : NSObject -@property(copy, nonatomic) NSMutableArray *contexts; -- (instancetype)initWithContexts:(NSArray *)contexts; +@property(copy, nonatomic) NSMutableArray> *contexts; +- (instancetype)initWithContexts:(NSArray> *)contexts; @end @implementation StubAuthContextFactory -- (instancetype)initWithContexts:(NSArray *)contexts { +- (instancetype)initWithContexts:(NSArray> *)contexts { self = [super init]; if (self) { _contexts = [contexts mutableCopy]; @@ -29,15 +27,63 @@ - (instancetype)initWithContexts:(NSArray *)contexts { return self; } -- (LAContext *)createAuthContext { +- (id)createAuthContext { NSAssert(self.contexts.count > 0, @"Insufficient test contexts provided"); - LAContext *context = [self.contexts firstObject]; + id context = [self.contexts firstObject]; [self.contexts removeObjectAtIndex:0]; return context; } @end +@interface StubAuthContext : NSObject +/// Whether calls to this stub are expected to be for biometric authentication. +/// +/// While this object could be set up to return different values for different policies, in +/// practice only one policy is needed by any given test, so this just allows asserting that the +/// code is calling with the intended policy. +@property(nonatomic) BOOL expectBiometrics; +/// The value to return from canEvaluatePolicy. +@property(nonatomic) BOOL canEvaluateResponse; +/// The error to return from canEvaluatePolicy. +@property(nonatomic) NSError *canEvaluateError; +/// The value to return from evaluatePolicy:error:. +@property(nonatomic) BOOL evaluateResponse; +/// The error to return from evaluatePolicy:error:. +@property(nonatomic) NSError *evaluateError; + +// Overridden as read-write to allow stubbing. +@property(nonatomic, readwrite) LABiometryType biometryType; +@end + +@implementation StubAuthContext +@synthesize localizedFallbackTitle; + +- (BOOL)canEvaluatePolicy:(LAPolicy)policy + error:(NSError *__autoreleasing _Nullable *_Nullable)error { + XCTAssertEqual(policy, self.expectBiometrics ? LAPolicyDeviceOwnerAuthenticationWithBiometrics + : LAPolicyDeviceOwnerAuthentication); + if (error) { + *error = self.canEvaluateError; + } + return self.canEvaluateResponse; +} + +- (void)evaluatePolicy:(LAPolicy)policy + localizedReason:(nonnull NSString *)localizedReason + reply:(nonnull void (^)(BOOL, NSError *_Nullable))reply { + XCTAssertEqual(policy, self.expectBiometrics ? LAPolicyDeviceOwnerAuthenticationWithBiometrics + : LAPolicyDeviceOwnerAuthentication); + // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not + // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on + // a background thread. + dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ + reply(self.evaluateResponse, self.evaluateError); + }); +} + +@end + #pragma mark - @interface FLALocalAuthPluginTests : XCTestCase @@ -50,27 +96,15 @@ - (void)setUp { } - (void)testSuccessfullAuthWithBiometrics { - id mockAuthContext = OCMClassMock([LAContext class]); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; + initWithContexts:@[ stubAuthContext ]]]; - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; FLADAuthStrings *strings = [self createAuthStrings]; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not - // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on - // a background thread. - void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { - void (^reply)(BOOL, NSError *); - [invocation getArgument:&reply atIndex:4]; - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(YES, nil); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); + stubAuthContext.expectBiometrics = YES; + stubAuthContext.canEvaluateResponse = YES; + stubAuthContext.evaluateResponse = YES; XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:YES @@ -88,27 +122,14 @@ - (void)testSuccessfullAuthWithBiometrics { } - (void)testSuccessfullAuthWithoutBiometrics { - id mockAuthContext = OCMClassMock([LAContext class]); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; + initWithContexts:@[ stubAuthContext ]]]; - const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; FLADAuthStrings *strings = [self createAuthStrings]; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not - // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on - // a background thread. - void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { - void (^reply)(BOOL, NSError *); - [invocation getArgument:&reply atIndex:4]; - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(YES, nil); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); + stubAuthContext.canEvaluateResponse = YES; + stubAuthContext.evaluateResponse = YES; XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:NO @@ -126,27 +147,17 @@ - (void)testSuccessfullAuthWithoutBiometrics { } - (void)testFailedAuthWithBiometrics { - id mockAuthContext = OCMClassMock([LAContext class]); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; + initWithContexts:@[ stubAuthContext ]]]; - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; FLADAuthStrings *strings = [self createAuthStrings]; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not - // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on - // a background thread. - void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { - void (^reply)(BOOL, NSError *); - [invocation getArgument:&reply atIndex:4]; - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); + stubAuthContext.expectBiometrics = YES; + stubAuthContext.canEvaluateResponse = YES; + stubAuthContext.evaluateError = [NSError errorWithDomain:@"error" + code:LAErrorAuthenticationFailed + userInfo:nil]; XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:YES @@ -168,27 +179,14 @@ - (void)testFailedAuthWithBiometrics { } - (void)testFailedWithUnknownErrorCode { - id mockAuthContext = OCMClassMock([LAContext class]); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; + initWithContexts:@[ stubAuthContext ]]]; - const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; FLADAuthStrings *strings = [self createAuthStrings]; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not - // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on - // a background thread. - void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { - void (^reply)(BOOL, NSError *); - [invocation getArgument:&reply atIndex:4]; - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(NO, [NSError errorWithDomain:@"error" code:99 userInfo:nil]); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); + stubAuthContext.canEvaluateResponse = YES; + stubAuthContext.evaluateError = [NSError errorWithDomain:@"error" code:99 userInfo:nil]; XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:NO @@ -206,27 +204,16 @@ - (void)testFailedWithUnknownErrorCode { } - (void)testSystemCancelledWithoutStickyAuth { - id mockAuthContext = OCMClassMock([LAContext class]); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; + initWithContexts:@[ stubAuthContext ]]]; - const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; FLADAuthStrings *strings = [self createAuthStrings]; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not - // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on - // a background thread. - void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { - void (^reply)(BOOL, NSError *); - [invocation getArgument:&reply atIndex:4]; - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(NO, [NSError errorWithDomain:@"error" code:LAErrorSystemCancel userInfo:nil]); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); + stubAuthContext.canEvaluateResponse = YES; + stubAuthContext.evaluateError = [NSError errorWithDomain:@"error" + code:LAErrorSystemCancel + userInfo:nil]; XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:NO @@ -244,27 +231,16 @@ - (void)testSystemCancelledWithoutStickyAuth { } - (void)testFailedAuthWithoutBiometrics { - id mockAuthContext = OCMClassMock([LAContext class]); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; + initWithContexts:@[ stubAuthContext ]]]; - const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; FLADAuthStrings *strings = [self createAuthStrings]; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not - // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on - // a background thread. - void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { - void (^reply)(BOOL, NSError *); - [invocation getArgument:&reply atIndex:4]; - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(NO, [NSError errorWithDomain:@"error" code:LAErrorAuthenticationFailed userInfo:nil]); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); + stubAuthContext.canEvaluateResponse = YES; + stubAuthContext.evaluateError = [NSError errorWithDomain:@"error" + code:LAErrorAuthenticationFailed + userInfo:nil]; XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:NO @@ -286,28 +262,15 @@ - (void)testFailedAuthWithoutBiometrics { } - (void)testLocalizedFallbackTitle { - id mockAuthContext = OCMClassMock([LAContext class]); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; + initWithContexts:@[ stubAuthContext ]]]; - const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; FLADAuthStrings *strings = [self createAuthStrings]; strings.localizedFallbackTitle = @"a title"; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not - // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on - // a background thread. - void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { - void (^reply)(BOOL, NSError *); - [invocation getArgument:&reply atIndex:4]; - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(YES, nil); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); + stubAuthContext.canEvaluateResponse = YES; + stubAuthContext.evaluateResponse = YES; XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:NO @@ -316,36 +279,23 @@ - (void)testLocalizedFallbackTitle { strings:strings completion:^(FLADAuthResultDetails *_Nullable resultDetails, FlutterError *_Nullable error) { - OCMVerify([mockAuthContext - setLocalizedFallbackTitle:strings.localizedFallbackTitle]); + XCTAssertEqual(stubAuthContext.localizedFallbackTitle, + strings.localizedFallbackTitle); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testSkippedLocalizedFallbackTitle { - id mockAuthContext = OCMClassMock([LAContext class]); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; + initWithContexts:@[ stubAuthContext ]]]; - const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; FLADAuthStrings *strings = [self createAuthStrings]; strings.localizedFallbackTitle = nil; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - - // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not - // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on - // a background thread. - void (^backgroundThreadReplyCaller)(NSInvocation *) = ^(NSInvocation *invocation) { - void (^reply)(BOOL, NSError *); - [invocation getArgument:&reply atIndex:4]; - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(YES, nil); - }); - }; - OCMStub([mockAuthContext evaluatePolicy:policy localizedReason:strings.reason reply:[OCMArg any]]) - .andDo(backgroundThreadReplyCaller); + stubAuthContext.canEvaluateResponse = YES; + stubAuthContext.evaluateResponse = YES; XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:NO @@ -354,20 +304,20 @@ - (void)testSkippedLocalizedFallbackTitle { strings:strings completion:^(FLADAuthResultDetails *_Nullable resultDetails, FlutterError *_Nullable error) { - OCMVerify([mockAuthContext setLocalizedFallbackTitle:nil]); + XCTAssertNil(stubAuthContext.localizedFallbackTitle); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:kTimeout handler:nil]; } - (void)testDeviceSupportsBiometrics_withEnrolledHardware { - id mockAuthContext = OCMClassMock([LAContext class]); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; + initWithContexts:@[ stubAuthContext ]]]; - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); + stubAuthContext.expectBiometrics = YES; + stubAuthContext.canEvaluateResponse = YES; FlutterError *error; NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; @@ -376,25 +326,16 @@ - (void)testDeviceSupportsBiometrics_withEnrolledHardware { } - (void)testDeviceSupportsBiometrics_withNonEnrolledHardware { - id mockAuthContext = OCMClassMock([LAContext class]); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { - // Write error - NSError *__autoreleasing *authError; - [invocation getArgument:&authError atIndex:3]; - *authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil]; - // Write return value - BOOL returnValue = NO; - NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; - [invocation setReturnValue:&nsReturnValue]; - }; - OCMStub([mockAuthContext canEvaluatePolicy:policy - error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) - .andDo(canEvaluatePolicyHandler); + initWithContexts:@[ stubAuthContext ]]]; + + stubAuthContext.expectBiometrics = YES; + stubAuthContext.canEvaluateResponse = NO; + stubAuthContext.canEvaluateError = [NSError errorWithDomain:@"error" + code:LAErrorBiometryNotEnrolled + userInfo:nil]; FlutterError *error; NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; @@ -403,25 +344,14 @@ - (void)testDeviceSupportsBiometrics_withNonEnrolledHardware { } - (void)testDeviceSupportsBiometrics_withNoBiometricHardware { - id mockAuthContext = OCMClassMock([LAContext class]); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { - // Write error - NSError *__autoreleasing *authError; - [invocation getArgument:&authError atIndex:3]; - *authError = [NSError errorWithDomain:@"error" code:0 userInfo:nil]; - // Write return value - BOOL returnValue = NO; - NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; - [invocation setReturnValue:&nsReturnValue]; - }; - OCMStub([mockAuthContext canEvaluatePolicy:policy - error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) - .andDo(canEvaluatePolicyHandler); + initWithContexts:@[ stubAuthContext ]]]; + + stubAuthContext.expectBiometrics = YES; + stubAuthContext.canEvaluateResponse = NO; + stubAuthContext.canEvaluateError = [NSError errorWithDomain:@"error" code:0 userInfo:nil]; FlutterError *error; NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; @@ -430,14 +360,14 @@ - (void)testDeviceSupportsBiometrics_withNoBiometricHardware { } - (void)testGetEnrolledBiometricsWithFaceID { - id mockAuthContext = OCMClassMock([LAContext class]); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; + initWithContexts:@[ stubAuthContext ]]]; - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeFaceID); + stubAuthContext.expectBiometrics = YES; + stubAuthContext.canEvaluateResponse = YES; + stubAuthContext.biometryType = LABiometryTypeFaceID; FlutterError *error; NSArray *result = [plugin getEnrolledBiometricsWithError:&error]; @@ -447,14 +377,14 @@ - (void)testGetEnrolledBiometricsWithFaceID { } - (void)testGetEnrolledBiometricsWithTouchID { - id mockAuthContext = OCMClassMock([LAContext class]); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; + initWithContexts:@[ stubAuthContext ]]]; - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); - OCMStub([mockAuthContext biometryType]).andReturn(LABiometryTypeTouchID); + stubAuthContext.expectBiometrics = YES; + stubAuthContext.canEvaluateResponse = YES; + stubAuthContext.biometryType = LABiometryTypeTouchID; FlutterError *error; NSArray *result = [plugin getEnrolledBiometricsWithError:&error]; @@ -464,25 +394,16 @@ - (void)testGetEnrolledBiometricsWithTouchID { } - (void)testGetEnrolledBiometricsWithoutEnrolledHardware { - id mockAuthContext = OCMClassMock([LAContext class]); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; - - const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; - void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { - // Write error - NSError *__autoreleasing *authError; - [invocation getArgument:&authError atIndex:3]; - *authError = [NSError errorWithDomain:@"error" code:LAErrorBiometryNotEnrolled userInfo:nil]; - // Write return value - BOOL returnValue = NO; - NSValue *nsReturnValue = [NSValue valueWithBytes:&returnValue objCType:@encode(BOOL)]; - [invocation setReturnValue:&nsReturnValue]; - }; - OCMStub([mockAuthContext canEvaluatePolicy:policy - error:(NSError * __autoreleasing *)[OCMArg anyPointer]]) - .andDo(canEvaluatePolicyHandler); + initWithContexts:@[ stubAuthContext ]]]; + + stubAuthContext.expectBiometrics = YES; + stubAuthContext.canEvaluateResponse = NO; + stubAuthContext.canEvaluateError = [NSError errorWithDomain:@"error" + code:LAErrorBiometryNotEnrolled + userInfo:nil]; FlutterError *error; NSArray *result = [plugin getEnrolledBiometricsWithError:&error]; @@ -491,13 +412,11 @@ - (void)testGetEnrolledBiometricsWithoutEnrolledHardware { } - (void)testIsDeviceSupportedHandlesSupported { - id mockAuthContext = OCMClassMock([LAContext class]); - OCMStub([mockAuthContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication - error:[OCMArg setTo:nil]]) - .andReturn(YES); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; + stubAuthContext.canEvaluateResponse = YES; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; + initWithContexts:@[ stubAuthContext ]]]; FlutterError *error; NSNumber *result = [plugin isDeviceSupportedWithError:&error]; @@ -506,13 +425,11 @@ - (void)testIsDeviceSupportedHandlesSupported { } - (void)testIsDeviceSupportedHandlesUnsupported { - id mockAuthContext = OCMClassMock([LAContext class]); - OCMStub([mockAuthContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication - error:[OCMArg setTo:nil]]) - .andReturn(NO); + StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; + stubAuthContext.canEvaluateResponse = NO; FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ mockAuthContext ]]]; + initWithContexts:@[ stubAuthContext ]]]; FlutterError *error; NSNumber *result = [plugin isDeviceSupportedWithError:&error]; diff --git a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/FLALocalAuthPlugin.m b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/FLALocalAuthPlugin.m index 4563686b364..2cd2c169358 100644 --- a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/FLALocalAuthPlugin.m +++ b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/FLALocalAuthPlugin.m @@ -13,7 +13,10 @@ @interface FLADefaultAuthContextFactory : NSObject @end @implementation FLADefaultAuthContextFactory -- (LAContext *)createAuthContext { +- (id)createAuthContext { + // This works because FLADAuthContext intentionally uses the same signatures as LAContext. + // TODO(stuartmorgan): When converting to Swift, explicitly add conformance via an LAContext + // extension. return [[LAContext alloc] init]; } @end @@ -77,7 +80,7 @@ - (void)authenticateWithOptions:(nonnull FLADAuthOptions *)options strings:(nonnull FLADAuthStrings *)strings completion:(nonnull void (^)(FLADAuthResultDetails *_Nullable, FlutterError *_Nullable))completion { - LAContext *context = [self.authContextFactory createAuthContext]; + id context = [self.authContextFactory createAuthContext]; NSError *authError = nil; self.lastCallState = nil; context.localizedFallbackTitle = strings.localizedFallbackTitle; @@ -103,7 +106,7 @@ - (void)authenticateWithOptions:(nonnull FLADAuthOptions *)options - (nullable NSNumber *)deviceCanSupportBiometricsWithError: (FlutterError *_Nullable __autoreleasing *_Nonnull)error { - LAContext *context = [self.authContextFactory createAuthContext]; + id context = [self.authContextFactory createAuthContext]; NSError *authError = nil; // Check if authentication with biometrics is possible. if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics @@ -124,7 +127,7 @@ - (nullable NSNumber *)deviceCanSupportBiometricsWithError: - (nullable NSArray *)getEnrolledBiometricsWithError: (FlutterError *_Nullable __autoreleasing *_Nonnull)error { - LAContext *context = [self.authContextFactory createAuthContext]; + id context = [self.authContextFactory createAuthContext]; NSError *authError = nil; NSMutableArray *biometrics = [[NSMutableArray alloc] init]; if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics @@ -143,7 +146,7 @@ - (nullable NSNumber *)deviceCanSupportBiometricsWithError: - (nullable NSNumber *)isDeviceSupportedWithError: (FlutterError *_Nullable __autoreleasing *_Nonnull)error { - LAContext *context = [self.authContextFactory createAuthContext]; + id context = [self.authContextFactory createAuthContext]; return @([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:NULL]); } diff --git a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/include/local_auth_darwin/FLALocalAuthPlugin_Test.h b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/include/local_auth_darwin/FLALocalAuthPlugin_Test.h index eb12b29fae3..cfba07b8fb4 100644 --- a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/include/local_auth_darwin/FLALocalAuthPlugin_Test.h +++ b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/include/local_auth_darwin/FLALocalAuthPlugin_Test.h @@ -5,9 +5,25 @@ #import #import -/// Protocol for a source of LAContext instances. Used to allow context injection in unit tests. +NS_ASSUME_NONNULL_BEGIN + +/// Protocol for interacting with LAContext instances, abstracted to allow using mock/fake instances +/// in unit tests. +@protocol FLADAuthContext +@required +@property(nonatomic, nullable, copy) NSString *localizedFallbackTitle; +@property(nonatomic, readonly) LABiometryType biometryType; +- (BOOL)canEvaluatePolicy:(LAPolicy)policy error:(NSError *__autoreleasing *)error; +- (void)evaluatePolicy:(LAPolicy)policy + localizedReason:(NSString *)localizedReason + reply:(void (^)(BOOL success, NSError *__nullable error))reply; +@end + +/// Protocol for a source of FLADAuthContext instances. Used to allow context injection in unit +/// tests. @protocol FLADAuthContextFactory -- (LAContext *)createAuthContext; +@required +- (id)createAuthContext; @end @interface FLALocalAuthPlugin () @@ -15,3 +31,5 @@ - (instancetype)initWithContextFactory:(NSObject *)factory NS_DESIGNATED_INITIALIZER; @end + +NS_ASSUME_NONNULL_END diff --git a/packages/local_auth/local_auth_darwin/example/ios/Podfile b/packages/local_auth/local_auth_darwin/example/ios/Podfile index 196252cf8af..c66ac99f75a 100644 --- a/packages/local_auth/local_auth_darwin/example/ios/Podfile +++ b/packages/local_auth/local_auth_darwin/example/ios/Podfile @@ -31,8 +31,6 @@ target 'Runner' do flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths - - pod 'OCMock','3.5' end end diff --git a/packages/local_auth/local_auth_darwin/pubspec.yaml b/packages/local_auth/local_auth_darwin/pubspec.yaml index 2c914c34004..05a6be6cd59 100644 --- a/packages/local_auth/local_auth_darwin/pubspec.yaml +++ b/packages/local_auth/local_auth_darwin/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_darwin description: iOS implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_darwin issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.3.0 +version: 1.3.1 environment: sdk: ^3.2.3 From fcde5a92c12f30cd13680a83852b2f99e39e5b3b Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 20 May 2024 11:26:43 -0400 Subject: [PATCH 2/2] Add an auth wrapper to address warnings --- .../local_auth_darwin/FLALocalAuthPlugin.m | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/FLALocalAuthPlugin.m b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/FLALocalAuthPlugin.m index 2cd2c169358..c8bc3598085 100644 --- a/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/FLALocalAuthPlugin.m +++ b/packages/local_auth/local_auth_darwin/darwin/local_auth_darwin/Sources/local_auth_darwin/FLALocalAuthPlugin.m @@ -8,16 +8,61 @@ typedef void (^FLADAuthCompletion)(FLADAuthResultDetails *_Nullable, FlutterError *_Nullable); +/// A default auth context that wraps LAContext. +// TODO(stuartmorgan): When converting to Swift, eliminate this class and use an extension to make +// LAContext declare conformance to FLADAuthContext. +@interface FLADefaultAuthContext : NSObject +/// Returns a wrapper for the given LAContext. +- (instancetype)initWithContext:(LAContext *)context NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +/// The wrapped auth context. +@property(nonatomic) LAContext *context; +@end + +@implementation FLADefaultAuthContext +- (instancetype)initWithContext:(LAContext *)context { + self = [super init]; + if (self) { + _context = context; + } + return self; +} + +#pragma mark FLADAuthContext implementation + +- (NSString *)localizedFallbackTitle { + return self.context.localizedFallbackTitle; +} + +- (void)setLocalizedFallbackTitle:(NSString *)localizedFallbackTitle { + self.context.localizedFallbackTitle = localizedFallbackTitle; +} + +- (LABiometryType)biometryType { + return self.context.biometryType; +} + +- (BOOL)canEvaluatePolicy:(LAPolicy)policy error:(NSError *__autoreleasing *)error { + return [self.context canEvaluatePolicy:policy error:error]; +} + +- (void)evaluatePolicy:(LAPolicy)policy + localizedReason:(NSString *)localizedReason + reply:(void (^)(BOOL success, NSError *__nullable error))reply { + [self.context evaluatePolicy:policy localizedReason:localizedReason reply:reply]; +} + +@end + /// A default context factory that wraps standard LAContext allocation. @interface FLADefaultAuthContextFactory : NSObject @end @implementation FLADefaultAuthContextFactory - (id)createAuthContext { - // This works because FLADAuthContext intentionally uses the same signatures as LAContext. - // TODO(stuartmorgan): When converting to Swift, explicitly add conformance via an LAContext - // extension. - return [[LAContext alloc] init]; + // TODO(stuartmorgan): When converting to Swift, just return LAContext here. + return [[FLADefaultAuthContext alloc] initWithContext:[[LAContext alloc] init]]; } @end