chromium/chromeos/ash/components/drivefs/drivefs_host_unittest.cc

// Copyright 2018 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/drivefs/drivefs_host.h"

#include <type_traits>
#include <utility>

#include "ash/constants/ash_features.h"
#include "base/check.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/gmock_move_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/test/task_environment.h"
#include "base/timer/mock_timer.h"
#include "chromeos/ash/components/disks/disk_mount_manager.h"
#include "chromeos/ash/components/disks/mock_disk_mount_manager.h"
#include "chromeos/ash/components/drivefs/drivefs_host.h"
#include "chromeos/ash/components/drivefs/fake_drivefs.h"
#include "chromeos/ash/components/drivefs/mojom/drivefs.mojom-test-utils.h"
#include "chromeos/ash/components/drivefs/mojom/drivefs.mojom.h"
#include "chromeos/components/mojo_bootstrap/pending_connection_manager.h"
#include "components/account_id/account_id.h"
#include "components/invalidation/impl/fake_invalidation_service.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "mojo/public/cpp/bindings/clone_traits.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/test/test_network_connection_tracker.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/utility/utility.h"

namespace drivefs {
namespace {

using base::test::RunOnceClosure;
using testing::_;
using MountFailure = DriveFsHost::MountObserver::MountFailure;
using ChangeLogOptionPair = std::pair<int64_t, std::string>;

using mojom::ItemEvent::State::kCompleted;
using mojom::ItemEvent::State::kFailed;
using mojom::ItemEvent::State::kInProgress;
using mojom::ItemEventReason::kTransfer;

constexpr base::TimeDelta kTokenLifetime = base::Hours(1);

class MockDriveFs : public mojom::DriveFsInterceptorForTesting,
                    public mojom::SearchQuery {
 public:
  MockDriveFs() = default;

  DriveFs* GetForwardingInterface() override {
    NOTREACHED_IN_MIGRATION();
    return nullptr;
  }

  void FetchChangeLog(
      std::vector<mojom::FetchChangeLogOptionsPtr> options) override {
    std::vector<ChangeLogOptionPair> unwrapped_options;
    for (auto& entry : options) {
      unwrapped_options.push_back(
          std::make_pair(entry->change_id, entry->team_drive_id));
    }
    FetchChangeLogImpl(unwrapped_options);
  }

  MOCK_METHOD(void,
              FetchChangeLogImpl,
              (const std::vector<ChangeLogOptionPair>&));

  MOCK_METHOD(void, FetchAllChangeLogs, ());

  MOCK_METHOD(void,
              OnStartSearchQuery,
              (const mojom::QueryParameters&),
              (const));
  void StartSearchQuery(mojo::PendingReceiver<mojom::SearchQuery> receiver,
                        mojom::QueryParametersPtr query_params) override {
    search_receiver_.reset();
    OnStartSearchQuery(*query_params);
    search_receiver_.Bind(std::move(receiver));
  }

  MOCK_METHOD(drive::FileError,
              OnGetNextPage,
              (std::optional<std::vector<mojom::QueryItemPtr>> * items));

  void GetNextPage(GetNextPageCallback callback) override {
    std::optional<std::vector<mojom::QueryItemPtr>> items;
    auto error = OnGetNextPage(&items);
    std::move(callback).Run(error, std::move(items));
  }

 private:
  mojo::Receiver<mojom::SearchQuery> search_receiver_{this};
};

class TestingDriveFsHostDelegate : public DriveFsHost::Delegate,
                                   public DriveFsHost::MountObserver {
 public:
  TestingDriveFsHostDelegate(signin::IdentityManager* identity_manager,
                             const AccountId& account_id)
      : identity_manager_(identity_manager), account_id_(account_id) {}

  TestingDriveFsHostDelegate(const TestingDriveFsHostDelegate&) = delete;
  TestingDriveFsHostDelegate& operator=(const TestingDriveFsHostDelegate&) =
      delete;

  void set_pending_bootstrap(
      mojo::PendingRemote<mojom::DriveFsBootstrap> pending_bootstrap) {
    pending_bootstrap_ = std::move(pending_bootstrap);
  }

  void set_verbose_logging_enabled(bool enabled) {
    verbose_logging_enabled_ = enabled;
  }

  mojom::ExtensionConnectionParams& get_last_extension_params() {
    return *extension_params_;
  }

  // DriveFsHost::MountObserver:
  MOCK_METHOD(void, OnMounted, (const base::FilePath&));
  MOCK_METHOD(void,
              OnMountFailed,
              (MountFailure, std::optional<base::TimeDelta>));
  MOCK_METHOD(void, OnUnmounted, (std::optional<base::TimeDelta>));

 private:
  // DriveFsHost::Delegate:
  scoped_refptr<network::SharedURLLoaderFactory> GetURLLoaderFactory()
      override {
    return nullptr;
  }
  signin::IdentityManager* GetIdentityManager() override {
    return identity_manager_;
  }
  const AccountId& GetAccountId() override { return account_id_; }
  std::string GetObfuscatedAccountId() override {
    return "salt-" + account_id_.GetAccountIdKey();
  }
  bool IsMetricsCollectionEnabled() override { return false; }

  std::unique_ptr<DriveFsBootstrapListener> CreateMojoListener() override {
    DCHECK(pending_bootstrap_);
    return std::make_unique<FakeDriveFsBootstrapListener>(
        std::move(pending_bootstrap_));
  }

  std::string GetLostAndFoundDirectoryName() override {
    return "recovered files";
  }

  base::FilePath GetMyFilesPath() override {
    return base::FilePath("/MyFiles");
  }

  bool IsVerboseLoggingEnabled() override { return verbose_logging_enabled_; }

  void ConnectToExtension(
      mojom::ExtensionConnectionParamsPtr params,
      mojo::PendingReceiver<mojom::NativeMessagingPort> port,
      mojo::PendingRemote<mojom::NativeMessagingHost> host,
      mojom::DriveFsDelegate::ConnectToExtensionCallback callback) override {
    extension_params_ = std::move(params);
    std::move(callback).Run(
        mojom::ExtensionConnectionStatus::kExtensionNotFound);
  }

  const std::string GetMachineRootID() override { return ""; }

  void PersistMachineRootID(const std::string& id) override {}

  void PersistNotification(
      mojom::DriveFsNotificationPtr notification) override {}

  void PersistSyncErrors(mojom::MirrorSyncErrorListPtr error_list) override {}

  const raw_ptr<signin::IdentityManager> identity_manager_;
  const AccountId account_id_;
  mojo::PendingRemote<mojom::DriveFsBootstrap> pending_bootstrap_;
  bool verbose_logging_enabled_ = false;
  invalidation::FakeInvalidationService invalidation_service_;
  mojom::ExtensionConnectionParamsPtr extension_params_;
};

class MockDriveFsHostObserver : public DriveFsHost::Observer {
 public:
  MOCK_METHOD(void, OnUnmounted, ());
  MOCK_METHOD(void,
              OnSyncingStatusUpdate,
              (const mojom::SyncingStatus& status));
  MOCK_METHOD(void,
              OnMirrorSyncingStatusUpdate,
              (const mojom::SyncingStatus& status));
  MOCK_METHOD(void,
              OnFilesChanged,
              (const std::vector<mojom::FileChange>& changes));
  MOCK_METHOD(void, OnError, (const mojom::DriveError& error));
  MOCK_METHOD(void, OnItemProgress, (const mojom::ProgressEvent& event));
};

class DriveFsHostTest : public ::testing::Test, public mojom::DriveFsBootstrap {
 public:
  DriveFsHostTest()
      : network_connection_tracker_(
            network::TestNetworkConnectionTracker::CreateInstance()) {
    clock_.SetNow(base::Time::Now());
  }

