chromium/chrome/browser/ash/extensions/external_cache_impl_unittest.cc

// Copyright 2013 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/extensions/external_cache_impl.h"

#include <map>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <utility>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/run_loop.h"
#include "base/task/thread_pool.h"
#include "base/values.h"
#include "chrome/browser/ash/extensions/external_cache_delegate.h"
#include "chrome/browser/ash/settings/scoped_cros_settings_test_helper.h"
#include "chrome/browser/extensions/external_provider_impl.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_utils.h"
#include "extensions/common/extension_urls.h"
#include "extensions/common/verifier_formats.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace chromeos {

namespace {

using ::testing::Optional;

const char kTestExtensionId1[] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const char kTestExtensionId2[] = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
const char kTestExtensionId3[] = "cccccccccccccccccccccccccccccccc";
const char kTestExtensionId4[] = "dddddddddddddddddddddddddddddddd";
const char kNonWebstoreUpdateUrl[] = "https://localhost/service/update2/crx";
const char kExternalCrxPath[] = "/local/path/to/extension.crx";
const char kExternalCrxVersion[] = "1.2.3.4";

}  // namespace

class ExternalCacheImplTest : public testing::Test,
                              public ExternalCacheDelegate {
 public:
  ExternalCacheImplTest()
      : task_environment_(content::BrowserTaskEnvironment::REAL_IO_THREAD),
        test_shared_loader_factory_(
            base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
                &test_url_loader_factory_)) {}

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

  ~ExternalCacheImplTest() override = default;

  scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory() {
    return test_shared_loader_factory_;
  }

  const std::optional<base::Value::Dict>& provided_prefs() { return prefs_; }
  const std::set<extensions::ExtensionId>& deleted_extension_files() const {
    return deleted_extension_files_;
  }

  // ExternalCacheDelegate:
  void OnExtensionListsUpdated(const base::Value::Dict& prefs) override {
    prefs_ = prefs.Clone();
  }

  bool IsRollbackAllowed() const override { return is_rollback_allowed_; }

  bool CanRollbackNow() const override { return can_rollback_now_; }

  void OnCachedExtensionFileDeleted(
      const extensions::ExtensionId& id) override {
    deleted_extension_files_.insert(id);
  }

  base::FilePath CreateCacheDir(bool initialized) {
    EXPECT_TRUE(cache_dir_.CreateUniqueTempDir());
    if (initialized)
      CreateFlagFile(cache_dir_.GetPath());
    return cache_dir_.GetPath();
  }

  base::FilePath CreateTempDir() {
    EXPECT_TRUE(temp_dir_.CreateUniqueTempDir());
    return temp_dir_.GetPath();
  }

  void CreateFlagFile(const base::FilePath& dir) {
    CreateFile(
        dir.Append(extensions::LocalExtensionCache::kCacheReadyFlagFileName));
  }

  void CreateExtensionFile(const base::FilePath& dir,
                           const std::string& id,
                           const std::string& version) {
    CreateFile(GetExtensionFile(dir, id, version));
  }

  void CreateFile(const base::FilePath& file) {
    EXPECT_TRUE(base::WriteFile(file, std::string_view()));
  }

  base::FilePath GetExtensionFile(const base::FilePath& dir,
                                  const std::string& id,
                                  const std::string& version) {
    return dir.Append(id + "-" + version + ".crx");
  }

  base::Value CreateEntryWithUpdateUrl(bool from_webstore) {
    base::Value::Dict entry;
    entry.Set(extensions::ExternalProviderImpl::kExternalUpdateUrl,
              from_webstore ? extension_urls::GetWebstoreUpdateUrl().spec()
                            : kNonWebstoreUpdateUrl);
    return base::Value(std::move(entry));
  }

  base::Value CreateEntryWithExternalCrx() {
    base::Value::Dict entry;
    entry.Set(extensions::ExternalProviderImpl::kExternalCrx, kExternalCrxPath);
    entry.Set(extensions::ExternalProviderImpl::kExternalVersion,
              kExternalCrxVersion);
    return base::Value(std::move(entry));
  }

  void AllowImmediateRollback() {
    is_rollback_allowed_ = true;
    can_rollback_now_ = true;
  }

  void AllowRollbackOnNextInit() {
    is_rollback_allowed_ = true;
    can_rollback_now_ = false;
  }

 private:
  content::BrowserTaskEnvironment task_environment_;

  network::TestURLLoaderFactory test_url_loader_factory_;
  scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;

  bool is_rollback_allowed_ = false;
  bool can_rollback_now_ = false;
  base::ScopedTempDir cache_dir_;
  base::ScopedTempDir temp_dir_;
  std::optional<base::Value::Dict> prefs_;
  std::set<extensions::ExtensionId> deleted_extension_files_;

  ash::ScopedCrosSettingsTestHelper cros_settings_test_helper_;
};

