chromium/chromeos/ash/components/osauth/impl/auth_hub_impl_test.cc

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

#include "chromeos/ash/components/osauth/impl/auth_hub_impl.h"

#include <memory>
#include <optional>
#include <utility>

#include "base/memory/raw_ptr.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/gmock_move_support.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chromeos/ash/components/install_attributes/stub_install_attributes.h"
#include "chromeos/ash/components/osauth/impl/auth_hub_common.h"
#include "chromeos/ash/components/osauth/impl/auth_parts_impl.h"
#include "chromeos/ash/components/osauth/public/auth_factor_engine.h"
#include "chromeos/ash/components/osauth/public/auth_factor_status_consumer.h"
#include "chromeos/ash/components/osauth/public/auth_hub.h"
#include "chromeos/ash/components/osauth/public/common_types.h"
#include "chromeos/ash/components/osauth/test_support/mock_auth_attempt_consumer.h"
#include "chromeos/ash/components/osauth/test_support/mock_auth_factor_engine.h"
#include "chromeos/ash/components/osauth/test_support/mock_auth_factor_engine_factory.h"
#include "chromeos/ash/components/osauth/test_support/mock_auth_factor_status_consumer.h"
#include "components/prefs/testing_pref_service.h"
#include "components/user_manager/fake_user_manager.h"
#include "components/user_manager/known_user.h"
#include "components/user_manager/user_manager_base.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {

constexpr base::TimeDelta kLongTime = base::Minutes(1);
constexpr AshAuthFactor kFactor = AshAuthFactor::kGaiaPassword;

using base::test::RunOnceCallback;
using testing::_;
using testing::AnyNumber;
using testing::ByMove;
using testing::Eq;
using testing::Invoke;
using testing::Return;
using testing::SaveArg;
using testing::StrictMock;

class AuthHubTestBase : public ::testing::Test {
 protected:
  AuthHubTestBase() {
    user_manager::UserManagerBase::RegisterPrefs(local_state_.registry());
    user_manager_ =
        std::make_unique<user_manager::FakeUserManager>(&local_state_);
    user_manager_->Initialize();
    factors_cache_ = std::make_unique<AuthFactorPresenceCache>(&local_state_);
    parts_ = AuthPartsImpl::CreateTestInstance();
    parts_->SetAuthHub(std::make_unique<AuthHubImpl>(factors_cache_.get()));

    auto factory = std::make_unique<StrictMock<MockAuthFactorEngineFactory>>();
    engine_factory_ = factory.get();
    parts_->RegisterEngineFactory(std::move(factory));

    SetupEngineForNextInit();
  }

  ~AuthHubTestBase() override { user_manager_->Destroy(); }

  void SetupEngineForNextInit() {
    testing::Mock::VerifyAndClearExpectations(engine_factory_.get());

    auto engine = std::make_unique<StrictMock<MockAuthFactorEngine>>();
    engine_ = engine.get();
    EXPECT_CALL(*engine, GetFactor()).WillRepeatedly(Return(kFactor));
    EXPECT_CALL(*engine, InitializeCommon(_))
        .Times(AnyNumber())
        .WillRepeatedly(base::test::RunOnceCallbackRepeatedly<0>(kFactor));
    EXPECT_CALL(*engine, ShutdownCommon(_))
        .Times(AnyNumber())
        .WillRepeatedly(base::test::RunOnceCallbackRepeatedly<0>(kFactor));

    EXPECT_CALL(*engine_factory_, GetFactor()).WillRepeatedly(Return(kFactor));
    EXPECT_CALL(*engine_factory_, CreateEngine(_))
        .WillOnce(Return(ByMove(std::move(engine))));
  }

  void ExpectEngineStart(AuthAttemptVector vector) {
    EXPECT_CALL(*engine_,
                StartAuthFlow(Eq(vector.account), Eq(vector.purpose), _))
        .WillOnce(SaveArg<2>(&engine_observer_));
    EXPECT_CALL(*engine_, UpdateObserver(_))
        .Times(AnyNumber())
        .WillRepeatedly(SaveArg<0>(&engine_observer_));
  }

  void ExpectEngineStop() {
    EXPECT_CALL(*engine_, CleanUp(_))
        .WillRepeatedly(base::test::RunOnceCallbackRepeatedly<0>(kFactor));
    EXPECT_CALL(*engine_, StopAuthFlow(_))
        .WillRepeatedly(base::test::RunOnceCallbackRepeatedly<0>(kFactor));
  }