  DriveFsHostTest(const DriveFsHostTest&) = delete;
  DriveFsHostTest& operator=(const DriveFsHostTest&) = delete;

 protected:
  void SetUp() override {
    testing::Test::SetUp();
    profile_path_ = base::FilePath(FILE_PATH_LITERAL("/path/to/profile"));
    account_id_ = AccountId::FromUserEmailGaiaId("[email protected]", "ID");

    disk_manager_ = std::make_unique<ash::disks::MockDiskMountManager>();
    identity_test_env_.MakePrimaryAccountAvailable(
        "[email protected]", signin::ConsentLevel::kSignin);
    host_delegate_ = std::make_unique<TestingDriveFsHostDelegate>(
        identity_test_env_.identity_manager(), account_id_);
    auto timer = std::make_unique<base::MockOneShotTimer>();
    timer_ = timer.get();
    host_ = std::make_unique<DriveFsHost>(
        profile_path_, host_delegate_.get(), host_delegate_.get(),
        network_connection_tracker_.get(), &clock_, disk_manager_.get(),
        std::move(timer));
  }

  void TearDown() override {
    host_.reset();
    disk_manager_.reset();
  }

  std::string StartMount() {
    std::string source;
    EXPECT_CALL(
        *disk_manager_,
        MountPath(
            testing::StartsWith("drivefs://"), "", "drivefs-salt-g-ID",
            testing::AllOf(testing::Contains(
                               "datadir=/path/to/profile/GCache/v2/salt-g-ID"),
                           testing::Contains("myfiles=/MyFiles")),
            _, ash::MountAccessMode::kReadWrite, _))
        .WillOnce(testing::DoAll(testing::SaveArg<0>(&source),
                                 MoveArg<6>(&mount_callback_)));

    host_delegate_->set_pending_bootstrap(
        bootstrap_receiver_.BindNewPipeAndPassRemote());
    pending_delegate_receiver_ = delegate_.BindNewPipeAndPassReceiver();

    EXPECT_TRUE(host_->Mount());
    testing::Mock::VerifyAndClear(&disk_manager_);

    return source.substr(strlen("drivefs://"));
  }

