Skip to content

Commit 6525441

Browse files
[local_auth] Convert native unit tests to Swift (flutter#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 flutter/flutter#119104
1 parent cdd8e2d commit 6525441

File tree

7 files changed

+441
-454
lines changed

7 files changed

+441
-454
lines changed

packages/local_auth/local_auth_darwin/darwin/Tests/FLALocalAuthPluginTests.m

Lines changed: 0 additions & 449 deletions
This file was deleted.
Lines changed: 397 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,397 @@
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
6+
import XCTest
7+
8+
@testable import local_auth_darwin
9+
10+
// Set a long timeout to avoid flake due to slow CI.
11+
private let timeout: TimeInterval = 30.0
12+
13+
/// A context factory that returns preset contexts.
14+
final class StubAuthContextFactory: NSObject, FLADAuthContextFactory {
15+
var contexts: [FLADAuthContext]
16+
init(contexts: [FLADAuthContext]) {
17+
self.contexts = contexts
18+
}
19+
20+
func createAuthContext() -> FLADAuthContext {
21+
XCTAssert(self.contexts.count > 0, "Insufficient test contexts provided")
22+
return self.contexts.removeFirst()
23+
}
24+
}
25+
26+
final class StubAuthContext: NSObject, FLADAuthContext {
27+
/// Whether calls to this stub are expected to be for biometric authentication.
28+
///
29+
/// While this object could be set up to return different values for different policies, in
30+
/// practice only one policy is needed by any given test, so this just allows asserting that the
31+
/// code is calling with the intended policy.
32+
var expectBiometrics = false
33+
/// The error to return from canEvaluatePolicy.
34+
var canEvaluateError: NSError?
35+
/// The value to return from evaluatePolicy:error:.
36+
var evaluateResponse = false
37+
/// The error to return from evaluatePolicy:error:.
38+
var evaluateError: NSError?
39+
40+
// Overridden as read-write to allow stubbing.
41+
var biometryType: LABiometryType = .none
42+
var localizedFallbackTitle: String?
43+
44+
func canEvaluatePolicy(_ policy: LAPolicy) throws {
45+
XCTAssertEqual(
46+
policy,
47+
expectBiometrics
48+
? LAPolicy.deviceOwnerAuthenticationWithBiometrics
49+
: LAPolicy.deviceOwnerAuthentication)
50+
if let canEvaluateError = canEvaluateError {
51+
throw canEvaluateError
52+
}
53+
}
54+
55+
func evaluatePolicy(
56+
_ policy: LAPolicy, localizedReason: String, reply: @escaping (Bool, Error?) -> Void
57+
) {
58+
XCTAssertEqual(
59+
policy,
60+
expectBiometrics
61+
? LAPolicy.deviceOwnerAuthenticationWithBiometrics
62+
: LAPolicy.deviceOwnerAuthentication)
63+
// evaluatePolicy:localizedReason:reply: calls back on an internal queue, which is not
64+
// guaranteed to be on the main thread. Ensure that's handled correctly by calling back on
65+
// a background thread.
66+
DispatchQueue.global(qos: .background).async {
67+
reply(self.evaluateResponse, self.evaluateError)
68+
}
69+
}
70+
}
71+
72+
// MARK: -
73+
74+
class FLALocalAuthPluginTests: XCTestCase {
75+
76+
func testSuccessfullAuthWithBiometrics() throws {
77+
let stubAuthContext = StubAuthContext()
78+
let plugin = FLALocalAuthPlugin(
79+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
80+
81+
let strings = createAuthStrings()
82+
stubAuthContext.expectBiometrics = true
83+
stubAuthContext.evaluateResponse = true
84+
let expectation = expectation(description: "Result is called")
85+
plugin.authenticate(
86+
with: FLADAuthOptions.make(
87+
withBiometricOnly: true,
88+
sticky: false,
89+
useErrorDialogs: false),
90+
strings: strings
91+
) { resultDetails, error in
92+
XCTAssertTrue(Thread.isMainThread)
93+
XCTAssertEqual(resultDetails?.result, FLADAuthResult.success)
94+
XCTAssertNil(error)
95+
expectation.fulfill()
96+
}
97+
self.waitForExpectations(timeout: timeout)
98+
}
99+
100+
func testSuccessfullAuthWithoutBiometrics() {
101+
let stubAuthContext = StubAuthContext()
102+
let plugin = FLALocalAuthPlugin(
103+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
104+
105+
let strings = createAuthStrings()
106+
stubAuthContext.evaluateResponse = true
107+
108+
let expectation = expectation(description: "Result is called")
109+
plugin.authenticate(
110+
with: FLADAuthOptions.make(
111+
withBiometricOnly: false,
112+
sticky: false,
113+
useErrorDialogs: false),
114+
strings: strings
115+
) { resultDetails, error in
116+
XCTAssertTrue(Thread.isMainThread)
117+
XCTAssertEqual(resultDetails?.result, FLADAuthResult.success)
118+
XCTAssertNil(error)
119+
expectation.fulfill()
120+
}
121+
self.waitForExpectations(timeout: timeout)
122+
}
123+
124+
func testFailedAuthWithBiometrics() {
125+
let stubAuthContext = StubAuthContext()
126+
let plugin = FLALocalAuthPlugin(
127+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
128+
129+
let strings = createAuthStrings()
130+
stubAuthContext.expectBiometrics = true
131+
stubAuthContext.evaluateError = NSError(
132+
domain: "error", code: LAError.authenticationFailed.rawValue)
133+
134+
let expectation = expectation(description: "Result is called")
135+
plugin.authenticate(
136+
with: FLADAuthOptions.make(
137+
withBiometricOnly: true,
138+
sticky: false,
139+
useErrorDialogs: false),
140+
strings: strings
141+
) { resultDetails, error in
142+
XCTAssertTrue(Thread.isMainThread)
143+
// TODO(stuartmorgan): Fix this; this was the pre-Pigeon-migration
144+
// behavior, so is preserved as part of the migration, but a failed
145+
// authentication should return failure, not an error that results in a
146+
// PlatformException.
147+
XCTAssertEqual(resultDetails?.result, FLADAuthResult.errorNotAvailable)
148+
XCTAssertNil(error)
149+
expectation.fulfill()
150+
}
151+
self.waitForExpectations(timeout: timeout)
152+
}
153+
154+
func testFailedWithUnknownErrorCode() {
155+
let stubAuthContext = StubAuthContext()
156+
let plugin = FLALocalAuthPlugin(
157+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
158+
159+
let strings = createAuthStrings()
160+
stubAuthContext.evaluateError = NSError(domain: "error", code: 99)
161+
162+
let expectation = expectation(description: "Result is called")
163+
plugin.authenticate(
164+
with: FLADAuthOptions.make(
165+
withBiometricOnly: false,
166+
sticky: false,
167+
useErrorDialogs: false),
168+
strings: strings
169+
) { resultDetails, error in
170+
XCTAssertTrue(Thread.isMainThread)
171+
XCTAssertEqual(resultDetails?.result, FLADAuthResult.errorNotAvailable)
172+
XCTAssertNil(error)
173+
expectation.fulfill()
174+
}
175+
self.waitForExpectations(timeout: timeout)
176+
}
177+
178+
func testSystemCancelledWithoutStickyAuth() {
179+
let stubAuthContext = StubAuthContext()
180+
let plugin = FLALocalAuthPlugin(
181+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
182+
183+
let strings = createAuthStrings()
184+
stubAuthContext.evaluateError = NSError(domain: "error", code: LAError.systemCancel.rawValue)
185+
186+
let expectation = expectation(description: "Result is called")
187+
plugin.authenticate(
188+
with: FLADAuthOptions.make(
189+
withBiometricOnly: false,
190+
sticky: false,
191+
useErrorDialogs: false),
192+
strings: strings
193+
) { resultDetails, error in
194+
XCTAssertTrue(Thread.isMainThread)
195+
XCTAssertEqual(resultDetails?.result, FLADAuthResult.failure)
196+
XCTAssertNil(error)
197+
expectation.fulfill()
198+
}
199+
self.waitForExpectations(timeout: timeout)
200+
}
201+
202+
func testFailedAuthWithoutBiometrics() {
203+
let stubAuthContext = StubAuthContext()
204+
let plugin = FLALocalAuthPlugin(
205+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
206+
207+
let strings = createAuthStrings()
208+
stubAuthContext.evaluateError = NSError(
209+
domain: "error", code: LAError.authenticationFailed.rawValue)
210+
211+
let expectation = expectation(description: "Result is called")
212+
plugin.authenticate(
213+
with: FLADAuthOptions.make(
214+
withBiometricOnly: false,
215+
sticky: false,
216+
useErrorDialogs: false),
217+
strings: strings
218+
) { resultDetails, error in
219+
XCTAssertTrue(Thread.isMainThread)
220+
// TODO(stuartmorgan): Fix this; this was the pre-Pigeon-migration
221+
// behavior, so is preserved as part of the migration, but a failed
222+
// authentication should return failure, not an error that results in a
223+
// PlatformException.
224+
XCTAssertEqual(resultDetails?.result, FLADAuthResult.errorNotAvailable)
225+
XCTAssertNil(error)
226+
expectation.fulfill()
227+
}
228+
self.waitForExpectations(timeout: timeout)
229+
}
230+
231+
func testLocalizedFallbackTitle() {
232+
let stubAuthContext = StubAuthContext()
233+
let plugin = FLALocalAuthPlugin(
234+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
235+
236+
let strings = createAuthStrings()
237+
strings.localizedFallbackTitle = "a title"
238+
stubAuthContext.evaluateResponse = true
239+
240+
let expectation = expectation(description: "Result is called")
241+
plugin.authenticate(
242+
with: FLADAuthOptions.make(
243+
withBiometricOnly: false,
244+
sticky: false,
245+
useErrorDialogs: false),
246+
strings: strings
247+
) { resultDetails, error in
248+
XCTAssertEqual(
249+
stubAuthContext.localizedFallbackTitle,
250+
strings.localizedFallbackTitle)
251+
expectation.fulfill()
252+
}
253+
self.waitForExpectations(timeout: timeout)
254+
}
255+
256+
func testSkippedLocalizedFallbackTitle() {
257+
let stubAuthContext = StubAuthContext()
258+
let plugin = FLALocalAuthPlugin(
259+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
260+
261+
let strings = createAuthStrings()
262+
strings.localizedFallbackTitle = nil
263+
stubAuthContext.evaluateResponse = true
264+
265+
let expectation = expectation(description: "Result is called")
266+
plugin.authenticate(
267+
with: FLADAuthOptions.make(
268+
withBiometricOnly: false,
269+
sticky: false,
270+
useErrorDialogs: false),
271+
strings: strings
272+
) { resultDetails, error in
273+
XCTAssertNil(stubAuthContext.localizedFallbackTitle)
274+
expectation.fulfill()
275+
}
276+
self.waitForExpectations(timeout: timeout)
277+
}
278+
279+
func testDeviceSupportsBiometrics_withEnrolledHardware() {
280+
let stubAuthContext = StubAuthContext()
281+
let plugin = FLALocalAuthPlugin(
282+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
283+
284+
stubAuthContext.expectBiometrics = true
285+
286+
var error: FlutterError?
287+
let result = plugin.deviceCanSupportBiometricsWithError(&error)
288+
XCTAssertTrue(result!.boolValue)
289+
XCTAssertNil(error)
290+
}
291+
292+
func testDeviceSupportsBiometrics_withNonEnrolledHardware() {
293+
let stubAuthContext = StubAuthContext()
294+
let plugin = FLALocalAuthPlugin(
295+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
296+
297+
stubAuthContext.expectBiometrics = true
298+
stubAuthContext.canEvaluateError = NSError(
299+
domain: "error", code: LAError.biometryNotEnrolled.rawValue)
300+
301+
var error: FlutterError?
302+
let result = plugin.deviceCanSupportBiometricsWithError(&error)
303+
XCTAssertTrue(result!.boolValue)
304+
XCTAssertNil(error)
305+
}
306+
307+
func testDeviceSupportsBiometrics_withNoBiometricHardware() {
308+
let stubAuthContext = StubAuthContext()
309+
let plugin = FLALocalAuthPlugin(
310+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
311+
312+
stubAuthContext.expectBiometrics = true
313+
stubAuthContext.canEvaluateError = NSError(domain: "error", code: 0)
314+
315+
var error: FlutterError?
316+
let result = plugin.deviceCanSupportBiometricsWithError(&error)
317+
XCTAssertFalse(result!.boolValue)
318+
XCTAssertNil(error)
319+
}
320+
321+
func testGetEnrolledBiometricsWithFaceID() {
322+
let stubAuthContext = StubAuthContext()
323+
let plugin = FLALocalAuthPlugin(
324+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
325+
326+
stubAuthContext.expectBiometrics = true
327+
stubAuthContext.biometryType = .faceID
328+
329+
var error: FlutterError?
330+
let result = plugin.getEnrolledBiometricsWithError(&error)
331+
XCTAssertEqual(result!.count, 1)
332+
XCTAssertEqual(result![0].value, FLADAuthBiometric.face)
333+
XCTAssertNil(error)
334+
}
335+
336+
func testGetEnrolledBiometricsWithTouchID() {
337+
let stubAuthContext = StubAuthContext()
338+
let plugin = FLALocalAuthPlugin(
339+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
340+
341+
stubAuthContext.expectBiometrics = true
342+
stubAuthContext.biometryType = .touchID
343+
344+
var error: FlutterError?
345+
let result = plugin.getEnrolledBiometricsWithError(&error)
346+
XCTAssertEqual(result!.count, 1)
347+
XCTAssertEqual(result![0].value, FLADAuthBiometric.fingerprint)
348+
XCTAssertNil(error)
349+
}
350+
351+
func testGetEnrolledBiometricsWithoutEnrolledHardware() {
352+
let stubAuthContext = StubAuthContext()
353+
let plugin = FLALocalAuthPlugin(
354+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
355+
356+
stubAuthContext.expectBiometrics = true
357+
stubAuthContext.canEvaluateError = NSError(
358+
domain: "error", code: LAError.biometryNotEnrolled.rawValue)
359+
360+
var error: FlutterError?
361+
let result = plugin.getEnrolledBiometricsWithError(&error)
362+
XCTAssertTrue(result!.isEmpty)
363+
XCTAssertNil(error)
364+
}
365+
366+
func testIsDeviceSupportedHandlesSupported() {
367+
let stubAuthContext = StubAuthContext()
368+
let plugin = FLALocalAuthPlugin(
369+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
370+
371+
var error: FlutterError?
372+
let result = plugin.isDeviceSupportedWithError(&error)
373+
XCTAssertTrue(result!.boolValue)
374+
XCTAssertNil(error)
375+
}
376+
377+
func testIsDeviceSupportedHandlesUnsupported() {
378+
let stubAuthContext = StubAuthContext()
379+
// An arbitrary error to cause canEvaluatePolicy to return false.
380+
stubAuthContext.canEvaluateError = NSError(domain: "error", code: 1)
381+
let plugin = FLALocalAuthPlugin(
382+
contextFactory: StubAuthContextFactory(contexts: [stubAuthContext]))
383+
384+
var error: FlutterError?
385+
let result = plugin.isDeviceSupportedWithError(&error)
386+
XCTAssertFalse(result!.boolValue)
387+
XCTAssertNil(error)
388+
}
389+
390+
// Creates an FLADAuthStrings with placeholder values.
391+
func createAuthStrings() -> FLADAuthStrings {
392+
return FLADAuthStrings.make(
393+
withReason: "a reason", lockOut: "locked out", goToSettingsButton: "Go To Settings",
394+
goToSettingsDescription: "Settings", cancelButton: "Cancel", localizedFallbackTitle: nil)
395+
}
396+
397+
}

0 commit comments

Comments
 (0)