| // Copyright 2019 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/ash/smb_client/smbfs_share.h" |
| |
| #include <utility> |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_split.h" |
| #include "base/test/bind.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/task_environment.h" |
| #include "chrome/browser/ash/file_manager/volume_manager.h" |
| #include "chrome/browser/ash/file_manager/volume_manager_factory.h" |
| #include "chrome/browser/ash/file_manager/volume_manager_observer.h" |
| #include "chrome/browser/ash/profiles/profile_helper.h" |
| #include "chrome/browser/ash/smb_client/smb_url.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "chromeos/ash/components/disks/disk_mount_manager.h" |
| #include "chromeos/ash/components/disks/fake_disk_mount_manager.h" |
| #include "chromeos/ash/components/disks/mount_point.h" |
| #include "chromeos/ash/components/smbfs/smbfs_host.h" |
| #include "chromeos/ash/components/smbfs/smbfs_mounter.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "storage/browser/file_system/external_mount_points.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using testing::_; |
| using testing::AllOf; |
| using testing::Property; |
| using testing::Unused; |
| |
| namespace ash::smb_client { |
| namespace { |
| |
| constexpr char kSharePath[] = "smb://share/path"; |
| constexpr char kSharePath2[] = "smb://share/path2"; |
| constexpr char kShareUsername[] = "user"; |
| constexpr char kShareUsername2[] = "user2"; |
| constexpr char kShareWorkgroup[] = "workgroup"; |
| constexpr char kKerberosIdentity[] = "my-kerberos-identity"; |
| constexpr char kDisplayName[] = "Public"; |
| constexpr char kMountPath[] = "/share/mount/path"; |
| constexpr char kFileName[] = "file_name.ext"; |
| constexpr char kMountIdHashSeparator[] = "#"; |
| |
| // Creates a new VolumeManager for tests. |
| // By default, VolumeManager KeyedService is null for testing. |
| std::unique_ptr<KeyedService> BuildVolumeManager( |
| content::BrowserContext* context) { |
| return std::make_unique<file_manager::VolumeManager>( |
| Profile::FromBrowserContext(context), |
| nullptr /* drive_integration_service */, |
| nullptr /* power_manager_client */, |
| disks::DiskMountManager::GetInstance(), |
| nullptr /* file_system_provider_service */, |
| file_manager::VolumeManager::GetMtpStorageInfoCallback()); |
| } |
| |
| class MockVolumeManagerObsever : public file_manager::VolumeManagerObserver { |
| public: |
| MOCK_METHOD(void, |
| OnVolumeMounted, |
| (MountError error_code, const file_manager::Volume& volume), |
| (override)); |
| MOCK_METHOD(void, |
| OnVolumeUnmounted, |
| (MountError error_code, const file_manager::Volume& volume), |
| (override)); |
| }; |
| |
| class MockSmbFsMounter : public smbfs::SmbFsMounter { |
| public: |
| MOCK_METHOD(void, |
| Mount, |
| (smbfs::SmbFsMounter::DoneCallback callback), |
| (override)); |
| }; |
| |
| class TestSmbFsImpl : public smbfs::mojom::SmbFs { |
| public: |
| MOCK_METHOD(void, |
| RemoveSavedCredentials, |
| (RemoveSavedCredentialsCallback), |
| (override)); |
| |
| MOCK_METHOD(void, |
| DeleteRecursively, |
| (const base::FilePath&, DeleteRecursivelyCallback), |
| (override)); |
| }; |
| |
| } // namespace |
| |
| class SmbFsShareTest : public testing::Test { |
| protected: |
| static void SetUpTestSuite() { |
| disks::DiskMountManager::InitializeForTesting(disk_mount_manager()); |
| } |
| |
| void SetUp() override { |
| file_manager::VolumeManagerFactory::GetInstance()->SetTestingFactory( |
| &profile_, base::BindRepeating(&BuildVolumeManager)); |
| |
| file_manager::VolumeManager::Get(&profile_)->AddObserver(&observer_); |
| |
| mounter_creation_callback_ = base::BindLambdaForTesting( |
| [this](const std::string& share_path, const std::string& mount_dir_name, |
| const SmbFsShare::MountOptions& options, |
| smbfs::SmbFsHost::Delegate* delegate) |
| -> std::unique_ptr<smbfs::SmbFsMounter> { |
| EXPECT_EQ(share_path, kSharePath); |
| return std::move(mounter_); |
| }); |
| } |
| |
| void TearDown() override { |
| file_manager::VolumeManager::Get(&profile_)->RemoveObserver(&observer_); |
| } |
| |
| static disks::FakeDiskMountManager* disk_mount_manager() { |
| static disks::FakeDiskMountManager* manager = |
| new disks::FakeDiskMountManager(); |
| return manager; |
| } |
| |
| std::unique_ptr<smbfs::SmbFsHost> CreateSmbFsHost( |
| SmbFsShare* share, |
| mojo::Receiver<smbfs::mojom::SmbFs>* smbfs_receiver, |
| mojo::Remote<smbfs::mojom::SmbFsDelegate>* delegate) { |
| return std::make_unique<smbfs::SmbFsHost>( |
| std::make_unique<disks::MountPoint>(base::FilePath(kMountPath), |
| disk_mount_manager()), |
| share, |
| mojo::Remote<smbfs::mojom::SmbFs>( |
| smbfs_receiver->BindNewPipeAndPassRemote()), |
| delegate->BindNewPipeAndPassReceiver()); |
| } |
| |
| content::BrowserTaskEnvironment task_environment_{ |
| content::BrowserTaskEnvironment::REAL_IO_THREAD, |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| TestingProfile profile_; |
| MockVolumeManagerObsever observer_; |
| |
| SmbFsShare::MounterCreationCallback mounter_creation_callback_; |
| std::unique_ptr<MockSmbFsMounter> mounter_ = |
| std::make_unique<MockSmbFsMounter>(); |
| raw_ptr<MockSmbFsMounter, DanglingUntriaged> raw_mounter_ = mounter_.get(); |
| }; |
| |
| TEST_F(SmbFsShareTest, Mount) { |
| TestSmbFsImpl smbfs; |
| mojo::Receiver<smbfs::mojom::SmbFs> smbfs_receiver(&smbfs); |
| mojo::Remote<smbfs::mojom::SmbFsDelegate> delegate; |
| |
| SmbFsShare share(&profile_, SmbUrl(kSharePath), kDisplayName, {}); |
| share.SetMounterCreationCallbackForTest(mounter_creation_callback_); |
| |
| EXPECT_CALL(*raw_mounter_, Mount(_)) |
| .WillOnce([this, &share, &smbfs_receiver, |
| &delegate](smbfs::SmbFsMounter::DoneCallback callback) { |
| std::move(callback).Run( |
| smbfs::mojom::MountError::kOk, |
| CreateSmbFsHost(&share, &smbfs_receiver, &delegate)); |
| }); |
| EXPECT_CALL( |
| observer_, |
| OnVolumeMounted( |
| MountError::kSuccess, |
| AllOf(Property(&file_manager::Volume::type, |
| file_manager::VOLUME_TYPE_SMB), |
| Property(&file_manager::Volume::mount_path, |
| base::FilePath(kMountPath)), |
| Property(&file_manager::Volume::volume_label, kDisplayName)))) |
| .Times(1); |
| EXPECT_CALL(observer_, OnVolumeUnmounted( |
| MountError::kSuccess, |
| AllOf(Property(&file_manager::Volume::type, |
| file_manager::VOLUME_TYPE_SMB), |
| Property(&file_manager::Volume::mount_path, |
| base::FilePath(kMountPath))))) |
| .Times(1); |
| |
| base::RunLoop run_loop; |
| share.Mount(base::BindLambdaForTesting([&run_loop](SmbMountResult result) { |
| EXPECT_EQ(result, SmbMountResult::kSuccess); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| |
| EXPECT_TRUE(share.IsMounted()); |
| EXPECT_EQ(share.share_url().ToString(), kSharePath); |
| EXPECT_EQ(share.mount_path(), base::FilePath(kMountPath)); |
| |
| storage::ExternalMountPoints* const mount_points = |
| storage::ExternalMountPoints::GetSystemInstance(); |
| base::FilePath virtual_path; |
| EXPECT_TRUE(mount_points->GetVirtualPath( |
| base::FilePath(kMountPath).Append(kFileName), &virtual_path)); |
| } |
| |
| TEST_F(SmbFsShareTest, MountFailure) { |
| EXPECT_CALL(*raw_mounter_, Mount(_)) |
| .WillOnce([](smbfs::SmbFsMounter::DoneCallback callback) { |
| std::move(callback).Run(smbfs::mojom::MountError::kTimeout, nullptr); |
| }); |
| EXPECT_CALL(observer_, OnVolumeMounted(MountError::kSuccess, _)).Times(0); |
| EXPECT_CALL(observer_, OnVolumeUnmounted(MountError::kSuccess, _)).Times(0); |
| |
| SmbFsShare share(&profile_, SmbUrl(kSharePath), kDisplayName, {}); |
| share.SetMounterCreationCallbackForTest(mounter_creation_callback_); |
| |
| base::RunLoop run_loop; |
| share.Mount(base::BindLambdaForTesting([&run_loop](SmbMountResult result) { |
| EXPECT_EQ(result, SmbMountResult::kAborted); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| |
| EXPECT_FALSE(share.IsMounted()); |
| EXPECT_EQ(share.share_url().ToString(), kSharePath); |
| EXPECT_EQ(share.mount_path(), base::FilePath()); |
| } |
| |
| TEST_F(SmbFsShareTest, UnmountOnDisconnect) { |
| TestSmbFsImpl smbfs; |
| mojo::Receiver<smbfs::mojom::SmbFs> smbfs_receiver(&smbfs); |
| mojo::Remote<smbfs::mojom::SmbFsDelegate> delegate; |
| |
| SmbFsShare share(&profile_, SmbUrl(kSharePath), kDisplayName, {}); |
| share.SetMounterCreationCallbackForTest(mounter_creation_callback_); |
| |
| EXPECT_CALL(*raw_mounter_, Mount(_)) |
| .WillOnce([this, &share, &smbfs_receiver, |
| &delegate](smbfs::SmbFsMounter::DoneCallback callback) { |
| std::move(callback).Run( |
| smbfs::mojom::MountError::kOk, |
| CreateSmbFsHost(&share, &smbfs_receiver, &delegate)); |
| }); |
| |
| EXPECT_CALL( |
| observer_, |
| OnVolumeMounted( |
| MountError::kSuccess, |
| AllOf(Property(&file_manager::Volume::type, |
| file_manager::VOLUME_TYPE_SMB), |
| Property(&file_manager::Volume::mount_path, |
| base::FilePath(kMountPath)), |
| Property(&file_manager::Volume::volume_label, kDisplayName)))) |
| .Times(1); |
| base::RunLoop run_loop; |
| EXPECT_CALL(observer_, OnVolumeUnmounted( |
| MountError::kSuccess, |
| AllOf(Property(&file_manager::Volume::type, |
| file_manager::VOLUME_TYPE_SMB), |
| Property(&file_manager::Volume::mount_path, |
| base::FilePath(kMountPath))))) |
| .WillOnce(base::test::RunClosure(run_loop.QuitClosure())); |
| |
| share.Mount( |
| base::BindLambdaForTesting([&smbfs_receiver](SmbMountResult result) { |
| EXPECT_EQ(result, SmbMountResult::kSuccess); |
| |
| // Disconnect the Mojo service which should trigger the unmount. |
| smbfs_receiver.reset(); |
| })); |
| run_loop.Run(); |
| } |
| |
| TEST_F(SmbFsShareTest, DisallowCredentialsDialogByDefault) { |
| TestSmbFsImpl smbfs; |
| mojo::Receiver<smbfs::mojom::SmbFs> smbfs_receiver(&smbfs); |
| mojo::Remote<smbfs::mojom::SmbFsDelegate> delegate; |
| |
| SmbFsShare share(&profile_, SmbUrl(kSharePath), kDisplayName, {}); |
| share.SetMounterCreationCallbackForTest(mounter_creation_callback_); |
| |
| EXPECT_CALL(*raw_mounter_, Mount(_)) |
| .WillOnce([this, &share, &smbfs_receiver, |
| &delegate](smbfs::SmbFsMounter::DoneCallback callback) { |
| std::move(callback).Run( |
| smbfs::mojom::MountError::kOk, |
| CreateSmbFsHost(&share, &smbfs_receiver, &delegate)); |
| }); |
| EXPECT_CALL(observer_, OnVolumeMounted(MountError::kSuccess, _)).Times(1); |
| EXPECT_CALL(observer_, OnVolumeUnmounted(MountError::kSuccess, _)).Times(1); |
| |
| { |
| base::RunLoop run_loop; |
| share.Mount(base::BindLambdaForTesting([&run_loop](SmbMountResult result) { |
| EXPECT_EQ(result, SmbMountResult::kSuccess); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| base::RunLoop run_loop; |
| delegate->RequestCredentials(base::BindLambdaForTesting( |
| [&run_loop](smbfs::mojom::CredentialsPtr creds) { |
| EXPECT_FALSE(creds); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| TEST_F(SmbFsShareTest, DisallowCredentialsDialogAfterTimeout) { |
| TestSmbFsImpl smbfs; |
| mojo::Receiver<smbfs::mojom::SmbFs> smbfs_receiver(&smbfs); |
| mojo::Remote<smbfs::mojom::SmbFsDelegate> delegate; |
| |
| SmbFsShare share(&profile_, SmbUrl(kSharePath), kDisplayName, {}); |
| share.SetMounterCreationCallbackForTest(mounter_creation_callback_); |
| |
| EXPECT_CALL(*raw_mounter_, Mount(_)) |
| .WillOnce([this, &share, &smbfs_receiver, |
| &delegate](smbfs::SmbFsMounter::DoneCallback callback) { |
| std::move(callback).Run( |
| smbfs::mojom::MountError::kOk, |
| CreateSmbFsHost(&share, &smbfs_receiver, &delegate)); |
| }); |
| EXPECT_CALL(observer_, OnVolumeMounted(MountError::kSuccess, _)).Times(1); |
| EXPECT_CALL(observer_, OnVolumeUnmounted(MountError::kSuccess, _)).Times(1); |
| |
| { |
| base::RunLoop run_loop; |
| share.Mount(base::BindLambdaForTesting([&run_loop](SmbMountResult result) { |
| EXPECT_EQ(result, SmbMountResult::kSuccess); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| share.AllowCredentialsRequest(); |
| // Fast-forward time for the allow state to timeout. The timeout is 5 seconds, |
| // so moving forward by 6 will ensure the timeout runs. |
| task_environment_.FastForwardBy(base::Seconds(6)); |
| |
| base::RunLoop run_loop; |
| delegate->RequestCredentials(base::BindLambdaForTesting( |
| [&run_loop](smbfs::mojom::CredentialsPtr creds) { |
| EXPECT_FALSE(creds); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| TEST_F(SmbFsShareTest, RemoveSavedCredentials) { |
| TestSmbFsImpl smbfs; |
| mojo::Receiver<smbfs::mojom::SmbFs> smbfs_receiver(&smbfs); |
| mojo::Remote<smbfs::mojom::SmbFsDelegate> delegate; |
| |
| SmbFsShare share(&profile_, SmbUrl(kSharePath), kDisplayName, {}); |
| share.SetMounterCreationCallbackForTest(mounter_creation_callback_); |
| |
| EXPECT_CALL(*raw_mounter_, Mount(_)) |
| .WillOnce([this, &share, &smbfs_receiver, |
| &delegate](smbfs::SmbFsMounter::DoneCallback callback) { |
| std::move(callback).Run( |
| smbfs::mojom::MountError::kOk, |
| CreateSmbFsHost(&share, &smbfs_receiver, &delegate)); |
| }); |
| EXPECT_CALL(observer_, OnVolumeMounted(MountError::kSuccess, _)).Times(1); |
| EXPECT_CALL(observer_, OnVolumeUnmounted(MountError::kSuccess, _)).Times(1); |
| |
| { |
| base::RunLoop run_loop; |
| share.Mount(base::BindLambdaForTesting([&run_loop](SmbMountResult result) { |
| EXPECT_EQ(result, SmbMountResult::kSuccess); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| EXPECT_CALL(smbfs, RemoveSavedCredentials(_)) |
| .WillOnce(base::test::RunOnceCallback<0>(true /* success */)); |
| { |
| base::RunLoop run_loop; |
| share.RemoveSavedCredentials( |
| base::BindLambdaForTesting([&run_loop](bool success) { |
| EXPECT_TRUE(success); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| } |
| |
| TEST_F(SmbFsShareTest, RemoveSavedCredentials_Disconnect) { |
| TestSmbFsImpl smbfs; |
| mojo::Receiver<smbfs::mojom::SmbFs> smbfs_receiver(&smbfs); |
| mojo::Remote<smbfs::mojom::SmbFsDelegate> delegate; |
| |
| SmbFsShare share(&profile_, SmbUrl(kSharePath), kDisplayName, {}); |
| share.SetMounterCreationCallbackForTest(mounter_creation_callback_); |
| |
| EXPECT_CALL(*raw_mounter_, Mount(_)) |
| .WillOnce([this, &share, &smbfs_receiver, |
| &delegate](smbfs::SmbFsMounter::DoneCallback callback) { |
| std::move(callback).Run( |
| smbfs::mojom::MountError::kOk, |
| CreateSmbFsHost(&share, &smbfs_receiver, &delegate)); |
| }); |
| EXPECT_CALL(observer_, OnVolumeMounted(MountError::kSuccess, _)).Times(1); |
| EXPECT_CALL(observer_, OnVolumeUnmounted(MountError::kSuccess, _)).Times(1); |
| |
| { |
| base::RunLoop run_loop; |
| share.Mount(base::BindLambdaForTesting([&run_loop](SmbMountResult result) { |
| EXPECT_EQ(result, SmbMountResult::kSuccess); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| |
| EXPECT_CALL(smbfs, RemoveSavedCredentials(_)) |
| .WillOnce([&smbfs_receiver](Unused) { smbfs_receiver.reset(); }); |
| { |
| base::RunLoop run_loop; |
| share.RemoveSavedCredentials( |
| base::BindLambdaForTesting([&run_loop](bool success) { |
| EXPECT_FALSE(success); |
| run_loop.Quit(); |
| })); |
| run_loop.Run(); |
| } |
| } |
| |
| TEST_F(SmbFsShareTest, GenerateStableMountIdInput) { |
| TestSmbFsImpl smbfs; |
| |
| std::string profile_user_hash = |
| ProfileHelper::Get()->GetUserIdHashFromProfile(&profile_); |
| |
| smbfs::SmbFsMounter::MountOptions options1; |
| options1.username = kShareUsername; |
| options1.workgroup = kShareWorkgroup; |
| SmbFsShare share1(&profile_, SmbUrl(kSharePath), kDisplayName, options1); |
| |
| std::string hash_input1 = share1.GenerateStableMountIdInput(); |
| std::vector<std::string> tokens1 = |
| base::SplitString(hash_input1, kMountIdHashSeparator, |
| base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); |
| EXPECT_EQ(tokens1.size(), 5u); |
| EXPECT_EQ(tokens1[0], profile_user_hash); |
| EXPECT_EQ(tokens1[1], SmbUrl(kSharePath).ToString()); |
| EXPECT_EQ(tokens1[2], "0" /* kerberos */); |
| EXPECT_EQ(tokens1[3], kShareWorkgroup); |
| EXPECT_EQ(tokens1[4], kShareUsername); |
| |
| smbfs::SmbFsMounter::MountOptions options2; |
| options2.kerberos_options = |
| std::make_optional<smbfs::SmbFsMounter::KerberosOptions>( |
| smbfs::SmbFsMounter::KerberosOptions::Source::kKerberos, |
| kKerberosIdentity); |
| SmbFsShare share2(&profile_, SmbUrl(kSharePath2), kDisplayName, options2); |
| |
| std::string hash_input2 = share2.GenerateStableMountIdInput(); |
| std::vector<std::string> tokens2 = |
| base::SplitString(hash_input2, kMountIdHashSeparator, |
| base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); |
| EXPECT_EQ(tokens2.size(), 5u); |
| EXPECT_EQ(tokens2[0], profile_user_hash); |
| EXPECT_EQ(tokens2[1], SmbUrl(kSharePath2).ToString()); |
| EXPECT_EQ(tokens2[2], "1" /* kerberos */); |
| EXPECT_EQ(tokens2[3], ""); |
| EXPECT_EQ(tokens2[4], ""); |
| } |
| |
| TEST_F(SmbFsShareTest, GenerateStableMountId) { |
| TestSmbFsImpl smbfs; |
| |
| smbfs::SmbFsMounter::MountOptions options1; |
| options1.username = kShareUsername; |
| SmbFsShare share1(&profile_, SmbUrl(kSharePath), kDisplayName, options1); |
| std::string mount_id1 = share1.GenerateStableMountId(); |
| |
| // Check: We get a different hash when options are varied. |
| smbfs::SmbFsMounter::MountOptions options2; |
| options2.username = kShareUsername2; |
| SmbFsShare share2(&profile_, SmbUrl(kSharePath), kDisplayName, options2); |
| std::string mount_id2 = share2.GenerateStableMountId(); |
| EXPECT_TRUE(mount_id1.compare(mount_id2)); |
| |
| // Check: String is 64 characters long (SHA256 encoded as hex). |
| EXPECT_EQ(mount_id1.size(), 64u); |
| EXPECT_EQ(mount_id2.size(), 64u); |
| } |
| |
| } // namespace ash::smb_client |