| // 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 |