TEST_F(ExternalCacheImplTest, Basic) {
  base::FilePath cache_dir(CreateCacheDir(false));
  ExternalCacheImpl external_cache(
      cache_dir, url_loader_factory(),
      base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}), this,
      true, false, false);

  base::Value::Dict prefs;
  prefs.Set(kTestExtensionId1, CreateEntryWithUpdateUrl(true));
  CreateExtensionFile(cache_dir, kTestExtensionId1, "1");
  prefs.Set(kTestExtensionId2, CreateEntryWithUpdateUrl(true));
  prefs.Set(kTestExtensionId3, CreateEntryWithUpdateUrl(false));
  CreateExtensionFile(cache_dir, kTestExtensionId3, "3");
  prefs.Set(kTestExtensionId4, CreateEntryWithUpdateUrl(false));

  external_cache.UpdateExtensionsList(std::move(prefs));
  content::RunAllTasksUntilIdle();

  ASSERT_TRUE(provided_prefs());
  EXPECT_EQ(provided_prefs()->size(), 2ul);

  // File in cache from Webstore.
  const base::Value::Dict* entry1 =
      provided_prefs()->FindDictByDottedPath(kTestExtensionId1);
  ASSERT_TRUE(entry1);
  EXPECT_EQ(entry1->Find(extensions::ExternalProviderImpl::kExternalUpdateUrl),
            nullptr);
  EXPECT_NE(entry1->Find(extensions::ExternalProviderImpl::kExternalCrx),
            nullptr);
  EXPECT_NE(entry1->Find(extensions::ExternalProviderImpl::kExternalVersion),
            nullptr);
  EXPECT_THAT(
      entry1->FindBool(extensions::ExternalProviderImpl::kIsFromWebstore),
      Optional(true));

  // File in cache not from Webstore.
  const base::Value::Dict* entry3 =
      provided_prefs()->FindDictByDottedPath(kTestExtensionId3);
  ASSERT_TRUE(entry3);
  EXPECT_EQ(entry3->Find(extensions::ExternalProviderImpl::kExternalUpdateUrl),
            nullptr);
  EXPECT_NE(entry3->Find(extensions::ExternalProviderImpl::kExternalCrx),
            nullptr);
  EXPECT_NE(entry3->Find(extensions::ExternalProviderImpl::kExternalVersion),
            nullptr);
  EXPECT_EQ(entry3->Find(extensions::ExternalProviderImpl::kIsFromWebstore),
            nullptr);

  // Update from Webstore.
  base::FilePath temp_dir(CreateTempDir());
  base::FilePath temp_file2 = temp_dir.Append("b.crx");
  CreateFile(temp_file2);
  extensions::CRXFileInfo crx_info_v2(temp_file2,
                                      extensions::GetTestVerifierFormat());
  crx_info_v2.extension_id = kTestExtensionId2;
  crx_info_v2.expected_version = base::Version("2");
  external_cache.OnExtensionDownloadFinished(
      crx_info_v2, true, GURL(),
      extensions::ExtensionDownloaderDelegate::PingResult(), std::set<int>(),
      extensions::ExtensionDownloaderDelegate::InstallCallback());

  content::RunAllTasksUntilIdle();
  EXPECT_EQ(provided_prefs()->size(), 3ul);

  const base::Value::Dict* entry2 =
      provided_prefs()->FindDictByDottedPath(kTestExtensionId2);
  ASSERT_TRUE(entry2);
  EXPECT_EQ(entry2->Find(extensions::ExternalProviderImpl::kExternalUpdateUrl),
            nullptr);
  EXPECT_NE(entry2->Find(extensions::ExternalProviderImpl::kExternalCrx),
            nullptr);
  EXPECT_NE(entry2->Find(extensions::ExternalProviderImpl::kExternalVersion),
            nullptr);
  EXPECT_THAT(
      entry2->FindBool(extensions::ExternalProviderImpl::kIsFromWebstore),
      Optional(true));
  EXPECT_TRUE(
      base::PathExists(GetExtensionFile(cache_dir, kTestExtensionId2, "2")));

  // Update not from Webstore.
  base::FilePath temp_file4 = temp_dir.Append("d.crx");
  CreateFile(temp_file4);
  {
    extensions::CRXFileInfo crx_info_v4(temp_file4,
                                        extensions::GetTestVerifierFormat());
    crx_info_v4.extension_id = kTestExtensionId4;
    crx_info_v4.expected_version = base::Version("4");
    external_cache.OnExtensionDownloadFinished(
        crx_info_v4, true, GURL(),
        extensions::ExtensionDownloaderDelegate::PingResult(), std::set<int>(),
        extensions::ExtensionDownloaderDelegate::InstallCallback());
  }

  content::RunAllTasksUntilIdle();
  EXPECT_EQ(provided_prefs()->size(), 4ul);

  const base::Value::Dict* entry4 =
      provided_prefs()->FindDictByDottedPath(kTestExtensionId4);
  ASSERT_TRUE(entry4);
  EXPECT_EQ(entry4->Find(extensions::ExternalProviderImpl::kExternalUpdateUrl),
            nullptr);
  EXPECT_NE(entry4->Find(extensions::ExternalProviderImpl::kExternalCrx),
            nullptr);
  EXPECT_NE(entry4->Find(extensions::ExternalProviderImpl::kExternalVersion),
            nullptr);
  EXPECT_EQ(entry4->Find(extensions::ExternalProviderImpl::kIsFromWebstore),
            nullptr);
  EXPECT_TRUE(
      base::PathExists(GetExtensionFile(cache_dir, kTestExtensionId4, "4")));

  // Damaged file should be removed from disk.
  EXPECT_TRUE(deleted_extension_files().empty());
  external_cache.OnDamagedFileDetected(
      GetExtensionFile(cache_dir, kTestExtensionId2, "2"));
  content::RunAllTasksUntilIdle();
  EXPECT_EQ(3ul, provided_prefs()->size());
  EXPECT_FALSE(
      base::PathExists(GetExtensionFile(cache_dir, kTestExtensionId2, "2")));
  EXPECT_THAT(deleted_extension_files(),
              testing::ElementsAre(kTestExtensionId2));

  // Shutdown with callback OnExtensionListsUpdated that clears prefs.
  external_cache.Shutdown(
      base::BindOnce(&ExternalCacheImplTest::OnExtensionListsUpdated,
                     base::Unretained(this), base::Value::Dict()));
  content::RunAllTasksUntilIdle();
  EXPECT_TRUE(provided_prefs()->empty());

  // After Shutdown directory shouldn't be touched.
  external_cache.OnDamagedFileDetected(
      GetExtensionFile(cache_dir, kTestExtensionId4, "4"));
  content::RunAllTasksUntilIdle();
  EXPECT_TRUE(
      base::PathExists(GetExtensionFile(cache_dir, kTestExtensionId4, "4")));
}

