chromium/chromeos/ash/components/osauth/impl/auth_hub_vector_lifecycle_unittest.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_vector_lifecycle.h"

#include <memory>
#include <utility>

#include "base/containers/flat_map.h"
#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/time/time.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/common_types.h"
#include "chromeos/ash/components/osauth/test_support/mock_auth_factor_engine.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;
constexpr AshAuthFactor kSpecialFactor = AshAuthFactor::kCryptohomePin;

using base::test::RunOnceCallback;
using base::test::RunOnceClosure;
using testing::_;
using testing::AnyNumber;
using testing::AtMost;
using testing::ByMove;
using testing::DoAll;
using testing::Eq;
using testing::InSequence;
using testing::Invoke;
using testing::Mock;
using testing::MockFunction;
using testing::Return;
using testing::SaveArg;
using testing::StrictMock;

class MockVectorLifecycleOwner : public AuthHubVectorLifecycle::Owner {
 public:
  MockVectorLifecycleOwner() = default;
  ~MockVectorLifecycleOwner() override = default;

  MOCK_METHOD(AuthFactorEngine::FactorEngineObserver*,
              AsEngineObserver,
              (),
              (override));
  MOCK_METHOD(void,
              OnAttemptStarted,
              (const AuthAttemptVector&, AuthFactorsSet, AuthFactorsSet),
              (override));
  MOCK_METHOD(void, OnAttemptFinished, (const AuthAttemptVector&), (override));
  MOCK_METHOD(void, OnAttemptCancelled, (const AuthAttemptVector&), (override));
  MOCK_METHOD(void, OnAttemptCleanedUp, (const AuthAttemptVector&), (override));
  MOCK_METHOD(void, OnIdle, (), (override));
};

class AuthHubVectorLifecycleTest : public ::testing::Test {
 protected:
  AuthHubVectorLifecycleTest() {
    parts_ = AuthPartsImpl::CreateTestInstance();
    account_ = AccountId::FromUserEmail("[email protected]");
  }

  void SetUp() override {}

  void InitLifecycle() {
    AuthEnginesMap engines;
    for (const auto& engine : engines_) {
      engines[engine.first] = engine.second.get();
    }
    lifecycle_ = std::make_unique<AuthHubVectorLifecycle>(
        &owner_, AuthHubMode::kLoginScreen, engines);
    EXPECT_CALL(owner_, AsEngineObserver())
        .Times(AnyNumber())
        .WillRepeatedly(Return(&owner_engine_observer_));
  }

  void ExpectOwnerAttemptStartCalled(AuthAttemptVector attempt) {
    EXPECT_CALL(owner_, OnAttemptStarted(Eq(attempt), _, _))
        .WillOnce(Invoke([&](const AuthAttemptVector&, AuthFactorsSet usable,
                             AuthFactorsSet failed) {
          usable_factors_ = usable;
          failed_factors_ = failed;
        }));
  }

  void TriggerTimeout() { task_environment_.FastForwardBy(kLongTime); }

  void ExpectFactorAvailability(AuthFactorsSet usable, AuthFactorsSet failed) {
    ASSERT_EQ(usable_factors_, usable);
    ASSERT_EQ(failed_factors_, failed);
  }

  void StartAttemptWithAllFactors(AuthAttemptVector attempt) {
    AuthFactorsSet all_factors;
    lifecycle_->StartAttempt(attempt);
    ExpectOwnerAttemptStartCalled(attempt);
    for (const auto& observer : engine_obvservers_) {
      all_factors.Put(observer.first);
      observer.second->OnFactorPresenceChecked(observer.first, true);
    }
    ExpectFactorAvailability(all_factors, AuthFactorsSet{});
    for (const auto& observer : engine_obvservers_) {
      EXPECT_EQ(observer.second.get(), &owner_engine_observer_);
    }
  }

  void ExpectAllFactorsShutdown() {
    for (const auto& engine : engines_) {
      EXPECT_CALL(*engine.second, CleanUp(_))
          .WillOnce(RunOnceCallback<0>(engine.first));
      EXPECT_CALL(*engine.second, StopAuthFlow(_))
          .WillOnce(RunOnceCallback<0>(engine.first));
    }
  }

  void ExpectAuthOnEngine(AshAuthFactor factor, AuthAttemptVector attempt) {
    StrictMock<MockAuthFactorEngine>* engine = engines_[factor].get();

    EXPECT_CALL(*engine,
                StartAuthFlow(Eq(attempt.account), Eq(attempt.purpose), _))
        .WillOnce(Invoke(
            [&, factor](AccountId, AuthPurpose,
                        AuthFactorEngine::FactorEngineObserver* observer) {
              engine_obvservers_[factor] = observer;
            }));
  }