  void CallMountCallbackSuccess(const std::string& token) {
    std::move(mount_callback_)
        .Run(ash::MountError::kSuccess,
             {base::StrCat({"drivefs://", token}),
              "/media/drivefsroot/salt-g-ID", ash::MountType::kNetworkStorage});
  }

  void SendOnMounted() { delegate_->OnMounted(); }

  void SendOnUnmounted(std::optional<base::TimeDelta> delay) {
    delegate_->OnUnmounted(std::move(delay));
  }

  void SendMountFailed(std::optional<base::TimeDelta> delay) {
    delegate_->OnMountFailed(std::move(delay));
  }

  void EstablishConnection() {
    token_ = StartMount();
    CallMountCallbackSuccess(token_);

    ASSERT_TRUE(mojo_bootstrap::PendingConnectionManager::Get().OpenIpcChannel(
        token_, {}));
    {
      base::RunLoop run_loop;
      bootstrap_receiver_.set_disconnect_handler(run_loop.QuitClosure());
      run_loop.Run();
    }
  }

  void DoMount() {
    EstablishConnection();
    base::RunLoop run_loop;
    base::OnceClosure quit_closure = run_loop.QuitClosure();
    EXPECT_CALL(*host_delegate_,
                OnMounted(base::FilePath("/media/drivefsroot/salt-g-ID")))
        .WillOnce(RunOnceClosure(std::move(quit_closure)));
    // Eventually we must attempt unmount.
    EXPECT_CALL(*disk_manager_, UnmountPath("/media/drivefsroot/salt-g-ID", _));
    SendOnMounted();
    run_loop.Run();
    ASSERT_TRUE(host_->IsMounted());
    mount_path_ = host_->GetMountPath();
  }

  void DoUnmount() {
    EXPECT_CALL(*host_delegate_, OnUnmounted(_)).Times(0);
    host_->Unmount();
    receiver_.reset();
    bootstrap_receiver_.reset();
    delegate_.reset();
    base::RunLoop().RunUntilIdle();
    testing::Mock::VerifyAndClearExpectations(disk_manager_.get());
    testing::Mock::VerifyAndClearExpectations(host_delegate_.get());
  }

