chromium/chrome/browser/ash/file_suggest/local_file_suggestion_provider_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/ash/file_suggest/local_file_suggestion_provider.h"

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/time/time.h"
#include "chrome/browser/ash/file_manager/file_tasks_observer.h"
#include "chrome/browser/ash/file_manager/path_util.h"
#include "chrome/browser/ash/file_manager/trash_common_util.h"
#include "chrome/browser/ash/file_suggest/file_suggest_util.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.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"

namespace ash::test {
namespace {

using file_manager::file_tasks::FileTasksObserver;
using testing::UnorderedElementsAre;

FileTasksObserver::FileOpenEvent OpenEvent(const base::FilePath& path) {
  FileTasksObserver::FileOpenEvent e;
  e.path = path;
  e.open_type = FileTasksObserver::OpenType::kOpen;
  return e;
}

MATCHER_P(FilePathMatcher, file_path, "") {
  return arg.file_path == file_path;
}

}  // namespace

class LocalFileSuggestionProviderTest : public testing::Test {
 public:
  void OnSuggestionsUpdated(FileSuggestionType type) {
    ASSERT_TRUE(type == FileSuggestionType::kLocalFile);
  }

  base::FilePath Path(const std::string& filename) {
    return profile_->GetPath().AppendASCII(filename);
  }

  void WriteFile(const base::FilePath& path) {
    CHECK(base::WriteFile(path, "abcd"));
    CHECK(base::PathExists(path));
    Wait();
  }

  void Wait() { task_environment_.RunUntilIdle(); }

  void WaitForProviderToBeInitialized() {
    while (!provider_->IsInitialized()) {
      Wait();
    }
  }

  void UpdateResults() {
    base::RunLoop run_loop;
    auto cb = base::BindLambdaForTesting(
        [&](const std::optional<std::vector<FileSuggestData>>& data) {
          results_ = data;
          run_loop.Quit();
        });
    provider_->GetSuggestFileData(std::move(cb));
    run_loop.Run();
  }

  std::optional<std::vector<FileSuggestData>>& Results() { return results_; }

  LocalFileSuggestionProvider* GetProvider() { return provider_.get(); }

 protected:
  void SetUp() override {
    testing_profile_manager_ = std::make_unique<TestingProfileManager>(
        TestingBrowserProcess::GetGlobal());
    EXPECT_TRUE(testing_profile_manager_->SetUp());
    profile_ = testing_profile_manager_->CreateTestingProfile(
        "primary_profile@test", {});
    provider_ = std::make_unique<LocalFileSuggestionProvider>(
        profile_, base::BindRepeating(
                      &LocalFileSuggestionProviderTest::OnSuggestionsUpdated,
                      base::Unretained(this)));
    UpdateResults();
    WaitForProviderToBeInitialized();
  }

  raw_ptr<TestingProfile, DanglingUntriaged> profile_;

