From f4523444675b846a2a92a917ab3259e6cd25f8ca Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Thu, 11 May 2023 17:11:45 -0700 Subject: [PATCH] [local_auth] Improve iOS test DI (#3959) Replaces test-specific code in the implementation with a more standard DI approach, where the objects to be mocked are provided by a factory passed in during initialization. --- .../local_auth/local_auth_ios/CHANGELOG.md | 4 + .../ios/RunnerTests/FLTLocalAuthPluginTests.m | 109 ++++++++++++------ .../ios/Classes/FLTLocalAuthPlugin.m | 59 ++++++---- .../ios/Classes/FLTLocalAuthPlugin_Test.h | 21 ++++ .../local_auth/local_auth_ios/pubspec.yaml | 2 +- 5 files changed, 131 insertions(+), 64 deletions(-) create mode 100644 packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin_Test.h diff --git a/packages/local_auth/local_auth_ios/CHANGELOG.md b/packages/local_auth/local_auth_ios/CHANGELOG.md index 165a3c668e20..a5dd630c6579 100644 --- a/packages/local_auth/local_auth_ios/CHANGELOG.md +++ b/packages/local_auth/local_auth_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.1.2 + +* Internal refactoring for maintainability. + ## 1.1.1 * Clarifies explanation of endorsement in README. diff --git a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m index 8ca4c4ecbb7e..f6e6c0ad4229 100644 --- a/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m +++ b/packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m @@ -4,22 +4,41 @@ @import LocalAuthentication; @import XCTest; +@import local_auth_ios; #import -#if __has_include() -#import -#else -@import local_auth_ios; -#endif +// Set a long timeout to avoid flake due to slow CI. +static const NSTimeInterval kTimeout = 30.0; -// Private API needed for tests. -@interface FLTLocalAuthPlugin (Test) -- (void)setAuthContextOverrides:(NSArray *)authContexts; +/** + * A context factory that returns preset contexts. + */ +@interface StubAuthContextFactory : NSObject +@property(copy, nonatomic) NSMutableArray *contexts; +- (instancetype)initWithContexts:(NSArray *)contexts; @end -// Set a long timeout to avoid flake due to slow CI. -static const NSTimeInterval kTimeout = 30.0; +@implementation StubAuthContextFactory + +- (instancetype)initWithContexts:(NSArray *)contexts { + self = [super init]; + if (self) { + _contexts = [contexts mutableCopy]; + } + return self; +} + +- (LAContext *)createAuthContext { + NSAssert(self.contexts.count > 0, @"Insufficient test contexts provided"); + LAContext *context = [self.contexts firstObject]; + [self.contexts removeObjectAtIndex:0]; + return context; +} + +@end + +#pragma mark - @interface FLTLocalAuthPluginTests : XCTestCase @end @@ -31,9 +50,10 @@ - (void)setUp { } - (void)testSuccessfullAuthWithBiometrics { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; NSString *reason = @"a reason"; @@ -70,9 +90,10 @@ - (void)testSuccessfullAuthWithBiometrics { } - (void)testSuccessfullAuthWithoutBiometrics { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; NSString *reason = @"a reason"; @@ -109,9 +130,10 @@ - (void)testSuccessfullAuthWithoutBiometrics { } - (void)testFailedAuthWithBiometrics { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; NSString *reason = @"a reason"; @@ -147,9 +169,10 @@ - (void)testFailedAuthWithBiometrics { } - (void)testFailedWithUnknownErrorCode { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; NSString *reason = @"a reason"; @@ -185,9 +208,10 @@ - (void)testFailedWithUnknownErrorCode { } - (void)testSystemCancelledWithoutStickyAuth { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; NSString *reason = @"a reason"; @@ -225,9 +249,10 @@ - (void)testSystemCancelledWithoutStickyAuth { } - (void)testFailedAuthWithoutBiometrics { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; NSString *reason = @"a reason"; @@ -263,9 +288,10 @@ - (void)testFailedAuthWithoutBiometrics { } - (void)testLocalizedFallbackTitle { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; NSString *reason = @"a reason"; @@ -303,9 +329,10 @@ - (void)testLocalizedFallbackTitle { } - (void)testSkippedLocalizedFallbackTitle { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthentication; NSString *reason = @"a reason"; @@ -340,9 +367,10 @@ - (void)testSkippedLocalizedFallbackTitle { } - (void)testDeviceSupportsBiometrics_withEnrolledHardware { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); @@ -362,9 +390,10 @@ - (void)testDeviceSupportsBiometrics_withEnrolledHardware { } - (void)testDeviceSupportsBiometrics_withNonEnrolledHardware { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { @@ -396,9 +425,10 @@ - (void)testDeviceSupportsBiometrics_withNonEnrolledHardware { } - (void)testDeviceSupportsBiometrics_withNoBiometricHardware { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { @@ -430,9 +460,10 @@ - (void)testDeviceSupportsBiometrics_withNoBiometricHardware { } - (void)testGetEnrolledBiometrics_withFaceID { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); @@ -454,9 +485,10 @@ - (void)testGetEnrolledBiometrics_withFaceID { } - (void)testGetEnrolledBiometrics_withTouchID { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES); @@ -478,9 +510,10 @@ - (void)testGetEnrolledBiometrics_withTouchID { } - (void)testGetEnrolledBiometrics_withoutEnrolledHardware { - FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init]; id mockAuthContext = OCMClassMock([LAContext class]); - plugin.authContextOverrides = @[ mockAuthContext ]; + FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] + initWithContextFactory:[[StubAuthContextFactory alloc] + initWithContexts:@[ mockAuthContext ]]]; const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics; void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) { diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m index 10c1e812fbcb..08903d06f19e 100644 --- a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m @@ -1,22 +1,32 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#import "FLTLocalAuthPlugin.h" +#import "FLTLocalAuthPlugin_Test.h" + #import -#import "FLTLocalAuthPlugin.h" +/** + * A default context factory that wraps standard LAContext allocation. + */ +@interface FLADefaultAuthContextFactory : NSObject +@end + +@implementation FLADefaultAuthContextFactory +- (LAContext *)createAuthContext { + return [[LAContext alloc] init]; +} +@end + +#pragma mark - @interface FLTLocalAuthPlugin () @property(nonatomic, copy, nullable) NSDictionary *lastCallArgs; @property(nonatomic, nullable) FlutterResult lastResult; -// For unit tests to inject dummy LAContext instances that will be used when a new context would -// normally be created. Each call to createAuthContext will remove the current first element from -// the array. -- (void)setAuthContextOverrides:(NSArray *)authContexts; +@property(nonatomic, strong) NSObject *authContextFactory; @end -@implementation FLTLocalAuthPlugin { - NSMutableArray *_authContextOverrides; -} +@implementation FLTLocalAuthPlugin + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = @@ -27,6 +37,18 @@ + (void)registerWithRegistrar:(NSObject *)registrar { [registrar addApplicationDelegate:instance]; } +- (instancetype)init { + return [self initWithContextFactory:[[FLADefaultAuthContextFactory alloc] init]]; +} + +- (instancetype)initWithContextFactory:(NSObject *)factory { + self = [super init]; + if (self) { + _authContextFactory = factory; + } + return self; +} + - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { if ([@"authenticate" isEqualToString:call.method]) { bool isBiometricOnly = [call.arguments[@"biometricOnly"] boolValue]; @@ -48,19 +70,6 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result #pragma mark Private Methods -- (void)setAuthContextOverrides:(NSArray *)authContexts { - _authContextOverrides = [authContexts mutableCopy]; -} - -- (LAContext *)createAuthContext { - if ([_authContextOverrides count] > 0) { - LAContext *context = [_authContextOverrides firstObject]; - [_authContextOverrides removeObjectAtIndex:0]; - return context; - } - return [[LAContext alloc] init]; -} - - (void)alertMessage:(NSString *)message firstButton:(NSString *)firstButton flutterResult:(FlutterResult)result @@ -98,7 +107,7 @@ - (void)alertMessage:(NSString *)message } - (void)deviceSupportsBiometrics:(FlutterResult)result { - LAContext *context = self.createAuthContext; + LAContext *context = [self.authContextFactory createAuthContext]; NSError *authError = nil; // Check if authentication with biometrics is possible. if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics @@ -120,7 +129,7 @@ - (void)deviceSupportsBiometrics:(FlutterResult)result { } - (void)getEnrolledBiometrics:(FlutterResult)result { - LAContext *context = self.createAuthContext; + LAContext *context = [self.authContextFactory createAuthContext]; NSError *authError = nil; NSMutableArray *biometrics = [[NSMutableArray alloc] init]; if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics @@ -138,7 +147,7 @@ - (void)getEnrolledBiometrics:(FlutterResult)result { - (void)authenticateWithBiometrics:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { - LAContext *context = self.createAuthContext; + LAContext *context = [self.authContextFactory createAuthContext]; NSError *authError = nil; self.lastCallArgs = nil; self.lastResult = nil; @@ -164,7 +173,7 @@ - (void)authenticateWithBiometrics:(NSDictionary *)arguments } - (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { - LAContext *context = self.createAuthContext; + LAContext *context = [self.authContextFactory createAuthContext]; NSError *authError = nil; _lastCallArgs = nil; _lastResult = nil; diff --git a/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin_Test.h b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin_Test.h new file mode 100644 index 000000000000..c35322033f99 --- /dev/null +++ b/packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin_Test.h @@ -0,0 +1,21 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +/** + * Protocol for a source of LAContext instances. Used to allow context injection in unit tests. + */ +@protocol FLAAuthContextFactory +- (LAContext *)createAuthContext; +@end + +@interface FLTLocalAuthPlugin () +/** + * Returns an instance that uses the given factory to create LAContexts. + */ +- (instancetype)initWithContextFactory:(NSObject *)factory + NS_DESIGNATED_INITIALIZER; +@end diff --git a/packages/local_auth/local_auth_ios/pubspec.yaml b/packages/local_auth/local_auth_ios/pubspec.yaml index 544e820c082e..79654a0039c5 100644 --- a/packages/local_auth/local_auth_ios/pubspec.yaml +++ b/packages/local_auth/local_auth_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: local_auth_ios description: iOS implementation of the local_auth plugin. repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22 -version: 1.1.1 +version: 1.1.2 environment: sdk: ">=2.18.0 <4.0.0"