Skip to content

Commit e3f3894

Browse files
stuartmorgan-gnploi
authored andcommitted
[local_auth] Improve iOS test DI (flutter#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.
1 parent a0393fe commit e3f3894

File tree

5 files changed

+131
-64
lines changed

5 files changed

+131
-64
lines changed

packages/local_auth/local_auth_ios/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.1.2
2+
3+
* Internal refactoring for maintainability.
4+
15
## 1.1.1
26

37
* Clarifies explanation of endorsement in README.

packages/local_auth/local_auth_ios/example/ios/RunnerTests/FLTLocalAuthPluginTests.m

Lines changed: 71 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,41 @@
44

55
@import LocalAuthentication;
66
@import XCTest;
7+
@import local_auth_ios;
78

89
#import <OCMock/OCMock.h>
910

10-
#if __has_include(<local_auth/FLTLocalAuthPlugin.h>)
11-
#import <local_auth/FLTLocalAuthPlugin.h>
12-
#else
13-
@import local_auth_ios;
14-
#endif
11+
// Set a long timeout to avoid flake due to slow CI.
12+
static const NSTimeInterval kTimeout = 30.0;
1513

16-
// Private API needed for tests.
17-
@interface FLTLocalAuthPlugin (Test)
18-
- (void)setAuthContextOverrides:(NSArray<LAContext *> *)authContexts;
14+
/**
15+
* A context factory that returns preset contexts.
16+
*/
17+
@interface StubAuthContextFactory : NSObject <FLAAuthContextFactory>
18+
@property(copy, nonatomic) NSMutableArray *contexts;
19+
- (instancetype)initWithContexts:(NSArray *)contexts;
1920
@end
2021

21-
// Set a long timeout to avoid flake due to slow CI.
22-
static const NSTimeInterval kTimeout = 30.0;
22+
@implementation StubAuthContextFactory
23+
24+
- (instancetype)initWithContexts:(NSArray *)contexts {
25+
self = [super init];
26+
if (self) {
27+
_contexts = [contexts mutableCopy];
28+
}
29+
return self;
30+
}
31+
32+
- (LAContext *)createAuthContext {
33+
NSAssert(self.contexts.count > 0, @"Insufficient test contexts provided");
34+
LAContext *context = [self.contexts firstObject];
35+
[self.contexts removeObjectAtIndex:0];
36+
return context;
37+
}
38+
39+
@end
40+
41+
#pragma mark -
2342

2443
@interface FLTLocalAuthPluginTests : XCTestCase
2544
@end
@@ -31,9 +50,10 @@ - (void)setUp {
3150
}
3251

3352
- (void)testSuccessfullAuthWithBiometrics {
34-
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
3553
id mockAuthContext = OCMClassMock([LAContext class]);
36-
plugin.authContextOverrides = @[ mockAuthContext ];
54+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
55+
initWithContextFactory:[[StubAuthContextFactory alloc]
56+
initWithContexts:@[ mockAuthContext ]]];
3757

3858
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
3959
NSString *reason = @"a reason";
@@ -70,9 +90,10 @@ - (void)testSuccessfullAuthWithBiometrics {
7090
}
7191

7292
- (void)testSuccessfullAuthWithoutBiometrics {
73-
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
7493
id mockAuthContext = OCMClassMock([LAContext class]);
75-
plugin.authContextOverrides = @[ mockAuthContext ];
94+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
95+
initWithContextFactory:[[StubAuthContextFactory alloc]
96+
initWithContexts:@[ mockAuthContext ]]];
7697

7798
const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
7899
NSString *reason = @"a reason";
@@ -109,9 +130,10 @@ - (void)testSuccessfullAuthWithoutBiometrics {
109130
}
110131

111132
- (void)testFailedAuthWithBiometrics {
112-
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
113133
id mockAuthContext = OCMClassMock([LAContext class]);
114-
plugin.authContextOverrides = @[ mockAuthContext ];
134+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
135+
initWithContextFactory:[[StubAuthContextFactory alloc]
136+
initWithContexts:@[ mockAuthContext ]]];
115137

116138
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
117139
NSString *reason = @"a reason";
@@ -147,9 +169,10 @@ - (void)testFailedAuthWithBiometrics {
147169
}
148170

149171
- (void)testFailedWithUnknownErrorCode {
150-
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
151172
id mockAuthContext = OCMClassMock([LAContext class]);
152-
plugin.authContextOverrides = @[ mockAuthContext ];
173+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
174+
initWithContextFactory:[[StubAuthContextFactory alloc]
175+
initWithContexts:@[ mockAuthContext ]]];
153176

154177
const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
155178
NSString *reason = @"a reason";
@@ -185,9 +208,10 @@ - (void)testFailedWithUnknownErrorCode {
185208
}
186209

187210
- (void)testSystemCancelledWithoutStickyAuth {
188-
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
189211
id mockAuthContext = OCMClassMock([LAContext class]);
190-
plugin.authContextOverrides = @[ mockAuthContext ];
212+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
213+
initWithContextFactory:[[StubAuthContextFactory alloc]
214+
initWithContexts:@[ mockAuthContext ]]];
191215

192216
const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
193217
NSString *reason = @"a reason";
@@ -225,9 +249,10 @@ - (void)testSystemCancelledWithoutStickyAuth {
225249
}
226250

227251
- (void)testFailedAuthWithoutBiometrics {
228-
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
229252
id mockAuthContext = OCMClassMock([LAContext class]);
230-
plugin.authContextOverrides = @[ mockAuthContext ];
253+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
254+
initWithContextFactory:[[StubAuthContextFactory alloc]
255+
initWithContexts:@[ mockAuthContext ]]];
231256

232257
const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
233258
NSString *reason = @"a reason";
@@ -263,9 +288,10 @@ - (void)testFailedAuthWithoutBiometrics {
263288
}
264289

265290
- (void)testLocalizedFallbackTitle {
266-
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
267291
id mockAuthContext = OCMClassMock([LAContext class]);
268-
plugin.authContextOverrides = @[ mockAuthContext ];
292+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
293+
initWithContextFactory:[[StubAuthContextFactory alloc]
294+
initWithContexts:@[ mockAuthContext ]]];
269295

270296
const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
271297
NSString *reason = @"a reason";
@@ -303,9 +329,10 @@ - (void)testLocalizedFallbackTitle {
303329
}
304330

305331
- (void)testSkippedLocalizedFallbackTitle {
306-
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
307332
id mockAuthContext = OCMClassMock([LAContext class]);
308-
plugin.authContextOverrides = @[ mockAuthContext ];
333+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
334+
initWithContextFactory:[[StubAuthContextFactory alloc]
335+
initWithContexts:@[ mockAuthContext ]]];
309336

310337
const LAPolicy policy = LAPolicyDeviceOwnerAuthentication;
311338
NSString *reason = @"a reason";
@@ -340,9 +367,10 @@ - (void)testSkippedLocalizedFallbackTitle {
340367
}
341368

342369
- (void)testDeviceSupportsBiometrics_withEnrolledHardware {
343-
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
344370
id mockAuthContext = OCMClassMock([LAContext class]);
345-
plugin.authContextOverrides = @[ mockAuthContext ];
371+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
372+
initWithContextFactory:[[StubAuthContextFactory alloc]
373+
initWithContexts:@[ mockAuthContext ]]];
346374

347375
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
348376
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
@@ -362,9 +390,10 @@ - (void)testDeviceSupportsBiometrics_withEnrolledHardware {
362390
}
363391

364392
- (void)testDeviceSupportsBiometrics_withNonEnrolledHardware {
365-
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
366393
id mockAuthContext = OCMClassMock([LAContext class]);
367-
plugin.authContextOverrides = @[ mockAuthContext ];
394+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
395+
initWithContextFactory:[[StubAuthContextFactory alloc]
396+
initWithContexts:@[ mockAuthContext ]]];
368397

369398
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
370399
void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
@@ -396,9 +425,10 @@ - (void)testDeviceSupportsBiometrics_withNonEnrolledHardware {
396425
}
397426

398427
- (void)testDeviceSupportsBiometrics_withNoBiometricHardware {
399-
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
400428
id mockAuthContext = OCMClassMock([LAContext class]);
401-
plugin.authContextOverrides = @[ mockAuthContext ];
429+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
430+
initWithContextFactory:[[StubAuthContextFactory alloc]
431+
initWithContexts:@[ mockAuthContext ]]];
402432

403433
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
404434
void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) {
@@ -430,9 +460,10 @@ - (void)testDeviceSupportsBiometrics_withNoBiometricHardware {
430460
}
431461

432462
- (void)testGetEnrolledBiometrics_withFaceID {
433-
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
434463
id mockAuthContext = OCMClassMock([LAContext class]);
435-
plugin.authContextOverrides = @[ mockAuthContext ];
464+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
465+
initWithContextFactory:[[StubAuthContextFactory alloc]
466+
initWithContexts:@[ mockAuthContext ]]];
436467

437468
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
438469
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
@@ -454,9 +485,10 @@ - (void)testGetEnrolledBiometrics_withFaceID {
454485
}
455486

456487
- (void)testGetEnrolledBiometrics_withTouchID {
457-
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
458488
id mockAuthContext = OCMClassMock([LAContext class]);
459-
plugin.authContextOverrides = @[ mockAuthContext ];
489+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
490+
initWithContextFactory:[[StubAuthContextFactory alloc]
491+
initWithContexts:@[ mockAuthContext ]]];
460492

461493
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
462494
OCMStub([mockAuthContext canEvaluatePolicy:policy error:[OCMArg setTo:nil]]).andReturn(YES);
@@ -478,9 +510,10 @@ - (void)testGetEnrolledBiometrics_withTouchID {
478510
}
479511

480512
- (void)testGetEnrolledBiometrics_withoutEnrolledHardware {
481-
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc] init];
482513
id mockAuthContext = OCMClassMock([LAContext class]);
483-
plugin.authContextOverrides = @[ mockAuthContext ];
514+
FLTLocalAuthPlugin *plugin = [[FLTLocalAuthPlugin alloc]
515+
initWithContextFactory:[[StubAuthContextFactory alloc]
516+
initWithContexts:@[ mockAuthContext ]]];
484517