  void Init(mojom::DriveFsConfigurationPtr config,
            mojo::PendingReceiver<mojom::DriveFs> drive_fs_receiver,
            mojo::PendingRemote<mojom::DriveFsDelegate> delegate) override {
    EXPECT_EQ("[email protected]", config->user_email);
    EXPECT_EQ("recovered files",
              config->lost_and_found_directory_name.value_or("<None>"));
    verbose_logging_enabled_ = config->enable_verbose_logging;
    init_access_token_ = std::move(config->access_token);
    receiver_.Bind(std::move(drive_fs_receiver));
    mojo::FusePipes(std::move(pending_delegate_receiver_), std::move(delegate));
  }

  base::FilePath profile_path_;
  base::test::TaskEnvironment task_environment_;
  AccountId account_id_;
  std::unique_ptr<ash::disks::MockDiskMountManager> disk_manager_;
  ash::disks::DiskMountManager::MountPathCallback mount_callback_;
  std::unique_ptr<network::TestNetworkConnectionTracker>
      network_connection_tracker_;
  base::SimpleTestClock clock_;
  signin::IdentityTestEnvironment identity_test_env_;
  std::unique_ptr<TestingDriveFsHostDelegate> host_delegate_;
  std::unique_ptr<DriveFsHost> host_;
  raw_ptr<base::MockOneShotTimer, DanglingUntriaged> timer_;
  std::optional<bool> verbose_logging_enabled_;

