Skip to content

Commit

Permalink
[local_auth] Improve iOS test DI (#3959)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
stuartmorgan authored May 12, 2023
1 parent e0ceb34 commit f452344
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 64 deletions.
4 changes: 4 additions & 0 deletions packages/local_auth/local_auth_ios/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 1.1.2

* Internal refactoring for maintainability.

## 1.1.1

* Clarifies explanation of endorsement in README.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,41 @@

@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
// 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<LAContext *> *)authContexts;
/**
* A context factory that returns preset contexts.
*/
@interface StubAuthContextFactory : NSObject <FLAAuthContextFactory>
@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
Expand All @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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) {
Expand Down
59 changes: 34 additions & 25 deletions packages/local_auth/local_auth_ios/ios/Classes/FLTLocalAuthPlugin.m
Original file line number Diff line number Diff line change
@@ -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 =
Expand All @@ -27,6 +37,18 @@ + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
[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];
Expand All @@ -48,19 +70,6 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result

#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
Expand Down Expand Up @@ -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
Expand All @@ -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<NSString *> *biometrics = [[NSMutableArray<NSString *> alloc] init];
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
Expand All @@ -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;
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion packages/local_auth/local_auth_ios/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit f452344

Please sign in to comment.