485518
const LAPolicy policy = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
486519
void (^canEvaluatePolicyHandler)(NSInvocation *) = ^(NSInvocation *invocation) {

packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
// Copyright 2013 The Flutter Authors. All rights reserved.
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
4+
#import "FLTLocalAuthPlugin.h"
5+
#import "FLTLocalAuthPlugin_Test.h"
6+
47
#import <LocalAuthentication/LocalAuthentication.h>
58

6-
#import "FLTLocalAuthPlugin.h"
9+
/**
10+
* A default context factory that wraps standard LAContext allocation.
11+
*/
12+
@interface FLADefaultAuthContextFactory : NSObject <FLAAuthContextFactory>
13+
@end
14+
15+
@implementation FLADefaultAuthContextFactory
16+
- (LAContext *)createAuthContext {
17+
return [[LAContext alloc] init];
18+
}
19+
@end
20+
21+
#pragma mark -
722

823
@interface FLTLocalAuthPlugin ()
924
@property(nonatomic, copy, nullable) NSDictionary<NSString *, NSNumber *> *lastCallArgs;
1025
@property(nonatomic, nullable) FlutterResult lastResult;
11-
// For unit tests to inject dummy LAContext instances that will be used when a new context would
12-
// normally be created. Each call to createAuthContext will remove the current first element from
13-
// the array.
14-
- (void)setAuthContextOverrides:(NSArray<LAContext *> *)authContexts;
26+
@property(nonatomic, strong) NSObject<FLAAuthContextFactory> *authContextFactory;
1527
@end
1628

17-
@implementation FLTLocalAuthPlugin {
18-
NSMutableArray<LAContext *> *_authContextOverrides;
19-
}
29+
@implementation FLTLocalAuthPlugin
2030

2131
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
2232
FlutterMethodChannel *channel =
@@ -27,6 +37,18 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
2737
[registrar addApplicationDelegate:instance];
2838
}
2939