  mojo::Receiver<mojom::DriveFsBootstrap> bootstrap_receiver_{this};
  MockDriveFs mock_drivefs_;
  mojo::Receiver<mojom::DriveFs> receiver_{&mock_drivefs_};
  mojo::Remote<mojom::DriveFsDelegate> delegate_;
  mojo::PendingReceiver<mojom::DriveFsDelegate> pending_delegate_receiver_;
  std::string token_;
  std::optional<std::string> init_access_token_;
  base::FilePath mount_path_;
};

TEST_F(DriveFsHostTest, Basic) {
  MockDriveFsHostObserver observer;
  observer.Observe(host_.get());

  EXPECT_FALSE(host_->IsMounted());

  EXPECT_EQ(base::FilePath("/path/to/profile/GCache/v2/salt-g-ID"),
            host_->GetDataPath());

  ASSERT_NO_FATAL_FAILURE(DoMount());
  EXPECT_FALSE(init_access_token_);
  ASSERT_TRUE(verbose_logging_enabled_);
  EXPECT_FALSE(verbose_logging_enabled_.value());

  EXPECT_EQ(base::FilePath("/media/drivefsroot/salt-g-ID"),
            host_->GetMountPath());

  EXPECT_CALL(observer, OnUnmounted());
  EXPECT_CALL(*host_delegate_, OnUnmounted(_)).Times(0);
  base::RunLoop run_loop;
  delegate_.set_disconnect_handler(run_loop.QuitClosure());
  host_->Unmount();
  run_loop.Run();
}

TEST_F(DriveFsHostTest, EnableVerboseLogging) {
  ASSERT_FALSE(host_->IsMounted());

  host_delegate_->set_verbose_logging_enabled(true);
  ASSERT_NO_FATAL_FAILURE(DoMount());
  ASSERT_TRUE(verbose_logging_enabled_);
  EXPECT_TRUE(verbose_logging_enabled_.value());
}

TEST_F(DriveFsHostTest, GetMountPathWhileUnmounted) {
  EXPECT_EQ(base::FilePath("/media/fuse/drivefs-salt-g-ID"),
            host_->GetMountPath());
}

TEST_F(DriveFsHostTest, OnMountFailedFromMojo) {
  ASSERT_FALSE(host_->IsMounted());

  ASSERT_NO_FATAL_FAILURE(EstablishConnection());
  base::RunLoop run_loop;
  base::OnceClosure quit_closure = run_loop.QuitClosure();
  EXPECT_CALL(*host_delegate_, OnMountFailed(MountFailure::kUnknown, _))
      .WillOnce(RunOnceClosure(std::move(quit_closure)));
  SendMountFailed({});
  run_loop.Run();
  ASSERT_FALSE(host_->IsMounted());
}

TEST_F(DriveFsHostTest, OnMountFailedFromDbus) {
  ASSERT_FALSE(host_->IsMounted());
  EXPECT_CALL(*disk_manager_, UnmountPath(_, _)).Times(0);

  auto token = StartMount();

  base::RunLoop run_loop;
  base::OnceClosure quit_closure = run_loop.QuitClosure();
  EXPECT_CALL(*host_delegate_, OnMountFailed(MountFailure::kInvocation, _))
      .WillOnce(RunOnceClosure(std::move(quit_closure)));
  std::move(mount_callback_)
      .Run(ash::MountError::kInvalidMountOptions,
           {base::StrCat({"drivefs://", token}), "/media/drivefsroot/salt-g-ID",
            ash::MountType::kNetworkStorage});
  run_loop.Run();

  ASSERT_FALSE(host_->IsMounted());
  EXPECT_FALSE(mojo_bootstrap::PendingConnectionManager::Get().OpenIpcChannel(
      token, {}));
}

TEST_F(DriveFsHostTest, DestroyBeforeMojoConnection) {
  auto token = StartMount();
  CallMountCallbackSuccess(token);

  base::RunLoop run_loop;
  EXPECT_CALL(*disk_manager_, UnmountPath("/media/drivefsroot/salt-g-ID", _))
      .WillOnce(base::test::RunClosure(run_loop.QuitClosure()));

  host_.reset();
  EXPECT_FALSE(mojo_bootstrap::PendingConnectionManager::Get().OpenIpcChannel(
      token, {}));

  run_loop.Run();
}

TEST_F(DriveFsHostTest, MountWhileAlreadyMounted) {
  DoMount();
  EXPECT_FALSE(host_->Mount());
}

TEST_F(DriveFsHostTest, UnsupportedAccountTypes) {
  EXPECT_CALL(*disk_manager_, MountPath(_, _, _, _, _, _, _)).Times(0);
  const AccountId unsupported_accounts[] = {
      AccountId::FromUserEmail("[email protected]"),
  };
  for (auto& account : unsupported_accounts) {
    host_delegate_ = std::make_unique<TestingDriveFsHostDelegate>(
        identity_test_env_.identity_manager(), account);
    host_ = std::make_unique<DriveFsHost>(
        profile_path_, host_delegate_.get(), host_delegate_.get(),
        network_connection_tracker_.get(), &clock_, disk_manager_.get(),
        std::make_unique<base::MockOneShotTimer>());
    EXPECT_FALSE(host_->Mount());
    EXPECT_FALSE(host_->IsMounted());
  }
}

TEST_F(DriveFsHostTest, GetAccessToken_UnmountDuringMojoRequest) {
  ASSERT_NO_FATAL_FAILURE(DoMount());

  base::RunLoop run_loop;
  delegate_.set_disconnect_handler(run_loop.QuitClosure());
  delegate_->GetAccessToken(
      "client ID", "app ID", {"scope1", "scope2"},
      base::BindLambdaForTesting([](mojom::AccessTokenStatus status,
                                    const std::string& token) { FAIL(); }));
  host_->Unmount();
  run_loop.Run();
  EXPECT_FALSE(host_->IsMounted());

  EXPECT_FALSE(identity_test_env_.IsAccessTokenRequestPending());
}

ACTION_P(CloneStruct, output) {
  *output = arg0.Clone();
}

TEST_F(DriveFsHostTest, OnSyncingStatusUpdate_ForwardToObservers) {
  ASSERT_NO_FATAL_FAILURE(DoMount());
  MockDriveFsHostObserver observer;
  observer.Observe(host_.get());
  auto status = mojom::SyncingStatus::New();
  status->item_events.emplace_back(std::in_place, 12, 34, "filename.txt",
                                   kInProgress, 123, 456,
                                   mojom::ItemEventReason::kPin);
  mojom::SyncingStatusPtr observed_status;
  EXPECT_CALL(observer, OnSyncingStatusUpdate(_))
      .WillOnce(CloneStruct(&observed_status));
  delegate_->OnSyncingStatusUpdate(status.Clone());
  delegate_.FlushForTesting();
  testing::Mock::VerifyAndClear(&observer);

  EXPECT_EQ(status, observed_status);
}

ACTION_P(CloneVectorOfStructs, output) {
  for (auto& s : arg0) {
    output->emplace_back(s.Clone());
  }
}

TEST_F(DriveFsHostTest, OnFilesChanged_ForwardToObservers) {
  ASSERT_NO_FATAL_FAILURE(DoMount());
  MockDriveFsHostObserver observer;
  observer.Observe(host_.get());
  std::vector<mojom::FileChangePtr> changes;
  changes.emplace_back(std::in_place, base::FilePath("/create"),
                       mojom::FileChange::Type::kCreate);
  changes.emplace_back(std::in_place, base::FilePath("/delete"),
                       mojom::FileChange::Type::kDelete);
  changes.emplace_back(std::in_place, base::FilePath("/modify"),
                       mojom::FileChange::Type::kModify);
  std::vector<mojom::FileChangePtr> observed_changes;
  EXPECT_CALL(observer, OnFilesChanged(_))
      .WillOnce(CloneVectorOfStructs(&observed_changes));
  delegate_->OnFilesChanged(mojo::Clone(changes));
  delegate_.FlushForTesting();
  testing::Mock::VerifyAndClear(&observer);

  EXPECT_EQ(changes, observed_changes);
}

TEST_F(DriveFsHostTest, OnError_ForwardToObservers) {
  ASSERT_NO_FATAL_FAILURE(DoMount());
  MockDriveFsHostObserver observer;
  observer.Observe(host_.get());
  auto error =
      mojom::DriveError::New(mojom::DriveError::Type::kCantUploadStorageFull,
                             base::FilePath("/foo"), 1);
  mojom::DriveErrorPtr observed_error;
  EXPECT_CALL(observer, OnError(_)).WillOnce(CloneStruct(&observed_error));
  delegate_->OnError(error.Clone());
  delegate_.FlushForTesting();
  testing::Mock::VerifyAndClear(&observer);

  EXPECT_EQ(error, observed_error);
}

TEST_F(DriveFsHostTest, OnError_IgnoreUnknownErrorTypes) {
  ASSERT_NO_FATAL_FAILURE(DoMount());
  MockDriveFsHostObserver observer;
  observer.Observe(host_.get());
  EXPECT_CALL(observer, OnError(_)).Times(0);
  delegate_->OnError(mojom::DriveError::New(
      static_cast<mojom::DriveError::Type>(
          static_cast<std::underlying_type_t<mojom::DriveError::Type>>(
              mojom::DriveError::Type::kMaxValue) +
          1),
      base::FilePath("/foo"), 1));
  delegate_.FlushForTesting();
}

TEST_F(DriveFsHostTest, DisplayConfirmDialog_ForwardToHandler) {
  ASSERT_NO_FATAL_FAILURE(DoMount());
  auto reason = mojom::DialogReason::New(
      mojom::DialogReason::Type::kEnableDocsOffline, base::FilePath());
  mojom::DialogReasonPtr observed_reason;
  host_->set_dialog_handler(base::BindLambdaForTesting(
      [&](const mojom::DialogReason& reason,
          base::OnceCallback<void(mojom::DialogResult)> callback) {
        observed_reason = reason.Clone();
        std::move(callback).Run(mojom::DialogResult::kAccept);
      }));
  bool called = false;
  delegate_->DisplayConfirmDialog(
      reason.Clone(),
      base::BindLambdaForTesting([&](mojom::DialogResult result) {
        EXPECT_EQ(mojom::DialogResult::kAccept, result);
        called = true;
      }));
  delegate_.FlushForTesting();
  EXPECT_EQ(reason, observed_reason);
  EXPECT_TRUE(called);
}

TEST_F(DriveFsHostTest, DisplayConfirmDialogImpl_IgnoreIfNoHandler) {
  ASSERT_NO_FATAL_FAILURE(DoMount());
  bool called = false;
  delegate_->DisplayConfirmDialog(
      mojom::DialogReason::New(mojom::DialogReason::Type::kEnableDocsOffline,
                               base::FilePath()),
      base::BindLambdaForTesting([&](mojom::DialogResult result) {
        EXPECT_EQ(mojom::DialogResult::kNotDisplayed, result);
        called = true;
      }));
  delegate_.FlushForTesting();
  EXPECT_TRUE(called);
}

TEST_F(DriveFsHostTest, DisplayConfirmDialogImpl_IgnoreUnknownReasonTypes) {
  ASSERT_NO_FATAL_FAILURE(DoMount());
  host_->set_dialog_handler(
      base::BindRepeating([](const mojom::DialogReason&,
                             base::OnceCallback<void(mojom::DialogResult)>) {
        NOTREACHED_IN_MIGRATION();
      }));
  bool called = false;
  delegate_->DisplayConfirmDialog(
      mojom::DialogReason::New(
          static_cast<mojom::DialogReason::Type>(
              static_cast<std::underlying_type_t<mojom::DialogReason::Type>>(
                  mojom::DialogReason::Type::kMaxValue) +
              1),
          base::FilePath()),
      base::BindLambdaForTesting([&](mojom::DialogResult result) {
        EXPECT_EQ(mojom::DialogResult::kNotDisplayed, result);
        called = true;
      }));
  delegate_.FlushForTesting();
  EXPECT_TRUE(called);
}

TEST_F(DriveFsHostTest, Remount_CachedOnceOnly) {
  ASSERT_NO_FATAL_FAILURE(DoMount());

  // Request an access token.
  delegate_->GetAccessToken(
      "client ID", "app ID", {"scope1", "scope2"},
      base::BindLambdaForTesting(
          [&](mojom::AccessTokenStatus status, const std::string& token) {
            EXPECT_EQ(mojom::AccessTokenStatus::kSuccess, status);
            EXPECT_EQ("auth token", token);
          }));
  delegate_.FlushForTesting();
  EXPECT_TRUE(identity_test_env_.IsAccessTokenRequestPending());

  // Fulfill the request.
  identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
      "auth token", clock_.Now() + kTokenLifetime);
  EXPECT_FALSE(identity_test_env_.IsAccessTokenRequestPending());