TEST_F(ExternalCacheImplTest, PreserveExternalCrx) {
  base::FilePath cache_dir(CreateCacheDir(false));
  ExternalCacheImpl external_cache(
      cache_dir, url_loader_factory(),
      base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}), this,
      true, false, false);

  base::Value::Dict prefs;
  prefs.Set(kTestExtensionId1, CreateEntryWithExternalCrx());
  prefs.Set(kTestExtensionId2, CreateEntryWithUpdateUrl(true));

  external_cache.UpdateExtensionsList(std::move(prefs));
  content::RunAllTasksUntilIdle();

  ASSERT_TRUE(provided_prefs());
  EXPECT_EQ(provided_prefs()->size(), 1ul);

  // Extensions downloaded from update url will only be visible in the provided
  // prefs once the download of the .crx has finished. Extensions that are
  // provided as external crx path directly should also be visible in the
  // provided prefs directly.
  const base::Value::Dict* entry1 =
      provided_prefs()->FindDictByDottedPath(kTestExtensionId1);
  ASSERT_TRUE(entry1);
  EXPECT_EQ(entry1->Find(extensions::ExternalProviderImpl::kExternalUpdateUrl),
            nullptr);
  EXPECT_NE(entry1->Find(extensions::ExternalProviderImpl::kExternalCrx),
            nullptr);
  EXPECT_NE(entry1->Find(extensions::ExternalProviderImpl::kExternalVersion),
            nullptr);
}