40+
- (instancetype)init {
41+
return [self initWithContextFactory:[[FLADefaultAuthContextFactory alloc] init]];
42+
}
43+
44+
- (instancetype)initWithContextFactory:(NSObject<FLAAuthContextFactory> *)factory {
45+
self = [super init];
46+
if (self) {
47+
_authContextFactory = factory;
48+
}
49+
return self;
50+
}
51+
3052
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
3153
if ([@"authenticate" isEqualToString:call.method]) {
3254
bool isBiometricOnly = [call.arguments[@"biometricOnly"] boolValue];
@@ -48,19 +70,6 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result
4870

4971
#pragma mark Private Methods
5072

51-
- (void)setAuthContextOverrides:(NSArray<LAContext *> *)authContexts {
52-
_authContextOverrides = [authContexts mutableCopy];
53-
}
54-
55-
- (LAContext *)createAuthContext {
56-
if ([_authContextOverrides count] > 0) {
57-
LAContext *context = [_authContextOverrides firstObject];
58-
[_authContextOverrides removeObjectAtIndex:0];
59-
return context;
60-
}
61-
return [[LAContext alloc] init];
62-
}
63-
6473
- (void)alertMessage:(NSString *)message
6574
firstButton:(NSString *)firstButton
6675
flutterResult:(FlutterResult)result
@@ -98,7 +107,7 @@ - (void)alertMessage:(NSString *)message
98107
}
99108

