chromium/chrome/browser/device_reauth/win/device_authenticator_win_unittest.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/device_reauth/win/device_authenticator_win.h"

#include <memory>
#include <string>
#include <utility>

#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "chrome/browser/device_reauth/chrome_device_authenticator_factory.h"
#include "chrome/test/base/scoped_testing_local_state.h"
#include "chrome/test/base/testing_browser_process.h"
#include "components/device_reauth/device_authenticator.h"
#include "components/device_reauth/device_reauth_metrics_util.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

using device_reauth::DeviceAuthenticator;
using device_reauth::ReauthResult;
using testing::_;
using testing::Return;

constexpr base::TimeDelta kAuthValidityPeriod = base::Seconds(60);
constexpr char kHistogramName[] =
    "PasswordManager.ReauthToAccessPasswordInSettings";

class MockSystemAuthenticator : public AuthenticatorWinInterface {
 public:
  MOCK_METHOD(void,
              AuthenticateUser,
              (const std::u16string& message,
               base::OnceCallback<void(bool)> callback),
              (override));
  MOCK_METHOD(void,
              CheckIfBiometricsAvailable,
              (AvailabilityCallback callback),
              (override));
  MOCK_METHOD(bool, CanAuthenticateWithScreenLock, (), (override));
};

class DeviceAuthenticatorWinTest : public testing::Test {
 public:
  DeviceAuthenticatorWinTest()
      : testing_local_state_(TestingBrowserProcess::GetGlobal()),
        device_authenticator_params_(
            kAuthValidityPeriod,
            device_reauth::DeviceAuthSource::kPasswordManager,
            kHistogramName) {}
  void SetUp() override {
    std::unique_ptr<MockSystemAuthenticator> system_authenticator =
        std::make_unique<MockSystemAuthenticator>();
    system_authenticator_ = system_authenticator.get();
    authenticator_ = std::make_unique<DeviceAuthenticatorWin>(
        std::move(system_authenticator), &proxy_, device_authenticator_params_);
  }

  DeviceAuthenticatorWin* authenticator() { return authenticator_.get(); }

  MockSystemAuthenticator& system_authenticator() {
    return *system_authenticator_;
  }

  base::test::TaskEnvironment& task_environment() { return task_environment_; }

  ScopedTestingLocalState& local_state() { return testing_local_state_; }

  base::HistogramTester& histogram_tester() { return histogram_tester_; }

  void ExpectAuthenticationAndSetResult(bool result) {
    EXPECT_CALL(system_authenticator(), AuthenticateUser)
        .WillOnce(testing::WithArg<1>([result](auto callback) {
          base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
              FROM_HERE,
              base::BindOnce(std::move(callback), /*auth_succeeded=*/result));
        }));
  }

 private:
  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  DeviceAuthenticatorProxy proxy_;
  std::unique_ptr<DeviceAuthenticatorWin> authenticator_;
  ScopedTestingLocalState testing_local_state_;
  device_reauth::DeviceAuthParams device_authenticator_params_;
  base::HistogramTester histogram_tester_;

  // This is owned by the authenticator.
  raw_ptr<MockSystemAuthenticator> system_authenticator_ = nullptr;
};

// If time that passed since the last successful authentication is smaller than
// kAuthValidityPeriod, no reauthentication is needed.
TEST_F(DeviceAuthenticatorWinTest,
       NoReauthenticationIfLessThanAuthValidityPeriod) {
  ExpectAuthenticationAndSetResult(true);
  authenticator()->AuthenticateWithMessage(
      /*message=*/u"Chrome is trying to show passwords.", base::DoNothing());

  // The delay is smaller than kAuthValidityPeriod there shouldn't be
  // another prompt, so the auth should be reported as successful.
  task_environment().FastForwardBy(kAuthValidityPeriod / 2);

  EXPECT_CALL(system_authenticator(), AuthenticateUser).Times(0);
  base::MockCallback<DeviceAuthenticator::AuthenticateCallback> result_callback;
  EXPECT_CALL(result_callback, Run(/*auth_succeeded=*/true));
  authenticator()->AuthenticateWithMessage(
      /*message=*/u"Chrome is trying to show passwords.",
      result_callback.Get());

  task_environment().RunUntilIdle();
}