// Checks that if immediate rollback is allowed, extension cache is removed
// immediately and a lower version is allowed to be installed.
TEST_F(ExternalCacheImplTest, ImmediateRollback) {
  base::FilePath cache_dir(CreateCacheDir(false));
  ExternalCacheImpl external_cache(
      cache_dir, url_loader_factory(),
      base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}), this,
      true, false, false);

  CreateExtensionFile(cache_dir, kTestExtensionId1, "2");
  external_cache.UpdateExtensionsList(base::Value::Dict().Set(
      kTestExtensionId1, CreateEntryWithUpdateUrl(false)));
  content::RunAllTasksUntilIdle();

  ASSERT_TRUE(provided_prefs());
  EXPECT_EQ(provided_prefs()->size(), 1ul);

  // Allow rollback by ExternalCacheDelegate and check that rollback request
  // succeeds.
  AllowImmediateRollback();
  EXPECT_EQ(ExternalCacheImpl::RequestRollbackResult::kAllowed,
            external_cache.RequestRollback(kTestExtensionId1));

  // Check that kTestExtensionId1's entry in the ExtensionCache will be deleted.
  content::RunAllTasksUntilIdle();
  EXPECT_THAT(deleted_extension_files(),
              testing::ElementsAre(kTestExtensionId1));
  EXPECT_TRUE(provided_prefs()->empty());
  EXPECT_FALSE(
      base::PathExists(GetExtensionFile(cache_dir, kTestExtensionId1, "2")));

  // Check that lower version installs correctly.
  base::FilePath temp_dir(CreateTempDir());
  base::FilePath temp_file = temp_dir.Append("a.crx");
  CreateFile(temp_file);
  {
    extensions::CRXFileInfo crx_info_v1(temp_file,
                                        extensions::GetTestVerifierFormat());
    crx_info_v1.extension_id = kTestExtensionId1;
    crx_info_v1.expected_version = base::Version("1");
    external_cache.OnExtensionDownloadFinished(
        crx_info_v1, true, GURL(),
        extensions::ExtensionDownloaderDelegate::PingResult(), std::set<int>(),
        extensions::ExtensionDownloaderDelegate::InstallCallback());
  }

  content::RunAllTasksUntilIdle();
  EXPECT_EQ(1ul, provided_prefs()->size());
  EXPECT_TRUE(
      base::PathExists(GetExtensionFile(cache_dir, kTestExtensionId1, "1")));
}