  StrictMock<MockAuthFactorEngine>* SetUpEngine(AshAuthFactor factor) {
    engines_[factor] = std::make_unique<StrictMock<MockAuthFactorEngine>>();
    StrictMock<MockAuthFactorEngine>* engine = engines_[factor].get();

    ExpectAuthOnEngine(factor,
                       AuthAttemptVector{account_, AuthPurpose::kLogin});

    EXPECT_CALL(*engine, UpdateObserver(_))
        .Times(AnyNumber())
        .WillRepeatedly(Invoke(
            [&, factor](AuthFactorEngine::FactorEngineObserver* observer) {
              engine_obvservers_[factor] = observer;
            }));
    return engine;
  }

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

  AccountId account_;
  std::unique_ptr<AuthPartsImpl> parts_;
  StrictMock<MockVectorLifecycleOwner> owner_;
  StrictMock<MockAuthFactorEngineObserver> owner_engine_observer_;

  base::flat_map<AshAuthFactor,
                 std::unique_ptr<StrictMock<MockAuthFactorEngine>>>
      engines_;
  base::flat_map<
      AshAuthFactor,
      raw_ptr<AuthFactorEngine::FactorEngineObserver, DanglingUntriaged>>
      engine_obvservers_;
  AuthFactorsSet usable_factors_;
  AuthFactorsSet failed_factors_;

  std::unique_ptr<AuthHubVectorLifecycle> lifecycle_;

  raw_ptr<AuthHubConnector> connector_;
};

// Standard init/shutdown flow.
TEST_F(AuthHubVectorLifecycleTest, SingleFactorAttemptStartCancel) {
  auto* engine = SetUpEngine(kFactor);
  InitLifecycle();

  EXPECT_TRUE(lifecycle_->IsIdle());

  AuthAttemptVector attempt{account_, AuthPurpose::kLogin};

  // Start auth attempt, Lifecycle sets observer to engine.
  lifecycle_->StartAttempt(attempt);

  EXPECT_FALSE(lifecycle_->IsIdle());

  ExpectOwnerAttemptStartCalled(attempt);

  // Report that engine is ready.
  engine_obvservers_[kFactor]->OnFactorPresenceChecked(kFactor, true);

  // Upon initialization Lifecycle should report usable factors, and
  // update Observer to Owner for them.
  ExpectFactorAvailability(AuthFactorsSet{kFactor}, AuthFactorsSet{});

  EXPECT_EQ(engine_obvservers_[kFactor].get(), &owner_engine_observer_);
  EXPECT_FALSE(lifecycle_->IsIdle());

  Mock::VerifyAndClearExpectations(&owner_);

  AuthFactorEngine::CleanupCallback cleanup_callback;
  AuthFactorEngine::ShutdownCallback shutdown_callback;
  EXPECT_CALL(*engine, CleanUp(_)).WillOnce(MoveArg<0>(&cleanup_callback));
  EXPECT_CALL(*engine, StopAuthFlow(_))
      .WillOnce(MoveArg<0>(&shutdown_callback));
  MockFunction<void(std::string check_point_name)> check;
  {
    InSequence s;
    EXPECT_CALL(owner_, OnAttemptCleanedUp(Eq(attempt)));
    EXPECT_CALL(check, Call("NotifyCleanedUp"));
    EXPECT_CALL(owner_, OnAttemptCancelled(Eq(attempt)));
    EXPECT_CALL(owner_, OnIdle());
    EXPECT_CALL(check, Call("NotifyFinished"));
  }
  lifecycle_->CancelAttempt();

  // Upon attempt cancellation, Lifecycle should provide callback
  // to CleanUp first.
  ASSERT_FALSE(cleanup_callback.is_null());

  // Once engine finishes attempt cleanup, Lifecycle should notify the Owner,
  // and provide callback to StopAuthFlow.
  std::move(cleanup_callback).Run(kFactor);
  check.Call("NotifyCleanedUp");
  ASSERT_FALSE(shutdown_callback.is_null());

  // Once engine finishes shutdown, Lifecycle should
  // notify the Owner.
  std::move(shutdown_callback).Run(kFactor);
  check.Call("NotifyFinished");

  EXPECT_TRUE(lifecycle_->IsIdle());
}