 private:
  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<TestingProfileManager> testing_profile_manager_;
  std::unique_ptr<LocalFileSuggestionProvider> provider_;
  std::optional<std::vector<FileSuggestData>> results_;
};

TEST_F(LocalFileSuggestionProviderTest, ResultsEmptyOnInitialization) {
  // The results fetched before initialization should have no value.
  EXPECT_FALSE(Results().has_value());

  // Now that the provider is initialized, results should be empty but not null.
  UpdateResults();
  ASSERT_TRUE(Results().has_value());
  EXPECT_EQ(Results()->size(), 0u);
}

TEST_F(LocalFileSuggestionProviderTest, ResultsAreOnlyOpenedFiles) {
  WriteFile(Path("exists_1.txt"));
  WriteFile(Path("exists_2.png"));
  WriteFile(Path("exists_3.pdf"));

  // Results are only added if they exist and have been opened at least once.
  GetProvider()->OnFilesOpened({OpenEvent(Path("exists_1.txt")),
                                OpenEvent(Path("exists_2.png")),
                                OpenEvent(Path("nonexistent.txt"))});

  UpdateResults();

  ASSERT_TRUE(Results().has_value());
  EXPECT_THAT(Results().value(),
              UnorderedElementsAre(FilePathMatcher(Path("exists_1.txt")),
                                   FilePathMatcher(Path("exists_2.png"))));
}

TEST_F(LocalFileSuggestionProviderTest, OldFilesNotReturned) {
  WriteFile(Path("new.txt"));
  WriteFile(Path("old.png"));
  auto now = base::Time::Now();
  base::TouchFile(Path("old.png"), now, now - GetMaxFileSuggestionRecency());

  GetProvider()->OnFilesOpened(
      {OpenEvent(Path("new.txt")), OpenEvent(Path("old.png"))});

  UpdateResults();

  ASSERT_TRUE(Results().has_value());
  EXPECT_THAT(Results().value(),
              UnorderedElementsAre(FilePathMatcher(Path("new.txt"))));
}

class LocalFileSuggestionProviderTrashTest
    : public LocalFileSuggestionProviderTest {
 public:
  LocalFileSuggestionProviderTrashTest() = default;

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

  void SetUp() override {
    LocalFileSuggestionProviderTest::SetUp();

    trash_folder_ =
        profile_->GetPath().Append(file_manager::trash::kTrashFolderName);
    ASSERT_TRUE(base::CreateDirectory(trash_folder_));

    // Ensure the MyFiles and Downloads mount points are appropriately mocked
    // to allow the trash locations to be parented at the test directory.
    storage::ExternalMountPoints::GetSystemInstance()->RegisterFileSystem(
        file_manager::util::GetDownloadsMountPointName(profile_),
        storage::kFileSystemTypeLocal, storage::FileSystemMountOption(),
        profile_->GetPath());
  }

  base::FilePath TrashPath(const std::string& filename) {
    return trash_folder_.Append(filename);
  }

  void ToggleTrash(bool enabled) {
    profile_->GetPrefs()->SetBoolean(ash::prefs::kFilesAppTrashEnabled,
                                     enabled);
  }

 private:
  base::FilePath trash_folder_;
};

TEST_F(LocalFileSuggestionProviderTrashTest,
       WhenTrashEnabledFilesInTrashAreIgnored) {
  ToggleTrash(true);

  WriteFile(Path("exists_1.txt"));
  WriteFile(Path("exists_2.png"));
  WriteFile(TrashPath("trashed_file"));

  // Results are only added if they exist and have been opened at least once.
  GetProvider()->OnFilesOpened({OpenEvent(Path("exists_1.txt")),
                                OpenEvent(Path("exists_2.png")),
                                OpenEvent(TrashPath("trashed_file"))});

  UpdateResults();

  ASSERT_TRUE(Results().has_value());
  EXPECT_THAT(Results().value(),
              UnorderedElementsAre(FilePathMatcher(Path("exists_1.txt")),
                                   FilePathMatcher(Path("exists_2.png"))));
}

TEST_F(LocalFileSuggestionProviderTrashTest,
       WhenTrashDisabledTrashFilesAreIncluded) {
  ToggleTrash(false);

  WriteFile(Path("exists_1.txt"));
  WriteFile(Path("exists_2.png"));
  WriteFile(TrashPath("trashed_file"));

  // Results are only added if they exist and have been opened at least once.
  GetProvider()->OnFilesOpened({OpenEvent(Path("exists_1.txt")),
                                OpenEvent(Path("exists_2.png")),
                                OpenEvent(TrashPath("trashed_file"))});

  UpdateResults();

  ASSERT_TRUE(Results().has_value());
  EXPECT_THAT(Results().value(),
              UnorderedElementsAre(FilePathMatcher(Path("exists_1.txt")),
                                   FilePathMatcher(Path("exists_2.png")),
                                   FilePathMatcher(TrashPath("trashed_file"))));
}

}  // namespace ash::test