From 65254411e6773cdc97c5b091c07856b52f9965c9 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 22 May 2024 10:07:09 -0400 Subject: [PATCH] [local_auth] Convert native unit tests to Swift (#6779) Converts native unit tests from Objective-C to Swift, as a first step toward an eventual plugin conversion. Since OCMock usage was removed in a previous PR, the tests are converted essentially directly (the rewrites were largely mechanical syntax replacement), without any changes needed to production code. There are a few places where interacting with the Pigeon-generated Obj-C is somewhat awkward because the NSError signature isn't auto-converting to `throw` in cases where we use `NSNumber` wrapping, but all that will get cleaned up when we switch the plugin over to Swift Pigeon generation and have idiomatic Swift APIs that the tests will be calling instead. Fixes a couple of false positives in the repo tooling surfaced by this PR: - `darwin/Tests/` wasn't recognized as a test directory. - Swift tests weren't recognized as exempt from requiring Swift entries in the podspec. Part of https://github.com/flutter/flutter/issues/119104 --- .../darwin/Tests/FLALocalAuthPluginTests.m | 449 ------------------ .../Tests/FLALocalAuthPluginTests.swift | 397 ++++++++++++++++ .../ios/Runner.xcodeproj/project.pbxproj | 16 +- .../lib/src/common/package_state_utils.dart | 1 + .../tool/lib/src/podspec_check_command.dart | 6 + .../tool/test/podspec_check_command_test.dart | 25 + .../tool/test/version_check_command_test.dart | 1 + 7 files changed, 441 insertions(+), 454 deletions(-) delete mode 100644 packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.m create mode 100644 packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift diff --git a/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.m b/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.m deleted file mode 100644 index fd0ff531e59e..000000000000 --- a/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.m +++ /dev/null @@ -1,449 +0,0 @@ -// 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 LocalAuthentication; -@import XCTest; -@import local_auth_darwin; - -// 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 -@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; -} - -- (id)createAuthContext { - NSAssert(self.contexts.count > 0, @"Insufficient test contexts provided"); - id context = [self.contexts firstObject]; - [self.contexts removeObjectAtIndex:0]; - return context; -} - -@end - -@interface StubAuthContext : NSObject -/// Whether calls to this stub are expected to be for biometric authentication. -/// -/// While this object could be set up to return different values for different policies, in -/// practice only one policy is needed by any given test, so this just allows asserting that the -/// code is calling with the intended policy. -@property(nonatomic) BOOL expectBiometrics; -/// The value to return from canEvaluatePolicy. -@property(nonatomic) BOOL canEvaluateResponse; -/// The error to return from canEvaluatePolicy. -@property(nonatomic) NSError *canEvaluateError; -/// The value to return from evaluatePolicy:error:. -@property(nonatomic) BOOL evaluateResponse; -/// The error to return from evaluatePolicy:error:. -@property(nonatomic) NSError *evaluateError; - -// Overridden as read-write to allow stubbing. -@property(nonatomic, readwrite) LABiometryType biometryType; -@end - -@implementation StubAuthContext -@synthesize localizedFallbackTitle; - -- (BOOL)canEvaluatePolicy:(LAPolicy)policy - error:(NSError *__autoreleasing _Nullable *_Nullable)error { - XCTAssertEqual(policy, self.expectBiometrics ? LAPolicyDeviceOwnerAuthenticationWithBiometrics - : LAPolicyDeviceOwnerAuthentication); - if (error) { - *error = self.canEvaluateError; - } - return self.canEvaluateResponse; -} - -- (void)evaluatePolicy:(LAPolicy)policy - localizedReason:(nonnull NSString *)localizedReason - reply:(nonnull void (^)(BOOL, NSError *_Nullable))reply { - XCTAssertEqual(policy, self.expectBiometrics ? LAPolicyDeviceOwnerAuthenticationWithBiometrics - : LAPolicyDeviceOwnerAuthentication); - // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not - // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on - // a background thread. - dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{ - reply(self.evaluateResponse, self.evaluateError); - }); -} - -@end - -#pragma mark - - -@interface FLALocalAuthPluginTests : XCTestCase -@end - -@implementation FLALocalAuthPluginTests - -- (void)setUp { - self.continueAfterFailure = NO; -} - -- (void)testSuccessfullAuthWithBiometrics { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - FLADAuthStrings *strings = [self createAuthStrings]; - stubAuthContext.expectBiometrics = YES; - stubAuthContext.canEvaluateResponse = YES; - stubAuthContext.evaluateResponse = YES; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:YES - sticky:NO - useErrorDialogs:NO] - strings:strings - completion:^(FLADAuthResultDetails *_Nullable resultDetails, - FlutterError *_Nullable error) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertEqual(resultDetails.result, FLADAuthResultSuccess); - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - -- (void)testSuccessfullAuthWithoutBiometrics { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - FLADAuthStrings *strings = [self createAuthStrings]; - stubAuthContext.canEvaluateResponse = YES; - stubAuthContext.evaluateResponse = YES; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:NO - sticky:NO - useErrorDialogs:NO] - strings:strings - completion:^(FLADAuthResultDetails *_Nullable resultDetails, - FlutterError *_Nullable error) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertEqual(resultDetails.result, FLADAuthResultSuccess); - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - -- (void)testFailedAuthWithBiometrics { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - FLADAuthStrings *strings = [self createAuthStrings]; - stubAuthContext.expectBiometrics = YES; - stubAuthContext.canEvaluateResponse = YES; - stubAuthContext.evaluateError = [NSError errorWithDomain:@"error" - code:LAErrorAuthenticationFailed - userInfo:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:YES - sticky:NO - useErrorDialogs:NO] - strings:strings - completion:^(FLADAuthResultDetails *_Nullable resultDetails, - FlutterError *_Nullable error) { - XCTAssertTrue([NSThread isMainThread]); - // TODO(stuartmorgan): Fix this; this was the pre-Pigeon-migration - // behavior, so is preserved as part of the migration, but a failed - // authentication should return failure, not an error that results in a - // PlatformException. - XCTAssertEqual(resultDetails.result, FLADAuthResultErrorNotAvailable); - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - -- (void)testFailedWithUnknownErrorCode { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - FLADAuthStrings *strings = [self createAuthStrings]; - stubAuthContext.canEvaluateResponse = YES; - stubAuthContext.evaluateError = [NSError errorWithDomain:@"error" code:99 userInfo:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:NO - sticky:NO - useErrorDialogs:NO] - strings:strings - completion:^(FLADAuthResultDetails *_Nullable resultDetails, - FlutterError *_Nullable error) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertEqual(resultDetails.result, FLADAuthResultErrorNotAvailable); - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - -- (void)testSystemCancelledWithoutStickyAuth { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - FLADAuthStrings *strings = [self createAuthStrings]; - stubAuthContext.canEvaluateResponse = YES; - stubAuthContext.evaluateError = [NSError errorWithDomain:@"error" - code:LAErrorSystemCancel - userInfo:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:NO - sticky:NO - useErrorDialogs:NO] - strings:strings - completion:^(FLADAuthResultDetails *_Nullable resultDetails, - FlutterError *_Nullable error) { - XCTAssertTrue([NSThread isMainThread]); - XCTAssertEqual(resultDetails.result, FLADAuthResultFailure); - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - -- (void)testFailedAuthWithoutBiometrics { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - FLADAuthStrings *strings = [self createAuthStrings]; - stubAuthContext.canEvaluateResponse = YES; - stubAuthContext.evaluateError = [NSError errorWithDomain:@"error" - code:LAErrorAuthenticationFailed - userInfo:nil]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:NO - sticky:NO - useErrorDialogs:NO] - strings:strings - completion:^(FLADAuthResultDetails *_Nullable resultDetails, - FlutterError *_Nullable error) { - XCTAssertTrue([NSThread isMainThread]); - // TODO(stuartmorgan): Fix this; this was the pre-Pigeon-migration - // behavior, so is preserved as part of the migration, but a failed - // authentication should return failure, not an error that results in a - // PlatformException. - XCTAssertEqual(resultDetails.result, FLADAuthResultErrorNotAvailable); - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - -- (void)testLocalizedFallbackTitle { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - FLADAuthStrings *strings = [self createAuthStrings]; - strings.localizedFallbackTitle = @"a title"; - stubAuthContext.canEvaluateResponse = YES; - stubAuthContext.evaluateResponse = YES; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:NO - sticky:NO - useErrorDialogs:NO] - strings:strings - completion:^(FLADAuthResultDetails *_Nullable resultDetails, - FlutterError *_Nullable error) { - XCTAssertEqual(stubAuthContext.localizedFallbackTitle, - strings.localizedFallbackTitle); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - -- (void)testSkippedLocalizedFallbackTitle { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - FLADAuthStrings *strings = [self createAuthStrings]; - strings.localizedFallbackTitle = nil; - stubAuthContext.canEvaluateResponse = YES; - stubAuthContext.evaluateResponse = YES; - - XCTestExpectation *expectation = [self expectationWithDescription:@"Result is called"]; - [plugin authenticateWithOptions:[FLADAuthOptions makeWithBiometricOnly:NO - sticky:NO - useErrorDialogs:NO] - strings:strings - completion:^(FLADAuthResultDetails *_Nullable resultDetails, - FlutterError *_Nullable error) { - XCTAssertNil(stubAuthContext.localizedFallbackTitle); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:kTimeout handler:nil]; -} - -- (void)testDeviceSupportsBiometrics_withEnrolledHardware { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - stubAuthContext.expectBiometrics = YES; - stubAuthContext.canEvaluateResponse = YES; - - FlutterError *error; - NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; - XCTAssertTrue([result boolValue]); - XCTAssertNil(error); -} - -- (void)testDeviceSupportsBiometrics_withNonEnrolledHardware { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - stubAuthContext.expectBiometrics = YES; - stubAuthContext.canEvaluateResponse = NO; - stubAuthContext.canEvaluateError = [NSError errorWithDomain:@"error" - code:LAErrorBiometryNotEnrolled - userInfo:nil]; - - FlutterError *error; - NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; - XCTAssertTrue([result boolValue]); - XCTAssertNil(error); -} - -- (void)testDeviceSupportsBiometrics_withNoBiometricHardware { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - stubAuthContext.expectBiometrics = YES; - stubAuthContext.canEvaluateResponse = NO; - stubAuthContext.canEvaluateError = [NSError errorWithDomain:@"error" code:0 userInfo:nil]; - - FlutterError *error; - NSNumber *result = [plugin deviceCanSupportBiometricsWithError:&error]; - XCTAssertFalse([result boolValue]); - XCTAssertNil(error); -} - -- (void)testGetEnrolledBiometricsWithFaceID { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - stubAuthContext.expectBiometrics = YES; - stubAuthContext.canEvaluateResponse = YES; - stubAuthContext.biometryType = LABiometryTypeFaceID; - - FlutterError *error; - NSArray *result = [plugin getEnrolledBiometricsWithError:&error]; - XCTAssertEqual([result count], 1); - XCTAssertEqual(result[0].value, FLADAuthBiometricFace); - XCTAssertNil(error); -} - -- (void)testGetEnrolledBiometricsWithTouchID { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - stubAuthContext.expectBiometrics = YES; - stubAuthContext.canEvaluateResponse = YES; - stubAuthContext.biometryType = LABiometryTypeTouchID; - - FlutterError *error; - NSArray *result = [plugin getEnrolledBiometricsWithError:&error]; - XCTAssertEqual([result count], 1); - XCTAssertEqual(result[0].value, FLADAuthBiometricFingerprint); - XCTAssertNil(error); -} - -- (void)testGetEnrolledBiometricsWithoutEnrolledHardware { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - stubAuthContext.expectBiometrics = YES; - stubAuthContext.canEvaluateResponse = NO; - stubAuthContext.canEvaluateError = [NSError errorWithDomain:@"error" - code:LAErrorBiometryNotEnrolled - userInfo:nil]; - - FlutterError *error; - NSArray *result = [plugin getEnrolledBiometricsWithError:&error]; - XCTAssertEqual([result count], 0); - XCTAssertNil(error); -} - -- (void)testIsDeviceSupportedHandlesSupported { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - stubAuthContext.canEvaluateResponse = YES; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - FlutterError *error; - NSNumber *result = [plugin isDeviceSupportedWithError:&error]; - XCTAssertTrue([result boolValue]); - XCTAssertNil(error); -} - -- (void)testIsDeviceSupportedHandlesUnsupported { - StubAuthContext *stubAuthContext = [[StubAuthContext alloc] init]; - stubAuthContext.canEvaluateResponse = NO; - FLALocalAuthPlugin *plugin = [[FLALocalAuthPlugin alloc] - initWithContextFactory:[[StubAuthContextFactory alloc] - initWithContexts:@[ stubAuthContext ]]]; - - FlutterError *error; - NSNumber *result = [plugin isDeviceSupportedWithError:&error]; - XCTAssertFalse([result boolValue]); - XCTAssertNil(error); -} - -// Creates an FLADAuthStrings with placeholder values. -- (FLADAuthStrings *)createAuthStrings { - return [FLADAuthStrings makeWithReason:@"a reason" - lockOut:@"locked out" - goToSettingsButton:@"Go To Settings" - goToSettingsDescription:@"Settings" - cancelButton:@"Cancel" - localizedFallbackTitle:nil]; -} -@end diff --git a/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift b/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift new file mode 100644 index 000000000000..865be355fd16 --- /dev/null +++ b/packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.swift @@ -0,0 +1,397 @@ +// 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 +import XCTest + +@testable import local_auth_darwin + +// Set a long timeout to avoid flake due to slow CI. +private let timeout: TimeInterval = 30.0 + +/// A context factory that returns preset contexts. +final class StubAuthContextFactory: NSObject, FLADAuthContextFactory { + var contexts: [FLADAuthContext] + init(contexts: [FLADAuthContext]) { + self.contexts = contexts + } + + func createAuthContext() -> FLADAuthContext { + XCTAssert(self.contexts.count > 0, "Insufficient test contexts provided") + return self.contexts.removeFirst() + } +} + +final class StubAuthContext: NSObject, FLADAuthContext { + /// Whether calls to this stub are expected to be for biometric authentication. + /// + /// While this object could be set up to return different values for different policies, in + /// practice only one policy is needed by any given test, so this just allows asserting that the + /// code is calling with the intended policy. + var expectBiometrics = false + /// The error to return from canEvaluatePolicy. + var canEvaluateError: NSError? + /// The value to return from evaluatePolicy:error:. + var evaluateResponse = false + /// The error to return from evaluatePolicy:error:. + var evaluateError: NSError? + + // Overridden as read-write to allow stubbing. + var biometryType: LABiometryType = .none + var localizedFallbackTitle: String? + + func canEvaluatePolicy(_ policy: LAPolicy) throws { + XCTAssertEqual( + policy, + expectBiometrics + ? LAPolicy.deviceOwnerAuthenticationWithBiometrics + : LAPolicy.deviceOwnerAuthentication) + if let canEvaluateError = canEvaluateError { + throw canEvaluateError + } + } + + func evaluatePolicy( + _ policy: LAPolicy, localizedReason: String, reply: @escaping (Bool, Error?) -> Void + ) { + XCTAssertEqual( + policy, + expectBiometrics + ? LAPolicy.deviceOwnerAuthenticationWithBiometrics + : LAPolicy.deviceOwnerAuthentication) + // evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not + // guaranteed to be on the main thread. Ensure that's handled correctly by calling back on + // a background thread. + DispatchQueue.global(qos: .background).async { + reply(self.evaluateResponse, self.evaluateError) + } + } +} + +// MARK: - + +class FLALocalAuthPluginTests: XCTestCase { + + func testSuccessfullAuthWithBiometrics() throws { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.expectBiometrics = true + stubAuthContext.evaluateResponse = true + let expectation = expectation(description: "Result is called") + plugin.authenticate( + with: FLADAuthOptions.make( + withBiometricOnly: true, + sticky: false, + useErrorDialogs: false), + strings: strings + ) { resultDetails, error in + XCTAssertTrue(Thread.isMainThread) + XCTAssertEqual(resultDetails?.result, FLADAuthResult.success) + XCTAssertNil(error) + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + func testSuccessfullAuthWithoutBiometrics() { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.evaluateResponse = true + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + with: FLADAuthOptions.make( + withBiometricOnly: false, + sticky: false, + useErrorDialogs: false), + strings: strings + ) { resultDetails, error in + XCTAssertTrue(Thread.isMainThread) + XCTAssertEqual(resultDetails?.result, FLADAuthResult.success) + XCTAssertNil(error) + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + func testFailedAuthWithBiometrics() { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.expectBiometrics = true + stubAuthContext.evaluateError = NSError( + domain: "error", code: LAError.authenticationFailed.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + with: FLADAuthOptions.make( + withBiometricOnly: true, + sticky: false, + useErrorDialogs: false), + strings: strings + ) { resultDetails, error in + XCTAssertTrue(Thread.isMainThread) + // TODO(stuartmorgan): Fix this; this was the pre-Pigeon-migration + // behavior, so is preserved as part of the migration, but a failed + // authentication should return failure, not an error that results in a + // PlatformException. + XCTAssertEqual(resultDetails?.result, FLADAuthResult.errorNotAvailable) + XCTAssertNil(error) + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + func testFailedWithUnknownErrorCode() { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.evaluateError = NSError(domain: "error", code: 99) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + with: FLADAuthOptions.make( + withBiometricOnly: false, + sticky: false, + useErrorDialogs: false), + strings: strings + ) { resultDetails, error in + XCTAssertTrue(Thread.isMainThread) + XCTAssertEqual(resultDetails?.result, FLADAuthResult.errorNotAvailable) + XCTAssertNil(error) + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + func testSystemCancelledWithoutStickyAuth() { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.evaluateError = NSError(domain: "error", code: LAError.systemCancel.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + with: FLADAuthOptions.make( + withBiometricOnly: false, + sticky: false, + useErrorDialogs: false), + strings: strings + ) { resultDetails, error in + XCTAssertTrue(Thread.isMainThread) + XCTAssertEqual(resultDetails?.result, FLADAuthResult.failure) + XCTAssertNil(error) + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + func testFailedAuthWithoutBiometrics() { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + stubAuthContext.evaluateError = NSError( + domain: "error", code: LAError.authenticationFailed.rawValue) + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + with: FLADAuthOptions.make( + withBiometricOnly: false, + sticky: false, + useErrorDialogs: false), + strings: strings + ) { resultDetails, error in + XCTAssertTrue(Thread.isMainThread) + // TODO(stuartmorgan): Fix this; this was the pre-Pigeon-migration + // behavior, so is preserved as part of the migration, but a failed + // authentication should return failure, not an error that results in a + // PlatformException. + XCTAssertEqual(resultDetails?.result, FLADAuthResult.errorNotAvailable) + XCTAssertNil(error) + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + func testLocalizedFallbackTitle() { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + strings.localizedFallbackTitle = "a title" + stubAuthContext.evaluateResponse = true + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + with: FLADAuthOptions.make( + withBiometricOnly: false, + sticky: false, + useErrorDialogs: false), + strings: strings + ) { resultDetails, error in + XCTAssertEqual( + stubAuthContext.localizedFallbackTitle, + strings.localizedFallbackTitle) + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + func testSkippedLocalizedFallbackTitle() { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + let strings = createAuthStrings() + strings.localizedFallbackTitle = nil + stubAuthContext.evaluateResponse = true + + let expectation = expectation(description: "Result is called") + plugin.authenticate( + with: FLADAuthOptions.make( + withBiometricOnly: false, + sticky: false, + useErrorDialogs: false), + strings: strings + ) { resultDetails, error in + XCTAssertNil(stubAuthContext.localizedFallbackTitle) + expectation.fulfill() + } + self.waitForExpectations(timeout: timeout) + } + + func testDeviceSupportsBiometrics_withEnrolledHardware() { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + stubAuthContext.expectBiometrics = true + + var error: FlutterError? + let result = plugin.deviceCanSupportBiometricsWithError(&error) + XCTAssertTrue(result!.boolValue) + XCTAssertNil(error) + } + + func testDeviceSupportsBiometrics_withNonEnrolledHardware() { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + stubAuthContext.expectBiometrics = true + stubAuthContext.canEvaluateError = NSError( + domain: "error", code: LAError.biometryNotEnrolled.rawValue) + + var error: FlutterError? + let result = plugin.deviceCanSupportBiometricsWithError(&error) + XCTAssertTrue(result!.boolValue) + XCTAssertNil(error) + } + + func testDeviceSupportsBiometrics_withNoBiometricHardware() { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + stubAuthContext.expectBiometrics = true + stubAuthContext.canEvaluateError = NSError(domain: "error", code: 0) + + var error: FlutterError? + let result = plugin.deviceCanSupportBiometricsWithError(&error) + XCTAssertFalse(result!.boolValue) + XCTAssertNil(error) + } + + func testGetEnrolledBiometricsWithFaceID() { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + stubAuthContext.expectBiometrics = true + stubAuthContext.biometryType = .faceID + + var error: FlutterError? + let result = plugin.getEnrolledBiometricsWithError(&error) + XCTAssertEqual(result!.count, 1) + XCTAssertEqual(result![0].value, FLADAuthBiometric.face) + XCTAssertNil(error) + } + + func testGetEnrolledBiometricsWithTouchID() { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + stubAuthContext.expectBiometrics = true + stubAuthContext.biometryType = .touchID + + var error: FlutterError? + let result = plugin.getEnrolledBiometricsWithError(&error) + XCTAssertEqual(result!.count, 1) + XCTAssertEqual(result![0].value, FLADAuthBiometric.fingerprint) + XCTAssertNil(error) + } + + func testGetEnrolledBiometricsWithoutEnrolledHardware() { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + stubAuthContext.expectBiometrics = true + stubAuthContext.canEvaluateError = NSError( + domain: "error", code: LAError.biometryNotEnrolled.rawValue) + + var error: FlutterError? + let result = plugin.getEnrolledBiometricsWithError(&error) + XCTAssertTrue(result!.isEmpty) + XCTAssertNil(error) + } + + func testIsDeviceSupportedHandlesSupported() { + let stubAuthContext = StubAuthContext() + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + var error: FlutterError? + let result = plugin.isDeviceSupportedWithError(&error) + XCTAssertTrue(result!.boolValue) + XCTAssertNil(error) + } + + func testIsDeviceSupportedHandlesUnsupported() { + let stubAuthContext = StubAuthContext() + // An arbitrary error to cause canEvaluatePolicy to return false. + stubAuthContext.canEvaluateError = NSError(domain: "error", code: 1) + let plugin = FLALocalAuthPlugin( + contextFactory: StubAuthContextFactory(contexts: [stubAuthContext])) + + var error: FlutterError? + let result = plugin.isDeviceSupportedWithError(&error) + XCTAssertFalse(result!.boolValue) + XCTAssertNil(error) + } + + // Creates an FLADAuthStrings with placeholder values. + func createAuthStrings() -> FLADAuthStrings { + return FLADAuthStrings.make( + withReason: "a reason", lockOut: "locked out", goToSettingsButton: "Go To Settings", + goToSettingsDescription: "Settings", cancelButton: "Cancel", localizedFallbackTitle: nil) + } + +} diff --git a/packages/local_auth/local_auth_darwin/example/ios/Runner.xcodeproj/project.pbxproj b/packages/local_auth/local_auth_darwin/example/ios/Runner.xcodeproj/project.pbxproj index 8ef2595364c7..8645071ff164 100644 --- a/packages/local_auth/local_auth_darwin/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/local_auth/local_auth_darwin/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,13 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ 0CCCD07A2CE24E13C9C1EEA4 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3398D2E426164AD8005A052F /* FLALocalAuthPluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3398D2E326164AD8005A052F /* FLALocalAuthPluginTests.m */; }; + 338A5F9D2BFBA45B00DF0C4E /* FLALocalAuthPluginTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 338A5F9C2BFBA45B00DF0C4E /* FLALocalAuthPluginTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 691CB38B382734AF80FBCA4C /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADBFA21B380E07A3A585383D /* libPods-RunnerTests.a */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; @@ -45,11 +45,11 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 338A5F9C2BFBA45B00DF0C4E /* FLALocalAuthPluginTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FLALocalAuthPluginTests.swift; path = ../../../darwin/Tests/FLALocalAuthPluginTests.swift; sourceTree = ""; }; 3398D2CD26163948005A052F /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3398D2D126163948005A052F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3398D2DC261649CD005A052F /* liblocal_auth.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = liblocal_auth.a; sourceTree = BUILT_PRODUCTS_DIR; }; 3398D2DF26164A03005A052F /* liblocal_auth.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = liblocal_auth.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 3398D2E326164AD8005A052F /* FLALocalAuthPluginTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FLALocalAuthPluginTests.m; path = ../../darwin/Tests/FLALocalAuthPluginTests.m; sourceTree = SOURCE_ROOT; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 658CDD04B21E4EA92F8EF229 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -93,7 +93,7 @@ 33BF11D226680B2E002967F3 /* RunnerTests */ = { isa = PBXGroup; children = ( - 3398D2E326164AD8005A052F /* FLALocalAuthPluginTests.m */, + 338A5F9C2BFBA45B00DF0C4E /* FLALocalAuthPluginTests.swift */, 3398D2D126163948005A052F /* Info.plist */, ); path = RunnerTests; @@ -232,6 +232,7 @@ TargetAttributes = { 3398D2CC26163948005A052F = { CreatedOnToolsVersion = 12.4; + LastSwiftMigration = 1510; ProvisioningStyle = Automatic; TestTargetID = 97C146ED1CF9000F007C117D; }; @@ -377,7 +378,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3398D2E426164AD8005A052F /* FLALocalAuthPluginTests.m in Sources */, + 338A5F9D2BFBA45B00DF0C4E /* FLALocalAuthPluginTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -428,6 +429,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -444,6 +446,8 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; @@ -456,6 +460,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; @@ -471,6 +476,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; diff --git a/script/tool/lib/src/common/package_state_utils.dart b/script/tool/lib/src/common/package_state_utils.dart index 1d09052f5e61..316251d4f01a 100644 --- a/script/tool/lib/src/common/package_state_utils.dart +++ b/script/tool/lib/src/common/package_state_utils.dart @@ -127,6 +127,7 @@ bool _isTestChange(List pathComponents) { pathComponents.contains('androidTest') || pathComponents.contains('RunnerTests') || pathComponents.contains('RunnerUITests') || + pathComponents.contains('Tests') || pathComponents.last == 'dart_test.yaml' || // Pigeon's custom platform tests. pathComponents.first == 'platform_tests'; diff --git a/script/tool/lib/src/podspec_check_command.dart b/script/tool/lib/src/podspec_check_command.dart index 96df0db31d90..4f1b5e0dfa55 100644 --- a/script/tool/lib/src/podspec_check_command.dart +++ b/script/tool/lib/src/podspec_check_command.dart @@ -181,6 +181,12 @@ class PodspecCheckCommand extends PackageLoopingCommand { if (relativePath.startsWith('example/')) { return false; } + // Ignore test code. + if (relativePath.contains('/Tests/') || + relativePath.contains('/RunnerTests/') || + relativePath.contains('/RunnerUITests/')) { + return false; + } final String filePath = entity.path; return filePath != iosSwiftPackageManifestPath && filePath != darwinSwiftPackageManifestPath && diff --git a/script/tool/test/podspec_check_command_test.dart b/script/tool/test/podspec_check_command_test.dart index 5ffa4bf2cb13..e416a488fc74 100644 --- a/script/tool/test/podspec_check_command_test.dart +++ b/script/tool/test/podspec_check_command_test.dart @@ -405,6 +405,31 @@ void main() { )); }); + test('does not require the search paths workaround for Swift tests', + () async { + final RepositoryPackage plugin = createFakePlugin( + 'plugin1', + packagesDir, + extraFiles: [ + 'darwin/Tests/SharedTest.swift', + 'example/ios/RunnerTests/UnitTest.swift', + 'example/ios/RunnerUITests/UITest.swift', + ], + ); + _writeFakePodspec(plugin, 'ios'); + + final List output = + await runCapturingPrint(runner, ['podspec-check']); + + expect( + output, + containsAllInOrder( + [ + contains('Ran for 1 package(s)'), + ], + )); + }); + test( 'does not require the search paths workaround for darwin Package.swift', () async { diff --git a/script/tool/test/version_check_command_test.dart b/script/tool/test/version_check_command_test.dart index 0a30c4dde401..489a43c227dd 100644 --- a/script/tool/test/version_check_command_test.dart +++ b/script/tool/test/version_check_command_test.dart @@ -876,6 +876,7 @@ packages/plugin/example/android/lint-baseline.xml packages/plugin/example/android/src/androidTest/foo/bar/FooTest.java packages/plugin/example/ios/RunnerTests/Foo.m packages/plugin/example/ios/RunnerUITests/info.plist +packages/plugin/darwin/Tests/Foo.swift packages/plugin/analysis_options.yaml packages/plugin/CHANGELOG.md ''')),