chromium/chromeos/ash/components/file_manager/indexing/index_storage_unittest.cc

// Copyright 2024 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/file_manager/indexing/index_storage.h"

#include <memory>
#include <optional>

#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
#include "base/time/time.h"
#include "chromeos/ash/components/file_manager/indexing/ram_storage.h"
#include "chromeos/ash/components/file_manager/indexing/sql_storage.h"
#include "chromeos/ash/components/file_manager/indexing/term.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest-spi.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace ash::file_manager {
namespace {

// Enumerates the types of storage technology used by test.
enum StorageType {
  RAM = 0,
  SQL,
};

static constexpr base::Time::Exploded kTestTimeExploded = {
    .year = 2020,
    .month = 11,
    .day_of_month = 4,
};

const base::FilePath::CharType kDatabaseName[] =
    FILE_PATH_LITERAL("SqlStorageTest.db");

class IndexStorageTest : public testing::TestWithParam<StorageType> {
 public:
  IndexStorageTest()
      : pinned_("label", u"pinned"), downloaded_("label", u"downloaded") {}

  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    if (GetParam() == StorageType::RAM) {
      storage_ = std::make_unique<RamStorage>();
    } else {
      storage_ = std::make_unique<SqlStorage>(db_file_path(), "test_uma_tag");
    }
    foo_url_ =
        GURL("filesystem:file://file-manager/external/Downloads-u123/foo.txt");
    bar_url_ =
        GURL("filesystem:file://file-manager/external/Downloads-u123/bar.png");
    EXPECT_TRUE(
        base::Time::FromUTCExploded(kTestTimeExploded, &foo_modified_time_));
  }

  void TearDown() override { EXPECT_TRUE(temp_dir_.Delete()); }

  base::FilePath db_file_path() {
    return temp_dir_.GetPath().Append(kDatabaseName);
  }

 protected:
  Term pinned_;
  Term downloaded_;
  GURL foo_url_;
  GURL bar_url_;
  base::Time foo_modified_time_;
  base::ScopedTempDir temp_dir_;
  std::unique_ptr<IndexStorage> storage_;
};

TEST_P(IndexStorageTest, Init) {
  EXPECT_TRUE(storage_->Init());
}

TEST_P(IndexStorageTest, Close) {
  EXPECT_TRUE(storage_->Close());
}

TEST_P(IndexStorageTest, GetTokenId) {
  // Must initialize before use.
  ASSERT_TRUE(storage_->Init());

  EXPECT_EQ(storage_->GetTokenId("foo"), -1);
  EXPECT_EQ(storage_->GetOrCreateTokenId("foo"), 1);
  EXPECT_EQ(storage_->GetTokenId("foo"), 1);
  // Adding the same token twice does not create a second version of "foo".
  EXPECT_EQ(storage_->GetOrCreateTokenId("foo"), 1);
}

TEST_P(IndexStorageTest, GetTermId) {
  // Must initialize before use.
  ASSERT_TRUE(storage_->Init());

  EXPECT_EQ(storage_->GetTermId(pinned_), -1);
  EXPECT_EQ(storage_->GetOrCreateTermId(pinned_), 1);
  EXPECT_EQ(storage_->GetTermId(pinned_), 1);
  EXPECT_EQ(storage_->GetTermId(downloaded_), -1);
  EXPECT_EQ(storage_->GetOrCreateTermId(downloaded_), 2);
}

TEST_P(IndexStorageTest, GetOrCreateUrlId) {
  // Must initialize before use.
  ASSERT_TRUE(storage_->Init());

  EXPECT_EQ(storage_->GetOrCreateUrlId(foo_url_), 1);
}

TEST_P(IndexStorageTest, GetUrlId) {
  // Must initialize before use.
  ASSERT_TRUE(storage_->Init());

  EXPECT_EQ(storage_->GetUrlId(foo_url_), -1);
  EXPECT_EQ(storage_->GetOrCreateUrlId(foo_url_), 1);
  EXPECT_EQ(storage_->GetUrlId(foo_url_), 1);
}

TEST_P(IndexStorageTest, DeleteUrl) {
  // Must initialize before use.
  ASSERT_TRUE(storage_->Init());

  EXPECT_EQ(storage_->DeleteUrl(foo_url_), -1);
  EXPECT_EQ(storage_->GetOrCreateUrlId(foo_url_), 1);
  EXPECT_EQ(storage_->DeleteUrl(foo_url_), 1);
}

TEST_P(IndexStorageTest, MoveUrl) {
  // Must initialize before use.
  ASSERT_TRUE(storage_->Init());

  // Cannot move non-existing URL.
  EXPECT_EQ(storage_->MoveUrl(foo_url_, bar_url_), -1);

  // Store a URL and a file info connected to it.
  EXPECT_EQ(storage_->GetOrCreateUrlId(foo_url_), 1);
  FileInfo foo_file_info(foo_url_, 100, base::Time());
  EXPECT_EQ(1, storage_->PutFileInfo(foo_file_info));

  // Move foo_url_ to foo_url_.
  EXPECT_EQ(storage_->MoveUrl(foo_url_, foo_url_), 1);

  // Move foo_url_ to bar_url_.
  EXPECT_EQ(storage_->MoveUrl(foo_url_, bar_url_), 1);

  // Expect to be able to retrieve foo_file_info by bar_url_.
  std::optional<FileInfo> file_info = storage_->GetFileInfo(1);
  EXPECT_TRUE(file_info.has_value());
  EXPECT_EQ(file_info.value().file_url, bar_url_);

  // Expect that one no longer can find foo_url_.
  EXPECT_EQ(-1, storage_->GetUrlId(foo_url_));
}