// If the time since the last reauthentication is greater than
// kAuthValidityPeriod reauthentication is needed.
TEST_F(DeviceAuthenticatorWinTest, ReauthenticationIfMoreThan60Seconds) {
  // Simulate a previous successful authentication
  ExpectAuthenticationAndSetResult(true);
  authenticator()->AuthenticateWithMessage(
      /*message=*/u"Chrome is trying to show passwords.", base::DoNothing());

  task_environment().FastForwardBy(kAuthValidityPeriod * 2);

  // The next call to `Authenticate()` should re-trigger an authentication.
  ExpectAuthenticationAndSetResult(false);
  base::MockCallback<DeviceAuthenticator::AuthenticateCallback> result_callback;
  EXPECT_CALL(result_callback, Run(/*auth_succeeded=*/false));
  authenticator()->AuthenticateWithMessage(
      /*message=*/u"Chrome is trying to show passwords.",
      result_callback.Get());

  task_environment().RunUntilIdle();
}

// If previous authentication failed, kAuthValidityPeriod isn't started and
// reauthentication will be needed.
TEST_F(DeviceAuthenticatorWinTest, ReauthenticationIfPreviousFailed) {
  ExpectAuthenticationAndSetResult(false);
  authenticator()->AuthenticateWithMessage(
      /*message=*/u"Chrome is trying to show passwords.", base::DoNothing());
  task_environment().RunUntilIdle();

  // The next call to `Authenticate()` should re-trigger an authentication.
  ExpectAuthenticationAndSetResult(true);
  base::MockCallback<DeviceAuthenticator::AuthenticateCallback> result_callback;
  EXPECT_CALL(result_callback, Run(/*auth_succeeded=*/true));
  authenticator()->AuthenticateWithMessage(
      /*message=*/u"Chrome is trying to show passwords.",
      result_callback.Get());

  task_environment().RunUntilIdle();
}

// Checks if CanAuthenticateWithBiometrics returns a valid pref value.
TEST_F(DeviceAuthenticatorWinTest, CanAuthenticateWithBiometrics) {
  local_state().Get()->SetBoolean(
      password_manager::prefs::kIsBiometricAvailable, true);
  EXPECT_TRUE(authenticator()->CanAuthenticateWithBiometrics());

  local_state().Get()->SetBoolean(
      password_manager::prefs::kIsBiometricAvailable, false);
  EXPECT_FALSE(authenticator()->CanAuthenticateWithBiometrics());
}

// Checks if CanAuthenticateWithBiometricOrScreenLock returns the correct
// response based on whether biometric or screen lock is available.
TEST_F(DeviceAuthenticatorWinTest, CanAuthenticateWithBiometricOrScreenLock) {
  local_state().Get()->SetBoolean(
      password_manager::prefs::kIsBiometricAvailable, true);
  EXPECT_TRUE(authenticator()->CanAuthenticateWithBiometricOrScreenLock());

  local_state().Get()->SetBoolean(
      password_manager::prefs::kIsBiometricAvailable, false);
  ON_CALL(system_authenticator(), CanAuthenticateWithScreenLock)
      .WillByDefault(testing::Return(true));
  EXPECT_TRUE(authenticator()->CanAuthenticateWithBiometricOrScreenLock());

  ON_CALL(system_authenticator(), CanAuthenticateWithScreenLock)
      .WillByDefault(testing::Return(false));
  EXPECT_FALSE(authenticator()->CanAuthenticateWithBiometricOrScreenLock());
}

TEST_F(DeviceAuthenticatorWinTest, RecordSuccessAuthHistogram) {
  ExpectAuthenticationAndSetResult(true);

  authenticator()->AuthenticateWithMessage(
      /*message=*/u"Chrome is trying to show passwords.", base::DoNothing());
  task_environment().RunUntilIdle();

  histogram_tester().ExpectUniqueSample(kHistogramName, ReauthResult::kSuccess,
                                        1);
}

