[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.
diff --git a/packages/local_auth/local_auth_ios/CHANGELOG.md b/packages/local_auth/local_auth_ios/CHANGELOG.md
index 165a3c6..a5dd630 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 8ca4c4e..f6e6c0a 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,23 +4,42 @@
 
 @import LocalAuthentication;
 @import XCTest;
+@import local_auth_ios;
 
 #import <OCMock/OCMock.h>
 
-#if __has_include(<local_auth/FLTLocalAuthPlugin.h>)
-#import <local_auth/FLTLocalAuthPlugin.h>
-#else
-@import local_auth_ios;
-#endif
-
-// Private API needed for tests.
-@interface FLTLocalAuthPlugin (Test)
-- (void)setAuthContextOverrides:(NSArray<LAContext *> *)authContexts;
-@end
-
 // Set a long timeout to avoid flake due to slow CI.
 static const NSTimeInterval kTimeout = 30.0;
 
+/**
+ * A context factory that returns preset contexts.
+ */
+@interface StubAuthContextFactory : NSObject <FLAAuthContextFactory>
+@property(copy, nonatomic) NSMutableArray *contexts;
+- (instancetype)initWithContexts:(NSArray *)contexts;
+@end
+
+@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)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)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)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)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)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)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)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)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)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_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_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)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_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_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 10c1e81..08903d0 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 <LocalAuthentication/LocalAuthentication.h>
 
-#import "FLTLocalAuthPlugin.h"
+/**
+ * A default context factory that wraps standard LAContext allocation.
+ */
+@interface FLADefaultAuthContextFactory : NSObject <FLAAuthContextFactory>
+@end
+
+@implementation FLADefaultAuthContextFactory
+- (LAContext *)createAuthContext {
+  return [[LAContext alloc] init];
+}
+@end
+
+#pragma mark -
 
 @interface FLTLocalAuthPlugin ()
 @property(nonatomic, copy, nullable) NSDictionary<NSString *, NSNumber *> *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<LAContext *> *)authContexts;
+@property(nonatomic, strong) NSObject<FLAAuthContextFactory> *authContextFactory;
 @end
 
-@implementation FLTLocalAuthPlugin {
-  NSMutableArray<LAContext *> *_authContextOverrides;
-}
+@implementation FLTLocalAuthPlugin
 
 + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
   FlutterMethodChannel *channel =
@@ -27,6 +37,18 @@
   [registrar addApplicationDelegate:instance];
 }
 
+- (instancetype)init {
+  return [self initWithContextFactory:[[FLADefaultAuthContextFactory alloc] init]];
+}
+
+- (instancetype)initWithContextFactory:(NSObject<FLAAuthContextFactory> *)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 @@
 
 #pragma mark Private Methods
 
-- (void)setAuthContextOverrides:(NSArray<LAContext *> *)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)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)getEnrolledBiometrics:(FlutterResult)result {
-  LAContext *context = self.createAuthContext;
+  LAContext *context = [self.authContextFactory createAuthContext];
   NSError *authError = nil;
   NSMutableArray<NSString *> *biometrics = [[NSMutableArray<NSString *> alloc] init];
   if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
@@ -138,7 +147,7 @@
 
 - (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)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 0000000..c353220
--- /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 <Flutter/Flutter.h>
+#import <LocalAuthentication/LocalAuthentication.h>
+
+/**
+ * Protocol for a source of LAContext instances. Used to allow context injection in unit tests.
+ */
+@protocol FLAAuthContextFactory <NSObject>
+- (LAContext *)createAuthContext;
+@end
+
+@interface FLTLocalAuthPlugin ()
+/**
+ * Returns an instance that uses the given factory to create LAContexts.
+ */
+- (instancetype)initWithContextFactory:(NSObject<FLAAuthContextFactory> *)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 544e820..79654a0 100644
--- a/packages/local_auth/local_auth_ios/pubspec.yaml
+++ b/packages/local_auth/local_auth_ios/pubspec.yaml
@@ -2,7 +2,7 @@
 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"