Skip to content

[local_auth] Improve iOS test DI #3959

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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