TEST_F(DeviceAuthenticatorWinTest, RecordSkippedAuthHistogram) {
  ExpectAuthenticationAndSetResult(true);

  authenticator()->AuthenticateWithMessage(
      /*message=*/u"Chrome is trying to show passwords.", base::DoNothing());
  task_environment().RunUntilIdle();
  authenticator()->AuthenticateWithMessage(
      /*message=*/u"Chrome is trying to show passwords.", base::DoNothing());
  task_environment().RunUntilIdle();

  histogram_tester().ExpectBucketCount(kHistogramName, ReauthResult::kSuccess,
                                       1);
  histogram_tester().ExpectBucketCount(kHistogramName, ReauthResult::kSkipped,
                                       1);
}

TEST_F(DeviceAuthenticatorWinTest, RecordFailAuthHistogram) {
  ExpectAuthenticationAndSetResult(false);

  authenticator()->AuthenticateWithMessage(
      /*message=*/u"Chrome is trying to show passwords.", base::DoNothing());
  task_environment().RunUntilIdle();

  histogram_tester().ExpectUniqueSample(kHistogramName, ReauthResult::kFailure,
                                        1);
}

// Verifies that the caching mechanism for BiometricsAvailable works.
struct TestCase {
  const char* description;
  BiometricAuthenticationStatusWin availability;
  bool expected_result;
  int expected_bucket;
};

class DeviceAuthenticatorWinTestAvailability
    : public DeviceAuthenticatorWinTest,
      public testing::WithParamInterface<TestCase> {};

TEST_P(DeviceAuthenticatorWinTestAvailability, AvailabilityCheck) {
  TestCase test_case = GetParam();
  SCOPED_TRACE(test_case.description);
  EXPECT_CALL(system_authenticator(), CheckIfBiometricsAvailable)
      .WillOnce(testing::WithArg<0>([&test_case](auto callback) {
        std::move(callback).Run(test_case.availability);
      }));

  DeviceAuthenticatorWin::CacheIfBiometricsAvailable(&system_authenticator());

  EXPECT_EQ(test_case.expected_result,
            authenticator()->CanAuthenticateWithBiometrics());
  EXPECT_EQ(test_case.expected_result,
            local_state().Get()->GetBoolean(
                password_manager::prefs::kHadBiometricsAvailable));
  histogram_tester().ExpectUniqueSample(
      "PasswordManager.BiometricAvailabilityWin", test_case.expected_bucket, 1);
}

INSTANTIATE_TEST_SUITE_P(
    All,
    DeviceAuthenticatorWinTestAvailability,
    ::testing::Values(
        TestCase{
            .description = "kUnknown",
            .availability = BiometricAuthenticationStatusWin::kUnknown,
            .expected_result = false,
            .expected_bucket = 0,
        },
        TestCase{
            .description = "kAvailable",
            .availability = BiometricAuthenticationStatusWin::kAvailable,
            .expected_result = true,
            .expected_bucket = 1,
        },
        TestCase{
            .description = "kDeviceBusy",
            .availability = BiometricAuthenticationStatusWin::kDeviceBusy,
            .expected_result = false,
            .expected_bucket = 2,
        },
        TestCase{
            .description = "kDisabledByPolicy",
            .availability = BiometricAuthenticationStatusWin::kDisabledByPolicy,
            .expected_result = false,
            .expected_bucket = 3,
        },
        TestCase{
            .description = "kDeviceNotPresent",
            .availability = BiometricAuthenticationStatusWin::kDeviceNotPresent,
            .expected_result = false,
            .expected_bucket = 4,
        },
        TestCase{
            .description = "kNotConfiguredForUser",
            .availability =
                BiometricAuthenticationStatusWin::kNotConfiguredForUser,
            .expected_result = false,
            .expected_bucket = 5,
        }));

}  // namespace