blob: 7279a3c49af2a9224e5651471d5d11380eb037b8 [file] [log] [blame]
// 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.
package io.flutter.plugins.localauth;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Activity;
import android.app.KeyguardManager;
import android.app.NativeActivity;
import android.content.Context;
import androidx.biometric.BiometricManager;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.Lifecycle;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricTestRunner.class)
public class LocalAuthTest {
@Test
public void authenticate_returnsErrorWhenAuthInProgress() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
plugin.authInProgress.set(true);
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
plugin.onMethodCall(new MethodCall("authenticate", null), mockResult);
verify(mockResult).error("auth_in_progress", "Authentication in progress", null);
}
@Test
public void authenticate_returnsErrorWithNoForegroundActivity() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
plugin.onMethodCall(new MethodCall("authenticate", null), mockResult);
verify(mockResult)
.error("no_activity", "local_auth plugin requires a foreground activity", null);
}
@Test
public void authenticate_returnsErrorWhenActivityNotFragmentActivity() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
setPluginActivity(plugin, buildMockActivityWithContext(mock(NativeActivity.class)));
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
plugin.onMethodCall(new MethodCall("authenticate", null), mockResult);
verify(mockResult)
.error(
"no_fragment_activity",
"local_auth plugin requires activity to be a FragmentActivity.",
null);
}
@Test
public void authenticate_returnsErrorWhenDeviceNotSupported() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
setPluginActivity(plugin, buildMockActivityWithContext(mock(FragmentActivity.class)));
plugin.onMethodCall(new MethodCall("authenticate", null), mockResult);
assertFalse(plugin.authInProgress.get());
verify(mockResult).error("NotAvailable", "Required security features not enabled", null);
}
@Test
public void authenticate_properlyConfiguresBiometricOnlyAuthenticationRequest() {
final LocalAuthPlugin plugin = spy(new LocalAuthPlugin());
setPluginActivity(plugin, buildMockActivityWithContext(mock(FragmentActivity.class)));
when(plugin.isDeviceSupported()).thenReturn(true);
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL))
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
plugin.setBiometricManager(mockBiometricManager);
ArgumentCaptor<Boolean> allowCredentialsCaptor = ArgumentCaptor.forClass(Boolean.class);
doNothing()
.when(plugin)
.sendAuthenticationRequest(
any(MethodCall.class),
any(AuthCompletionHandler.class),
allowCredentialsCaptor.capture());
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("biometricOnly", true);
plugin.onMethodCall(new MethodCall("authenticate", arguments), mockResult);
assertFalse(allowCredentialsCaptor.getValue());
}
@Test
@Config(sdk = 30)
public void authenticate_properlyConfiguresBiometricAndDeviceCredentialAuthenticationRequest() {
final LocalAuthPlugin plugin = spy(new LocalAuthPlugin());
setPluginActivity(plugin, buildMockActivityWithContext(mock(FragmentActivity.class)));
when(plugin.isDeviceSupported()).thenReturn(true);
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL))
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
plugin.setBiometricManager(mockBiometricManager);
ArgumentCaptor<Boolean> allowCredentialsCaptor = ArgumentCaptor.forClass(Boolean.class);
doNothing()
.when(plugin)
.sendAuthenticationRequest(
any(MethodCall.class),
any(AuthCompletionHandler.class),
allowCredentialsCaptor.capture());
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("biometricOnly", false);
plugin.onMethodCall(new MethodCall("authenticate", arguments), mockResult);
assertTrue(allowCredentialsCaptor.getValue());
}
@Test
@Config(sdk = 30)
public void authenticate_properlyConfiguresDeviceCredentialOnlyAuthenticationRequest() {
final LocalAuthPlugin plugin = spy(new LocalAuthPlugin());
setPluginActivity(plugin, buildMockActivityWithContext(mock(FragmentActivity.class)));
when(plugin.isDeviceSupported()).thenReturn(true);
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL))
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
plugin.setBiometricManager(mockBiometricManager);
ArgumentCaptor<Boolean> allowCredentialsCaptor = ArgumentCaptor.forClass(Boolean.class);
doNothing()
.when(plugin)
.sendAuthenticationRequest(
any(MethodCall.class),
any(AuthCompletionHandler.class),
allowCredentialsCaptor.capture());
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("biometricOnly", false);
plugin.onMethodCall(new MethodCall("authenticate", arguments), mockResult);
assertTrue(allowCredentialsCaptor.getValue());
}
@Test
public void isDeviceSupportedReturnsFalse() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
plugin.onMethodCall(new MethodCall("isDeviceSupported", null), mockResult);
verify(mockResult).success(false);
}
@Test
public void deviceSupportsBiometrics_returnsTrueForPresentNonEnrolledBiometrics() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
plugin.setBiometricManager(mockBiometricManager);
plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult);
verify(mockResult).success(true);
}
@Test
public void deviceSupportsBiometrics_returnsTrueForPresentEnrolledBiometrics() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
plugin.setBiometricManager(mockBiometricManager);
plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult);
verify(mockResult).success(true);
}
@Test
public void deviceSupportsBiometrics_returnsFalseForNoBiometricHardware() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE);
plugin.setBiometricManager(mockBiometricManager);
plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult);
verify(mockResult).success(false);
}
@Test
public void deviceSupportsBiometrics_returnsFalseForNullBiometricManager() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
plugin.setBiometricManager(null);
plugin.onMethodCall(new MethodCall("deviceSupportsBiometrics", null), mockResult);
verify(mockResult).success(false);
}
@Test
public void onDetachedFromActivity_ShouldReleaseActivity() {
final Activity mockActivity = mock(Activity.class);
final ActivityPluginBinding mockActivityBinding = mock(ActivityPluginBinding.class);
when(mockActivityBinding.getActivity()).thenReturn(mockActivity);
Context mockContext = mock(Context.class);
when(mockActivity.getBaseContext()).thenReturn(mockContext);
when(mockActivity.getApplicationContext()).thenReturn(mockContext);
final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class);
when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference);
final Lifecycle mockLifecycle = mock(Lifecycle.class);
when(mockLifecycleReference.getLifecycle()).thenReturn(mockLifecycle);
final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class);
final FlutterEngine mockFlutterEngine = mock(FlutterEngine.class);
when(mockPluginBinding.getFlutterEngine()).thenReturn(mockFlutterEngine);
DartExecutor mockDartExecutor = mock(DartExecutor.class);
when(mockFlutterEngine.getDartExecutor()).thenReturn(mockDartExecutor);
final LocalAuthPlugin plugin = new LocalAuthPlugin();
plugin.onAttachedToEngine(mockPluginBinding);
plugin.onAttachedToActivity(mockActivityBinding);
assertNotNull(plugin.getActivity());
plugin.onDetachedFromActivity();
assertNull(plugin.getActivity());
}
@Test
public void getEnrolledBiometrics_shouldReturnError_whenNoActivity() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
verify(mockResult)
.error("no_activity", "local_auth plugin requires a foreground activity", null);
}
@Test
public void getEnrolledBiometrics_shouldReturnError_whenFinishingActivity() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
final Activity mockActivity = buildMockActivityWithContext(mock(Activity.class));
when(mockActivity.isFinishing()).thenReturn(true);
setPluginActivity(plugin, mockActivity);
plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
verify(mockResult)
.error("no_activity", "local_auth plugin requires a foreground activity", null);
}
@Test
public void getEnrolledBiometrics_shouldReturnEmptyList_withoutHardwarePresent() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
setPluginActivity(plugin, buildMockActivityWithContext(mock(Activity.class)));
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
when(mockBiometricManager.canAuthenticate(anyInt()))
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE);
plugin.setBiometricManager(mockBiometricManager);
plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
verify(mockResult).success(Collections.emptyList());
}
@Test
public void getEnrolledBiometrics_shouldReturnEmptyList_withNoMethodsEnrolled() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
setPluginActivity(plugin, buildMockActivityWithContext(mock(Activity.class)));
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
when(mockBiometricManager.canAuthenticate(anyInt()))
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
plugin.setBiometricManager(mockBiometricManager);
plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
verify(mockResult).success(Collections.emptyList());
}
@Test
public void getEnrolledBiometrics_shouldOnlyAddEnrolledBiometrics() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
setPluginActivity(plugin, buildMockActivityWithContext(mock(Activity.class)));
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG))
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
plugin.setBiometricManager(mockBiometricManager);
plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
verify(mockResult)
.success(
new ArrayList<String>() {
{
add("weak");
}
});
}
@Test
public void getEnrolledBiometrics_shouldAddStrongBiometrics() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
setPluginActivity(plugin, buildMockActivityWithContext(mock(Activity.class)));
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG))
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
plugin.setBiometricManager(mockBiometricManager);
plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
verify(mockResult)
.success(
new ArrayList<String>() {
{
add("weak");
add("strong");
}
});
}
@Test
@Config(sdk = 22)
public void isDeviceSecure_returnsFalseOnBelowApi23() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
assertFalse(plugin.isDeviceSecure());
}
@Test
@Config(sdk = 23)
public void isDeviceSecure_returnsTrueIfDeviceIsSecure() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
KeyguardManager mockKeyguardManager = mock(KeyguardManager.class);
plugin.setKeyguardManager(mockKeyguardManager);
when(mockKeyguardManager.isDeviceSecure()).thenReturn(true);
assertTrue(plugin.isDeviceSecure());
when(mockKeyguardManager.isDeviceSecure()).thenReturn(false);
assertFalse(plugin.isDeviceSecure());
}
@Test
@Config(sdk = 30)
public void
canAuthenticateWithDeviceCredential_returnsTrueIfHasBiometricManagerSupportAboveApi30() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
plugin.setBiometricManager(mockBiometricManager);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL))
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
assertTrue(plugin.canAuthenticateWithDeviceCredential());
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL))
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
assertFalse(plugin.canAuthenticateWithDeviceCredential());
}
private Activity buildMockActivityWithContext(Activity mockActivity) {
final Context mockContext = mock(Context.class);
when(mockActivity.getBaseContext()).thenReturn(mockContext);
when(mockActivity.getApplicationContext()).thenReturn(mockContext);
return mockActivity;
}
private void setPluginActivity(LocalAuthPlugin plugin, Activity activity) {
final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class);
final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class);
final ActivityPluginBinding mockActivityBinding = mock(ActivityPluginBinding.class);
final FlutterEngine mockFlutterEngine = mock(FlutterEngine.class);
final DartExecutor mockDartExecutor = mock(DartExecutor.class);
when(mockPluginBinding.getFlutterEngine()).thenReturn(mockFlutterEngine);
when(mockFlutterEngine.getDartExecutor()).thenReturn(mockDartExecutor);
when(mockActivityBinding.getActivity()).thenReturn(activity);
when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference);
plugin.onAttachedToEngine(mockPluginBinding);
plugin.onAttachedToActivity(mockActivityBinding);
}
}