TEST_F(AuthHubVectorLifecycleTest, FactorNotPresent) {
  SetUpEngine(kFactor);
  SetUpEngine(kSpecialFactor);
  InitLifecycle();

  EXPECT_TRUE(lifecycle_->IsIdle());

  AuthAttemptVector attempt{account_, AuthPurpose::kLogin};
  // Start auth attempt, Lifecycle sets observer to engine.
  lifecycle_->StartAttempt(attempt);
  EXPECT_FALSE(lifecycle_->IsIdle());

  ExpectOwnerAttemptStartCalled(attempt);

  engine_obvservers_[kFactor]->OnFactorPresenceChecked(kFactor, true);
  engine_obvservers_[kSpecialFactor]->OnFactorPresenceChecked(kSpecialFactor,
                                                              false);

  // Upon initialization Lifecycle should report usable factors, and
  // update Observer to Owner for them.
  // Note that as kSpecialFactor is just not configured, it is not reported
  // neither in `failed_factors` nor in `usable_factors`, it's observer is not
  // changed.

  ExpectFactorAvailability(AuthFactorsSet{kFactor}, AuthFactorsSet{});

  EXPECT_EQ(engine_obvservers_[kFactor].get(), &owner_engine_observer_);
  EXPECT_NE(engine_obvservers_[kSpecialFactor].get(), &owner_engine_observer_);
  EXPECT_FALSE(lifecycle_->IsIdle());

  // Upon cleanup, non-present factor should still be asked
  // to stop auth flow.
  ExpectAllFactorsShutdown();

  EXPECT_CALL(owner_, OnAttemptCleanedUp(_));
  EXPECT_CALL(owner_, OnAttemptCancelled(_));
  EXPECT_CALL(owner_, OnIdle());

  lifecycle_->CancelAttempt();
}

TEST_F(AuthHubVectorLifecycleTest, FactorTimedOutDuringInit) {
  SetUpEngine(kFactor);
  auto* timeout_engine = SetUpEngine(kSpecialFactor);
  InitLifecycle();

  EXPECT_TRUE(lifecycle_->IsIdle());

  AuthAttemptVector attempt{account_, AuthPurpose::kLogin};
  // Start auth attempt, Lifecycle sets observer to engine.
  lifecycle_->StartAttempt(attempt);
  EXPECT_FALSE(lifecycle_->IsIdle());

  ExpectOwnerAttemptStartCalled(attempt);

  engine_obvservers_[kFactor]->OnFactorPresenceChecked(kFactor, true);
  EXPECT_CALL(*timeout_engine, StartFlowTimedOut());

  TriggerTimeout();

  // Upon initialization Lifecycle should report usable and failed factors, and
  // update Observer to Owner for usable ones.
  ExpectFactorAvailability(AuthFactorsSet{kFactor},
                           AuthFactorsSet{kSpecialFactor});

  EXPECT_EQ(engine_obvservers_[kFactor].get(), &owner_engine_observer_);
  EXPECT_NE(engine_obvservers_[kSpecialFactor].get(), &owner_engine_observer_);
  EXPECT_FALSE(lifecycle_->IsIdle());

  // Upon cleanup, timed-out factor should still be asked
  // to stop auth flow.
  ExpectAllFactorsShutdown();

  EXPECT_CALL(owner_, OnAttemptCleanedUp(_));
  EXPECT_CALL(owner_, OnAttemptCancelled(_));
  EXPECT_CALL(owner_, OnIdle());

  lifecycle_->CancelAttempt();
}

TEST_F(AuthHubVectorLifecycleTest, FactorCriticalErrorDuringInit) {
  SetUpEngine(kFactor);
  SetUpEngine(kSpecialFactor);
  InitLifecycle();

  EXPECT_TRUE(lifecycle_->IsIdle());

  AuthAttemptVector attempt{account_, AuthPurpose::kLogin};
  // Start auth attempt, Lifecycle sets observer to engine.
  lifecycle_->StartAttempt(attempt);
  EXPECT_FALSE(lifecycle_->IsIdle());

  ExpectOwnerAttemptStartCalled(attempt);

  engine_obvservers_[kFactor]->OnFactorPresenceChecked(kFactor, true);
  engine_obvservers_[kFactor]->OnCriticalError(kSpecialFactor);

  // Upon initialization Lifecycle should report usable and failed factors, and
  // update Observer to Owner for usable ones.

  ExpectFactorAvailability(AuthFactorsSet{kFactor},
                           AuthFactorsSet{kSpecialFactor});

  EXPECT_EQ(engine_obvservers_[kFactor].get(), &owner_engine_observer_);
  EXPECT_NE(engine_obvservers_[kSpecialFactor].get(), &owner_engine_observer_);
  EXPECT_FALSE(lifecycle_->IsIdle());

  // Upon cleanup, failed factor should still be asked to stop auth flow.
  ExpectAllFactorsShutdown();

  EXPECT_CALL(owner_, OnAttemptCleanedUp(_));
  EXPECT_CALL(owner_, OnAttemptCancelled(_));
  EXPECT_CALL(owner_, OnIdle());

  lifecycle_->CancelAttempt();
}