100109
- (void)deviceSupportsBiometrics:(FlutterResult)result {
101-
LAContext *context = self.createAuthContext;
110+
LAContext *context = [self.authContextFactory createAuthContext];
102111
NSError *authError = nil;
103112
// Check if authentication with biometrics is possible.
104113
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
@@ -120,7 +129,7 @@ - (void)deviceSupportsBiometrics:(FlutterResult)result {
120129
}
121130

122131
- (void)getEnrolledBiometrics:(FlutterResult)result {
123-
LAContext *context = self.createAuthContext;
132+
LAContext *context = [self.authContextFactory createAuthContext];
124133
NSError *authError = nil;
125134
NSMutableArray<NSString *> *biometrics = [[NSMutableArray<NSString *> alloc] init];
126135
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
@@ -138,7 +147,7 @@ - (void)getEnrolledBiometrics:(FlutterResult)result {
138147

139148
- (void)authenticateWithBiometrics:(NSDictionary *)arguments
140149
withFlutterResult:(FlutterResult)result {
141-
LAContext *context = self.createAuthContext;
150+
LAContext *context = [self.authContextFactory createAuthContext];
142151
NSError *authError = nil;
143152
self.lastCallArgs = nil;
144153
self.lastResult = nil;
@@ -164,7 +173,7 @@ - (void)authenticateWithBiometrics:(NSDictionary *)arguments
164173
}
165174

166175
- (void)authenticate:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result {
167-
LAContext *context = self.createAuthContext;
176+
LAContext *context = [self.authContextFactory createAuthContext];
168177
NSError *authError = nil;
169178
_lastCallArgs = nil;
170179
_lastResult = nil;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
#import <Flutter/Flutter.h>
6+
#import <LocalAuthentication/LocalAuthentication.h>
7+
8+
/**
9+
* Protocol for a source of LAContext instances. Used to allow context injection in unit tests.
10+
*/
11+
@protocol FLAAuthContextFactory <NSObject>
12+
- (LAContext *)createAuthContext;
13+
@end
14+
15+
@interface FLTLocalAuthPlugin ()
16+
/**
17+
* Returns an instance that uses the given factory to create LAContexts.
18+
*/
19+
- (instancetype)initWithContextFactory:(NSObject<FLAAuthContextFactory> *)factory
20+
NS_DESIGNATED_INITIALIZER;
21+
@end

packages/local_auth/local_auth_ios/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: local_auth_ios
22
description: iOS implementation of the local_auth plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/local_auth/local_auth_ios
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22
5-
version: 1.1.1
5+
version: 1.1.2
66

77
environment:
88
sdk: ">=2.18.0 <4.0.0"

0 commit comments

Comments
 (0)