  std::optional<base::TimeDelta> delay = base::Seconds(5);
  EXPECT_CALL(*host_delegate_, OnUnmounted(delay));
  SendOnUnmounted(delay);
  base::RunLoop().RunUntilIdle();
  ASSERT_NO_FATAL_FAILURE(DoUnmount());

  // Second mount attempt should reuse already available token.
  ASSERT_NO_FATAL_FAILURE(DoMount());
  EXPECT_FALSE(identity_test_env_.IsAccessTokenRequestPending());
  EXPECT_EQ("auth token", init_access_token_.value_or(""));

  // But if it asks for token again it goes to identity manager.
  delegate_->GetAccessToken(
      "client ID", "app ID", {"scope1", "scope2"},
      base::BindLambdaForTesting(
          [&](mojom::AccessTokenStatus status, const std::string& token) {
            EXPECT_EQ(mojom::AccessTokenStatus::kSuccess, status);
            EXPECT_EQ("auth token 2", token);
          }));
  delegate_.FlushForTesting();
  EXPECT_TRUE(identity_test_env_.IsAccessTokenRequestPending());

  // Fulfill the request with a different token.
  identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
      "auth token 2", clock_.Now() + kTokenLifetime);
  EXPECT_FALSE(identity_test_env_.IsAccessTokenRequestPending());
}