// Checks that if rollback is generally allowed by delegate but cannot be
// performed immediately, cache invalidation is scheduled for the next run.
// Checks that cache is deleted on the next run.
TEST_F(ExternalCacheImplTest, RollbackOnNextInit) {
  base::FilePath cache_dir(CreateCacheDir(false));
  ExternalCacheImpl external_cache(
      cache_dir, url_loader_factory(),
      base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}), this,
      true, false, false);

  CreateExtensionFile(cache_dir, kTestExtensionId1, "2");
  base::Value::Dict prefs;
  prefs.Set(kTestExtensionId1, CreateEntryWithUpdateUrl(false));
  external_cache.UpdateExtensionsList(prefs.Clone());
  content::RunAllTasksUntilIdle();

  ASSERT_TRUE(provided_prefs());
  EXPECT_EQ(provided_prefs()->size(), 1ul);

  // Allow rollback on the next run by ExternalCacheDelegate and check that
  // rollback request returns SCHEDULED_FOR_NEXT_RUN value.
  AllowRollbackOnNextInit();
  EXPECT_EQ(ExternalCacheImpl::RequestRollbackResult::kScheduledForNextRun,
            external_cache.RequestRollback(kTestExtensionId1));

  // Check that extension cache is still there.
  content::RunAllTasksUntilIdle();
  EXPECT_TRUE(deleted_extension_files().empty());
  ASSERT_TRUE(provided_prefs());
  EXPECT_EQ(provided_prefs()->size(), 1ul);
  EXPECT_TRUE(
      base::PathExists(GetExtensionFile(cache_dir, kTestExtensionId1, "2")));

  // Shutdown and initialize new cache.
  base::RunLoop run_loop;
  external_cache.Shutdown(run_loop.QuitClosure());
  run_loop.Run();
  ExternalCacheImpl new_cache(
      cache_dir, url_loader_factory(),
      base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}), this,
      true, false, false);

  new_cache.UpdateExtensionsList(prefs.Clone());
  content::RunAllTasksUntilIdle();

  // Check that kTestExtensionId1's entry in the ExtensionCache was deleted
  // after initialization.
  EXPECT_TRUE(provided_prefs()->empty());
  EXPECT_FALSE(
      base::PathExists(GetExtensionFile(cache_dir, kTestExtensionId1, "2")));

  // Check that lower version installs correctly.
  base::FilePath temp_dir(CreateTempDir());
  base::FilePath temp_file = temp_dir.Append("a.crx");
  CreateFile(temp_file);
  {
    extensions::CRXFileInfo crx_info_v1(temp_file,
                                        extensions::GetTestVerifierFormat());
    crx_info_v1.extension_id = kTestExtensionId1;
    crx_info_v1.expected_version = base::Version("1");
    new_cache.OnExtensionDownloadFinished(
        crx_info_v1, true, GURL(),
        extensions::ExtensionDownloaderDelegate::PingResult(), std::set<int>(),
        extensions::ExtensionDownloaderDelegate::InstallCallback());
  }

  content::RunAllTasksUntilIdle();
  EXPECT_EQ(1ul, provided_prefs()->size());
  EXPECT_TRUE(
      base::PathExists(GetExtensionFile(cache_dir, kTestExtensionId1, "1")));
}

// Checks that if rollback is disallowed, cache is not invalidated.
TEST_F(ExternalCacheImplTest, RollbackDisallowed) {
  base::FilePath cache_dir(CreateCacheDir(false));
  ExternalCacheImpl external_cache(
      cache_dir, url_loader_factory(),
      base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}), this,
      true, false, false);

  CreateExtensionFile(cache_dir, kTestExtensionId1, "2");
  base::Value::Dict prefs;
  prefs.Set(kTestExtensionId1, CreateEntryWithUpdateUrl(false));
  external_cache.UpdateExtensionsList(prefs.Clone());
  content::RunAllTasksUntilIdle();

  ASSERT_TRUE(provided_prefs());
  EXPECT_EQ(provided_prefs()->size(), 1ul);

  // Check that rollback is disallowed.
  EXPECT_EQ(ExternalCacheImpl::RequestRollbackResult::kDisallowed,
            external_cache.RequestRollback(kTestExtensionId1));

  // Check that extension cache is still there.
  content::RunAllTasksUntilIdle();
  EXPECT_TRUE(deleted_extension_files().empty());
  ASSERT_TRUE(provided_prefs());
  EXPECT_EQ(provided_prefs()->size(), 1ul);
  EXPECT_TRUE(
      base::PathExists(GetExtensionFile(cache_dir, kTestExtensionId1, "2")));

  // Shutdown and initialize new cache.
  base::RunLoop run_loop;
  external_cache.Shutdown(run_loop.QuitClosure());
  run_loop.Run();
  ExternalCacheImpl new_cache(
      cache_dir, url_loader_factory(),
      base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}), this,
      true, false, false);

  new_cache.UpdateExtensionsList(prefs.Clone());
  content::RunAllTasksUntilIdle();

  // Check that extension cache was not deleted on initialization.
  EXPECT_EQ(provided_prefs()->size(), 1ul);
  EXPECT_TRUE(
      base::PathExists(GetExtensionFile(cache_dir, kTestExtensionId1, "2")));
}

}  // namespace chromeos