Skip to content

Commit c85111c

Browse files
[local_auth] Fix cancel handling on Android (#4120)
Fixes a regression introduced during the Pigeon conversion where a canceled auth returned success instead of failure. Adds initial unit test coverage of `AuthenticationHelper`, which was previously untested, including coverage of the incorrect code path. Fixes #127732
1 parent 4a5cc3e commit c85111c

File tree

4 files changed

+198
-3
lines changed

4 files changed

+198
-3
lines changed

packages/local_auth/local_auth_android/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 1.0.29
22

3+
* Fixes a regression in 1.0.23 that caused canceled auths to return success.
34
* Updates minimum supported SDK version to Flutter 3.3/Dart 2.18.
45

56
## 1.0.28

packages/local_auth/local_auth_android/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ public void onAuthenticationError(int errorCode, @NonNull CharSequence errString
155155
if (activityPaused && isAuthSticky) {
156156
return;
157157
} else {
158-
completionHandler.complete(Messages.AuthResult.SUCCESS);
158+
completionHandler.complete(Messages.AuthResult.FAILURE);
159159
}
160160
break;
161161
default:
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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+
package io.flutter.plugins.localauth;
6+
7+
import static org.mockito.Mockito.mock;
8+
import static org.mockito.Mockito.verify;
9+
import static org.mockito.Mockito.when;
10+
11+
import android.app.Application;
12+
import android.content.Context;
13+
import androidx.biometric.BiometricPrompt;
14+
import androidx.fragment.app.FragmentActivity;
15+
import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler;
16+
import io.flutter.plugins.localauth.Messages.AuthOptions;
17+
import io.flutter.plugins.localauth.Messages.AuthResult;
18+
import io.flutter.plugins.localauth.Messages.AuthStrings;
19+
import org.junit.Test;
20+
import org.junit.runner.RunWith;
21+
import org.robolectric.RobolectricTestRunner;
22+
23+
// TODO(stuartmorgan): Add injectable BiometricPrompt factory, and AlertDialog factor, and add
24+
// testing of the rest of the flows.
25+
26+
@RunWith(RobolectricTestRunner.class)
27+
public class AuthenticationHelperTest {
28+
static final AuthStrings dummyStrings =
29+
new AuthStrings.Builder()
30+
.setReason("a reason")
31+
.setBiometricHint("a hint")
32+
.setBiometricNotRecognized("biometric not recognized")
33+
.setBiometricRequiredTitle("biometric required")
34+
.setCancelButton("cancel")
35+
.setDeviceCredentialsRequiredTitle("credentials required")
36+
.setDeviceCredentialsSetupDescription("credentials setup description")
37+
.setGoToSettingsButton("go")
38+
.setGoToSettingsDescription("go to settings description")
39+
.setSignInTitle("sign in")
40+
.build();
41+
42+
static final AuthOptions defaultOptions =
43+
new AuthOptions.Builder()
44+
.setBiometricOnly(false)
45+
.setSensitiveTransaction(false)
46+
.setSticky(false)
47+
.setUseErrorDialgs(false)
48+
.build();
49+
50+
@Test
51+
public void onAuthenticationError_withoutDialogs_returnsNotAvailableForNoCredential() {
52+
final AuthCompletionHandler handler = mock(AuthCompletionHandler.class);
53+
final AuthenticationHelper helper =
54+
new AuthenticationHelper(
55+
null,
56+
buildMockActivityWithContext(mock(FragmentActivity.class)),
57+
defaultOptions,
58+
dummyStrings,
59+
handler,
60+
true);
61+
62+
helper.onAuthenticationError(BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL, "");
63+
64+
verify(handler).complete(AuthResult.ERROR_NOT_AVAILABLE);
65+
}
66+
67+
@Test
68+
public void onAuthenticationError_withoutDialogs_returnsNotEnrolledForNoBiometrics() {
69+
final AuthCompletionHandler handler = mock(AuthCompletionHandler.class);
70+
final AuthenticationHelper helper =
71+
new AuthenticationHelper(
72+
null,
73+
buildMockActivityWithContext(mock(FragmentActivity.class)),
74+
defaultOptions,
75+
dummyStrings,
76+
handler,
77+
true);
78+
79+
helper.onAuthenticationError(BiometricPrompt.ERROR_NO_BIOMETRICS, "");
80+
81+
verify(handler).complete(AuthResult.ERROR_NOT_ENROLLED);
82+
}
83+
84+
@Test
85+
public void onAuthenticationError_returnsNotAvailableForHardwareUnavailable() {
86+
final AuthCompletionHandler handler = mock(AuthCompletionHandler.class);
87+
final AuthenticationHelper helper =
88+
new AuthenticationHelper(
89+
null,
90+
buildMockActivityWithContext(mock(FragmentActivity.class)),
91+
defaultOptions,
92+
dummyStrings,
93+
handler,
94+
true);
95+
96+
helper.onAuthenticationError(BiometricPrompt.ERROR_HW_UNAVAILABLE, "");
97+
98+
verify(handler).complete(AuthResult.ERROR_NOT_AVAILABLE);
99+
}
100+
101+
@Test
102+
public void onAuthenticationError_returnsNotAvailableForHardwareNotPresent() {
103+
final AuthCompletionHandler handler = mock(AuthCompletionHandler.class);
104+
final AuthenticationHelper helper =
105+
new AuthenticationHelper(
106+
null,
107+
buildMockActivityWithContext(mock(FragmentActivity.class)),
108+
defaultOptions,
109+
dummyStrings,
110+
handler,
111+
true);
112+
113+
helper.onAuthenticationError(BiometricPrompt.ERROR_HW_NOT_PRESENT, "");
114+
115+
verify(handler).complete(AuthResult.ERROR_NOT_AVAILABLE);
116+
}
117+
118+
@Test
119+
public void onAuthenticationError_returnsTemporaryLockoutForLockout() {
120+
final AuthCompletionHandler handler = mock(AuthCompletionHandler.class);
121+
final AuthenticationHelper helper =
122+
new AuthenticationHelper(
123+
null,
124+
buildMockActivityWithContext(mock(FragmentActivity.class)),
125+
defaultOptions,
126+
dummyStrings,
127+
handler,
128+
true);
129+
130+
helper.onAuthenticationError(BiometricPrompt.ERROR_LOCKOUT, "");
131+
132+
verify(handler).complete(AuthResult.ERROR_LOCKED_OUT_TEMPORARILY);
133+
}
134+
135+
@Test
136+
public void onAuthenticationError_returnsPermanentLockoutForLockoutPermanent() {
137+
final AuthCompletionHandler handler = mock(AuthCompletionHandler.class);
138+
final AuthenticationHelper helper =
139+
new AuthenticationHelper(
140+
null,
141+
buildMockActivityWithContext(mock(FragmentActivity.class)),
142+
defaultOptions,
143+
dummyStrings,
144+
handler,
145+
true);
146+
147+
helper.onAuthenticationError(BiometricPrompt.ERROR_LOCKOUT_PERMANENT, "");
148+
149+
verify(handler).complete(AuthResult.ERROR_LOCKED_OUT_PERMANENTLY);
150+
}
151+
152+
@Test
153+
public void onAuthenticationError_withoutSticky_returnsFailureForCanceled() {
154+
final AuthCompletionHandler handler = mock(AuthCompletionHandler.class);
155+
final AuthenticationHelper helper =
156+
new AuthenticationHelper(
157+
null,
158+
buildMockActivityWithContext(mock(FragmentActivity.class)),
159+
defaultOptions,
160+
dummyStrings,
161+
handler,
162+
true);
163+
164+
helper.onAuthenticationError(BiometricPrompt.ERROR_CANCELED, "");
165+
166+
verify(handler).complete(AuthResult.FAILURE);
167+
}
168+
169+
@Test
170+
public void onAuthenticationError_withoutSticky_returnsFailureForOtherCases() {
171+
final AuthCompletionHandler handler = mock(AuthCompletionHandler.class);
172+
final AuthenticationHelper helper =
173+
new AuthenticationHelper(
174+
null,
175+
buildMockActivityWithContext(mock(FragmentActivity.class)),
176+
defaultOptions,
177+
dummyStrings,
178+
handler,
179+
true);
180+
181+
helper.onAuthenticationError(BiometricPrompt.ERROR_VENDOR, "");
182+
183+
verify(handler).complete(AuthResult.FAILURE);
184+
}
185+
186+
private FragmentActivity buildMockActivityWithContext(FragmentActivity mockActivity) {
187+
final Application mockApplication = mock(Application.class);
188+
final Context mockContext = mock(Context.class);
189+
when(mockActivity.getBaseContext()).thenReturn(mockContext);
190+
when(mockActivity.getApplicationContext()).thenReturn(mockContext);
191+
when(mockActivity.getApplication()).thenReturn(mockApplication);
192+
return mockActivity;
193+
}
194+
}

packages/local_auth/local_auth_android/pubspec.yaml

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

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

0 commit comments

Comments
 (0)