TEST_F(AuthHubVectorLifecycleTest, FactorTimedOutDuringCleanup) {
  auto* engine = SetUpEngine(kFactor);
  auto* timeout_engine = SetUpEngine(kSpecialFactor);
  InitLifecycle();

  AuthAttemptVector attempt{account_, AuthPurpose::kLogin};
  StartAttemptWithAllFactors(attempt);

  EXPECT_CALL(*engine, CleanUp(_)).WillOnce(RunOnceCallback<0>(kFactor));
  EXPECT_CALL(*engine, StopAuthFlow(_)).WillOnce(RunOnceCallback<0>(kFactor));
  AuthFactorEngine::CleanupCallback callback;
  EXPECT_CALL(*timeout_engine, CleanUp(_)).WillOnce(MoveArg<0>(&callback));
  EXPECT_CALL(*timeout_engine, StopAuthFlow(_))
      .WillOnce(RunOnceCallback<0>(kSpecialFactor));
  lifecycle_->CancelAttempt();

  ASSERT_FALSE(callback.is_null());

  EXPECT_CALL(owner_, OnAttemptCleanedUp(_));
  EXPECT_CALL(owner_, OnAttemptCancelled(_));
  EXPECT_CALL(owner_, OnIdle());
  EXPECT_CALL(*timeout_engine, StopFlowTimedOut());

  TriggerTimeout();
}

TEST_F(AuthHubVectorLifecycleTest, FactorTimedOutDuringShutdown) {
  auto* engine = SetUpEngine(kFactor);
  auto* timeout_engine = SetUpEngine(kSpecialFactor);
  InitLifecycle();

  AuthAttemptVector attempt{account_, AuthPurpose::kLogin};
  StartAttemptWithAllFactors(attempt);

  EXPECT_CALL(*engine, CleanUp(_)).WillOnce(RunOnceCallback<0>(kFactor));
  EXPECT_CALL(*engine, StopAuthFlow(_)).WillOnce(RunOnceCallback<0>(kFactor));
  AuthFactorEngine::ShutdownCallback callback;
  EXPECT_CALL(*timeout_engine, CleanUp(_))
      .WillOnce(RunOnceCallback<0>(kSpecialFactor));
  EXPECT_CALL(*timeout_engine, StopAuthFlow(_)).WillOnce(MoveArg<0>(&callback));
  EXPECT_CALL(owner_, OnAttemptCleanedUp(_));
  lifecycle_->CancelAttempt();

  ASSERT_FALSE(callback.is_null());

  EXPECT_CALL(owner_, OnAttemptCancelled(_));
  EXPECT_CALL(owner_, OnIdle());
  EXPECT_CALL(*timeout_engine, StopFlowTimedOut());

  TriggerTimeout();
}

TEST_F(AuthHubVectorLifecycleTest, SwitchAttemptDuringInit) {
  auto* engine = SetUpEngine(kFactor);
  InitLifecycle();

  AuthAttemptVector attempt1{account_, AuthPurpose::kLogin};
  AuthAttemptVector attempt2{AccountId::FromUserEmail("[email protected]"),
                             AuthPurpose::kLogin};

  lifecycle_->StartAttempt(attempt1);

  EXPECT_CALL(*engine, CleanUp(_)).WillOnce(RunOnceCallback<0>(kFactor));
  EXPECT_CALL(*engine, StopAuthFlow(_)).WillOnce(RunOnceCallback<0>(kFactor));
  ExpectAuthOnEngine(kFactor, attempt2);

  lifecycle_->StartAttempt(attempt2);
  // Now 1st attempt should be finished and 2nd attempt should take over.
  engine_obvservers_[kFactor]->OnFactorPresenceChecked(kFactor, true);

  ExpectOwnerAttemptStartCalled(attempt2);
  // Now the second attempt should be going.
  engine_obvservers_[kFactor]->OnFactorPresenceChecked(kFactor, true);
}

}  // namespace ash