diff --git a/packages/local_auth/local_auth_darwin/CHANGELOG.md b/packages/local_auth/local_auth_darwin/CHANGELOG.md index f6df9023d3f5..28d789ca0f68 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 786316835d78..fd0ff531e59e 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 4563686b364e..2cd2c1693584 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 eb12b29fae3b..cfba07b8fb4f 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 196252cf8af7..c66ac99f75af 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 2c914c340041..05a6be6cd59a 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