TEST_F(DriveFsHostTest, Remount_RequestInflight) {
  ASSERT_NO_FATAL_FAILURE(DoMount());

  delegate_->GetAccessToken(
      "client ID", "app ID", {"scope1", "scope2"},
      base::BindLambdaForTesting([&](mojom::AccessTokenStatus status,
                                     const std::string& token) { FAIL(); }));

  std::optional<base::TimeDelta> delay = base::Seconds(5);
  EXPECT_CALL(*host_delegate_, OnUnmounted(delay));
  SendOnUnmounted(delay);
  base::RunLoop().RunUntilIdle();
  ASSERT_NO_FATAL_FAILURE(DoUnmount());
  EXPECT_TRUE(identity_test_env_.IsAccessTokenRequestPending());

  // Now the response is ready.
  identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
      "auth token", clock_.Now() + kTokenLifetime);

  // Second mount will reuse previous token.
  ASSERT_NO_FATAL_FAILURE(DoMount());
  EXPECT_FALSE(identity_test_env_.IsAccessTokenRequestPending());
  EXPECT_EQ("auth token", init_access_token_.value_or(""));
}

TEST_F(DriveFsHostTest, Remount_RequestInflightCompleteAfterMount) {
  ASSERT_NO_FATAL_FAILURE(DoMount());

  delegate_->GetAccessToken(
      "client ID", "app ID", {"scope1", "scope2"},
      base::BindLambdaForTesting([&](mojom::AccessTokenStatus status,
                                     const std::string& token) { FAIL(); }));

  std::optional<base::TimeDelta> delay = base::Seconds(5);
  EXPECT_CALL(*host_delegate_, OnUnmounted(delay));
  SendOnUnmounted(delay);
  base::RunLoop().RunUntilIdle();
  ASSERT_NO_FATAL_FAILURE(DoUnmount());
  EXPECT_TRUE(identity_test_env_.IsAccessTokenRequestPending());

  // Second mount will reuse previous token.
  ASSERT_NO_FATAL_FAILURE(DoMount());
  EXPECT_FALSE(init_access_token_);
  EXPECT_TRUE(identity_test_env_.IsAccessTokenRequestPending());

  // Now the response is ready.
  identity_test_env_.WaitForAccessTokenRequestIfNecessaryAndRespondWithToken(
      "auth token", clock_.Now() + kTokenLifetime);
  EXPECT_FALSE(identity_test_env_.IsAccessTokenRequestPending());

  // A new request will reuse the cached token.
  delegate_->GetAccessToken(
      "client ID", "app ID", {"scope1", "scope2"},
      base::BindLambdaForTesting(
          [&](mojom::AccessTokenStatus status, const std::string& token) {
            EXPECT_EQ(mojom::AccessTokenStatus::kSuccess, status);
            EXPECT_EQ("auth token", token);
          }));
  delegate_.FlushForTesting();
  EXPECT_FALSE(identity_test_env_.IsAccessTokenRequestPending());
}