  void ConfigureFactorAsAvailable() {
    EXPECT_CALL(*engine_, IsDisabledByPolicy())
        .Times(AnyNumber())
        .WillRepeatedly(Return(false));
    EXPECT_CALL(*engine_, IsLockedOut())
        .Times(AnyNumber())
        .WillRepeatedly(Return(false));
    EXPECT_CALL(*engine_, IsFactorSpecificRestricted())
        .Times(AnyNumber())
        .WillRepeatedly(Return(false));

    EXPECT_CALL(*engine_, SetUsageAllowed(_))
        .Times(AnyNumber())
        .WillRepeatedly(Invoke([&](AuthFactorEngine::UsageAllowed usage) {
          engine_usage_ = usage;
        }));
  }

  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};

  TestingPrefServiceSimple local_state_;
  std::unique_ptr<AuthFactorPresenceCache> factors_cache_;
  ash::ScopedStubInstallAttributes install_attributes{
      ash::StubInstallAttributes::CreateConsumerOwned()};
  std::unique_ptr<user_manager::FakeUserManager> user_manager_;
  std::unique_ptr<AuthPartsImpl> parts_;

  raw_ptr<MockAuthFactorEngineFactory> engine_factory_ = nullptr;
  raw_ptr<MockAuthFactorEngine> engine_ = nullptr;
  raw_ptr<AuthFactorEngine::FactorEngineObserver, AcrossTasksDanglingUntriaged>
      engine_observer_ = nullptr;
  std::optional<AuthFactorEngine::UsageAllowed> engine_usage_;
};

using AuthHubTestMode = AuthHubTestBase;

TEST_F(AuthHubTestMode, CheckEnsureInitialized) {
  base::test::TestFuture<void> init_future;

  AuthHub::Get()->EnsureInitialized(init_future.GetCallback());

  AuthFactorEngine::CommonInitCallback init_callback;
  EXPECT_CALL(*engine_, InitializeCommon(_))
      .WillOnce(MoveArg<0>(&init_callback));

  AuthHub::Get()->InitializeForMode(AuthHubMode::kLoginScreen);

  EXPECT_FALSE(init_future.IsReady());

  std::move(init_callback).Run(kFactor);

  EXPECT_TRUE(init_future.IsReady());
}

class AuthHubTestVector : public AuthHubTestBase {
 protected:
  AuthHubTestVector() {
    account_ = AccountId::FromUserEmail("[email protected]");
    attempt_ = AuthAttemptVector{account_, AuthPurpose::kLogin};
  }

  void ExpectAttemptConfirmation() {
    EXPECT_CALL(status_consumer_, InitializeUi(_, _))
        .WillOnce(
            Invoke([&](AuthFactorsSet factors, AuthHubConnector* connector) {
              for (auto factor : factors) {
                factors_state_[factor] = AuthFactorState::kCheckingForPresence;
              }
            }));

    EXPECT_CALL(attempt_consumer_, OnUserAuthAttemptConfirmed(_, _))
        .WillOnce(Invoke([&](AuthHubConnector* connector,
                             raw_ptr<AuthFactorStatusConsumer>& out_consumer) {
          connector_ = connector;
          out_consumer = &status_consumer_;
        }));
  }

  void ExpectFactorListUpdate() {
    EXPECT_CALL(status_consumer_, OnFactorListChanged(_))
        .WillOnce(
            Invoke([&](FactorsStatusMap update) { factors_state_ = update; }));
  }

  void ExpectFactorStateUpdate() {
    EXPECT_CALL(status_consumer_, OnFactorStatusesChanged(_))
        .WillOnce(Invoke([&](FactorsStatusMap update) {
          for (const auto& factor : update) {
            factors_state_[factor.first] = factor.second;
          }
        }));
  }

  AccountId account_;
  AuthAttemptVector attempt_;
  raw_ptr<AuthHubConnector, AcrossTasksDanglingUntriaged> connector_ = nullptr;
  StrictMock<MockAuthAttemptConsumer> attempt_consumer_;
  StrictMock<MockAuthFactorStatusConsumer> status_consumer_;
  FactorsStatusMap factors_state_;
};

TEST_F(AuthHubTestVector, InvalidPurposeOnLoginScreen) {
  AuthHub::Get()->InitializeForMode(AuthHubMode::kLoginScreen);

  EXPECT_CALL(attempt_consumer_, OnUserAuthAttemptRejected());
  AuthHub::Get()->StartAuthentication(account_, AuthPurpose::kWebAuthN,
                                      &attempt_consumer_);
}
TEST_F(AuthHubTestVector, InvalidPurposeInSession) {
  AuthHub::Get()->InitializeForMode(AuthHubMode::kInSession);

  EXPECT_CALL(attempt_consumer_, OnUserAuthAttemptRejected());
  AuthHub::Get()->StartAuthentication(account_, AuthPurpose::kLogin,
                                      &attempt_consumer_);
}

TEST_F(AuthHubTestVector, SingleFactorSuccess) {
  AuthHub::Get()->InitializeForMode(AuthHubMode::kLoginScreen);

  ExpectEngineStart(attempt_);
  ExpectAttemptConfirmation();

  // Make an empty auth factors cache to check if it would be updated.
  factors_cache_->StoreFactorPresenceCache(attempt_, AuthFactorsSet());
  AuthHub::Get()->StartAuthentication(attempt_.account, attempt_.purpose,
                                      &attempt_consumer_);

  ASSERT_NE(engine_observer_, nullptr);

  ConfigureFactorAsAvailable();
  // As factor cache is initially empty, there are no factors.
  EXPECT_TRUE(factors_state_.empty());

  ExpectFactorListUpdate();
  engine_observer_->OnFactorPresenceChecked(kFactor, true);

  EXPECT_TRUE(factors_state_.contains(kFactor));
  EXPECT_EQ(factors_state_[kFactor], AuthFactorState::kFactorReady);

  EXPECT_EQ(AuthFactorsSet({kFactor}),
            factors_cache_->GetExpectedFactorsPresence(attempt_));

  ASSERT_TRUE(engine_usage_.has_value());
  EXPECT_EQ(*engine_usage_, AuthFactorEngine::UsageAllowed::kEnabled);
}