TEST_P(IndexStorageTest, GetFileInfo) {
  // Must initialize before use.
  ASSERT_TRUE(storage_->Init());

  int64_t foo_url_id = storage_->GetOrCreateUrlId(foo_url_);

  EXPECT_FALSE(storage_->GetFileInfo(-1).has_value());
  EXPECT_FALSE(storage_->GetFileInfo(foo_url_id).has_value());

  FileInfo put_file_info(foo_url_, 100, base::Time());
  EXPECT_FALSE(put_file_info.remote_id.has_value());
  EXPECT_EQ(foo_url_id, storage_->PutFileInfo(put_file_info));
  std::optional<FileInfo> file_info = storage_->GetFileInfo(foo_url_id);
  EXPECT_TRUE(file_info.has_value());
  EXPECT_EQ(put_file_info.file_url, file_info.value().file_url);
  EXPECT_EQ(put_file_info.last_modified, file_info.value().last_modified);
  EXPECT_EQ(put_file_info.size, file_info.value().size);
  EXPECT_FALSE(file_info.value().remote_id.has_value());

  put_file_info.size = put_file_info.size + 100;
  EXPECT_EQ(foo_url_id, storage_->PutFileInfo(put_file_info));
  file_info = storage_->GetFileInfo(foo_url_id);
  EXPECT_TRUE(file_info.has_value());
  EXPECT_EQ(put_file_info.size, file_info.value().size);

  const std::string remote_id = "i-am-a-remote-id";
  put_file_info.remote_id = remote_id;
  EXPECT_EQ(foo_url_id, storage_->PutFileInfo(put_file_info));

  file_info = storage_->GetFileInfo(foo_url_id);
  EXPECT_TRUE(file_info.has_value());
  EXPECT_EQ(put_file_info.remote_id, file_info.value().remote_id);
  EXPECT_EQ(remote_id, file_info.value().remote_id);
}

TEST_P(IndexStorageTest, PutFileInfo) {
  // Must initialize before use.
  ASSERT_TRUE(storage_->Init());

  FileInfo file_info(foo_url_, 100, base::Time());
  // Inserting file is always successful and the returned ID is equal to that
  // of the ID generated from file_info.file_url.
  int64_t gotten_url_id = storage_->PutFileInfo(file_info);
  int64_t foo_url_id = storage_->GetUrlId(foo_url_);
  EXPECT_EQ(foo_url_id, gotten_url_id);
}

TEST_P(IndexStorageTest, DeleteFileInfo) {
  // Must initialize before use.
  ASSERT_TRUE(storage_->Init());

  // You cannot delete file info by the invalid URL ID.
  EXPECT_EQ(-1, storage_->DeleteFileInfo(-1));

  int64_t foo_url_id = storage_->GetOrCreateUrlId(foo_url_);
  EXPECT_EQ(foo_url_id, 1);
  // Not deletion needed, but still signals that the file was "deleted"
  // successfully, as it is no longer in the index.
  EXPECT_EQ(foo_url_id, storage_->DeleteFileInfo(foo_url_id));

  FileInfo put_file_info(foo_url_, 100, base::Time());
  EXPECT_EQ(foo_url_id, storage_->PutFileInfo(put_file_info));
  EXPECT_EQ(foo_url_id, storage_->DeleteFileInfo(foo_url_id));
  EXPECT_EQ(foo_url_id, storage_->DeleteFileInfo(foo_url_id));
}

TEST_P(IndexStorageTest, AddToPostingList) {
  // Must initialize before use.
  ASSERT_TRUE(storage_->Init());

  int64_t pinned_id = storage_->GetOrCreateTermId(pinned_);
  int64_t foo_url_id = storage_->GetOrCreateUrlId(foo_url_);

  EXPECT_EQ(1u, storage_->AddToPostingList(pinned_id, foo_url_id));
  // Second time adding the term does not change the database.
  EXPECT_EQ(0u, storage_->AddToPostingList(pinned_id, foo_url_id));
}

TEST_P(IndexStorageTest, DeleteFromPostingList) {
  // Must initialize before use.
  ASSERT_TRUE(storage_->Init());

  int64_t pinned_id = storage_->GetOrCreateTermId(pinned_);
  int64_t foo_url_id = storage_->GetOrCreateUrlId(foo_url_);

  // Can delete something that was not added. Results in 0 changes.
  EXPECT_EQ(0u, storage_->DeleteFromPostingList(pinned_id, foo_url_id));

  // Add and delete, expect it to succeed.
  EXPECT_EQ(1u, storage_->AddToPostingList(pinned_id, foo_url_id));
  EXPECT_EQ(1u, storage_->DeleteFromPostingList(pinned_id, foo_url_id));
  // No more deletion after the first one.
  EXPECT_EQ(0u, storage_->DeleteFromPostingList(pinned_id, foo_url_id));
}