TEST_F(DriveFsHostTest, ConnectToExtension) {
  ASSERT_NO_FATAL_FAILURE(DoMount());

  mojo::Remote<mojom::NativeMessagingPort> remote;
  mojo::PendingRemote<mojom::NativeMessagingHost> host_remote;
  auto receiver = host_remote.InitWithNewPipeAndPassReceiver();

  base::RunLoop run_loop;
  delegate_->ConnectToExtension(
      mojom::ExtensionConnectionParams::New("foo"),
      remote.BindNewPipeAndPassReceiver(), std::move(host_remote),
      base::BindLambdaForTesting([&](mojom::ExtensionConnectionStatus status) {
        EXPECT_EQ(mojom::ExtensionConnectionStatus::kExtensionNotFound, status);
        run_loop.Quit();
      }));
  run_loop.Run();
  EXPECT_EQ("foo", host_delegate_->get_last_extension_params().extension_id);
}

TEST_F(DriveFsHostTest, OnMirrorSyncingStatusUpdate_ForwardToObservers) {
  ASSERT_NO_FATAL_FAILURE(DoMount());
  MockDriveFsHostObserver observer;
  observer.Observe(host_.get());
  auto status = mojom::SyncingStatus::New();
  status->item_events.emplace_back(std::in_place, 12, 34, "filename.txt",
                                   kInProgress, 123, 456,
                                   mojom::ItemEventReason::kPin);
  mojom::SyncingStatusPtr observed_status;
  EXPECT_CALL(observer, OnMirrorSyncingStatusUpdate(_))
      .WillOnce(CloneStruct(&observed_status));
  delegate_->OnMirrorSyncingStatusUpdate(status.Clone());
  delegate_.FlushForTesting();
  testing::Mock::VerifyAndClear(&observer);

  EXPECT_EQ(status, observed_status);
}

}  // namespace
}  // namespace drivefs