// Test that AuthHub correctly switches mode if there was no
// authentication attempt.
TEST_F(AuthHubTestVector, TestSwitchingMode) {
  // We're on the login screen
  AuthHub::Get()->InitializeForMode(AuthHubMode::kLoginScreen);
  SetupEngineForNextInit();
  // And without starting any authentication attempt,
  // we're getting into session (e.g. via "add new user" flow).
  AuthHub::Get()->InitializeForMode(AuthHubMode::kInSession);
}

// Test that AuthHub correctly switches mode if there is some authentication
// request, but no factor attempt.
TEST_F(AuthHubTestVector, TestSwitchingModeWaitingForAuth) {
  // We're on the login screen
  AuthHub::Get()->InitializeForMode(AuthHubMode::kLoginScreen);
  // We've selected a user pod
  ExpectEngineStart(attempt_);
  ExpectAttemptConfirmation();
  AuthHub::Get()->StartAuthentication(attempt_.account, attempt_.purpose,
                                      &attempt_consumer_);
  // Engine replies that factor is present
  ExpectFactorStateUpdate();
  ConfigureFactorAsAvailable();

  engine_observer_->OnFactorPresenceChecked(kFactor, true);

  // We've logged in in some other way (e.g. via "add new user" flow).
  testing::Mock::VerifyAndClearExpectations(&attempt_consumer_);
  EXPECT_CALL(attempt_consumer_, OnUserAuthAttemptCancelled());
  ExpectEngineStop();
  SetupEngineForNextInit();

  AuthHub::Get()->InitializeForMode(AuthHubMode::kInSession);
  testing::Mock::VerifyAndClearExpectations(&attempt_consumer_);
  // And now we've bringing lock screen.
  attempt_ = AuthAttemptVector{account_, AuthPurpose::kScreenUnlock};
  testing::Mock::VerifyAndClearExpectations(&status_consumer_);
  ExpectEngineStart(attempt_);
  ExpectAttemptConfirmation();
  AuthHub::Get()->StartAuthentication(attempt_.account, attempt_.purpose,
                                      &attempt_consumer_);
  // Engine replies that factor is present
  ExpectFactorStateUpdate();
  ConfigureFactorAsAvailable();
  engine_observer_->OnFactorPresenceChecked(kFactor, true);
}

// Test that AuthHub correctly switches mode if there is some authentication
// request, but before engine have chance to start.
TEST_F(AuthHubTestVector, TestSwitchingModeWhileInitializingEngine) {
  // We're on the login screen
  AuthHub::Get()->InitializeForMode(AuthHubMode::kLoginScreen);
  // We've selected a user pod
  ExpectEngineStart(attempt_);
  ExpectAttemptConfirmation();
  AuthHub::Get()->StartAuthentication(attempt_.account, attempt_.purpose,
                                      &attempt_consumer_);

  // We've logged in in some other way (e.g. via "add new user" flow).
  testing::Mock::VerifyAndClearExpectations(&attempt_consumer_);
  EXPECT_CALL(attempt_consumer_, OnUserAuthAttemptCancelled());
  ExpectEngineStop();
  SetupEngineForNextInit();

  AuthHub::Get()->InitializeForMode(AuthHubMode::kInSession);

  // And now the engine finally starts.
  engine_observer_->OnFactorPresenceChecked(kFactor, true);
}

// Test that AuthHub correctly switches mode if there is some authentication
// request, but engine fails to start in time.
TEST_F(AuthHubTestVector, TestSwitchingModeWithEngineTimeout) {
  // We're on the login screen
  AuthHub::Get()->InitializeForMode(AuthHubMode::kLoginScreen);
  // We've selected a user pod
  ExpectEngineStart(attempt_);
  ExpectAttemptConfirmation();
  AuthHub::Get()->StartAuthentication(attempt_.account, attempt_.purpose,
                                      &attempt_consumer_);

  // We've logged in in some other way (e.g. via "add new user" flow).
  testing::Mock::VerifyAndClearExpectations(&attempt_consumer_);
  EXPECT_CALL(attempt_consumer_, OnUserAuthAttemptCancelled());
  EXPECT_CALL(*engine_, StartFlowTimedOut());
  ExpectEngineStop();

  SetupEngineForNextInit();

  AuthHub::Get()->InitializeForMode(AuthHubMode::kInSession);

  // Engine start times out.
  task_environment_.FastForwardBy(kLongTime);
}

// TODO (b/271248265): add tests for preemption during initialization.

// TODO (b/271248265): add tests for interaction between AuthHub and
// AttemptHandler.

// TODO (b/271248265): add tests that make sure that engines that have failed to
// start are reported as failed to AttemptHandler (for the purpose of UI cache
// tracking).

}  // namespace ash