TEST_P(IndexStorageTest, GetUrlIdsForTerm) {
  // Must initialize before use.
  ASSERT_TRUE(storage_->Init());

  // Setup: prefetch URL IDs.
  int64_t foo_url_id = storage_->GetOrCreateUrlId(foo_url_);
  int64_t bar_url_id = storage_->GetOrCreateUrlId(bar_url_);
  int64_t pinned_id = storage_->GetOrCreateTermId(pinned_);

  // No terms were associated with any files, so the results must be empty.
  EXPECT_TRUE(storage_->GetUrlIdsForTermId(pinned_id).empty());

  // Associate pinned with foo.
  EXPECT_EQ(1u, storage_->AddToPostingList(pinned_id, foo_url_id));
  EXPECT_THAT(storage_->GetUrlIdsForTermId(pinned_id),
              testing::UnorderedElementsAre(foo_url_id));

  // Associate downloaded_ with foo.
  int64_t downloaded_term_id = storage_->GetOrCreateTermId(downloaded_);
  EXPECT_EQ(1u, storage_->AddToPostingList(downloaded_term_id, foo_url_id));
  EXPECT_THAT(storage_->GetUrlIdsForTermId(pinned_id),
              testing::UnorderedElementsAre(foo_url_id));
  EXPECT_THAT(storage_->GetUrlIdsForTermId(downloaded_term_id),
              testing::UnorderedElementsAre(foo_url_id));

  // Associate downloaded with bar.
  EXPECT_EQ(1u, storage_->AddToPostingList(downloaded_term_id, bar_url_id));
  EXPECT_THAT(storage_->GetUrlIdsForTermId(pinned_id),
              testing::UnorderedElementsAre(foo_url_id));
  EXPECT_THAT(storage_->GetUrlIdsForTermId(downloaded_term_id),
              testing::UnorderedElementsAre(foo_url_id, bar_url_id));
}

TEST_P(IndexStorageTest, GetTermIdsForUrl) {
  // Must initialize before use.
  ASSERT_TRUE(storage_->Init());

  // Setup: prefetch URL IDs.
  int64_t foo_url_id = storage_->GetOrCreateUrlId(foo_url_);
  int64_t pinned_id = storage_->GetOrCreateTermId(pinned_);
  int64_t downloaded_id = storage_->GetOrCreateTermId(downloaded_);

  // Before anything is associated with a given URL expect empty set.
  EXPECT_TRUE(storage_->GetTermIdsForUrl(foo_url_id).empty());

  EXPECT_EQ(1u, storage_->AddToPostingList(pinned_id, foo_url_id));
  EXPECT_THAT(storage_->GetTermIdsForUrl(foo_url_id),
              testing::UnorderedElementsAre(pinned_id));

  EXPECT_EQ(1u, storage_->AddToPostingList(downloaded_id, foo_url_id));
  std::set<int64_t> ids_of_foo = storage_->GetTermIdsForUrl(foo_url_id);
  EXPECT_THAT(ids_of_foo,
              testing::UnorderedElementsAre(pinned_id, downloaded_id));

  // Expect that no terms are left once we delete them for the given URL ID.
  storage_->DeleteTermIdsForUrl(ids_of_foo, foo_url_id);
  EXPECT_TRUE(storage_->GetTermIdsForUrl(foo_url_id).empty());
}

TEST_P(IndexStorageTest, CatastrophicError) {
  if (GetParam() == StorageType::RAM) {
    return;
  }
  base::FilePath db_path = temp_dir_.GetPath().Append("CatastrophicError.db");
  auto db_under_test = std::make_unique<SqlStorage>(db_path, "test_uma_tag");

  // Initialize the database and store token "foo" in it. Check that we can
  // retrieve.
  ASSERT_TRUE(db_under_test->Init());
  EXPECT_EQ(db_under_test->GetOrCreateTokenId("foo"), 1);
  EXPECT_EQ(db_under_test->GetTokenId("foo"), 1);

  // Drop the token table to cause a catastrophic error.
  ASSERT_TRUE(
      db_under_test->GetDbForTests()->Execute("DROP TABLE token_table"));

  // Expect the db to recover but not to have any data.
  EXPECT_EQ(db_under_test->GetTokenId("foo"), -1);
  // Test that the sql storage works after a recovery.
  EXPECT_EQ(db_under_test->GetOrCreateTokenId("foo"), 1);
}

INSTANTIATE_TEST_SUITE_P(Sql,
                         IndexStorageTest,
                         testing::ValuesIn<StorageType>({StorageType::SQL}));
INSTANTIATE_TEST_SUITE_P(Ram,
                         IndexStorageTest,
                         testing::ValuesIn<StorageType>({StorageType::RAM}));

}  // namespace
}  // namespace ash::file_manager