blob: 054f9f869dc712ea750242f55e74f69e4fc176ba [file] [log] [blame]
// Copyright 2025 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/extensions/initial_external_extension_loader.h"
#include <string>
#include "base/containers/contains.h"
#include "base/files/file_util.h"
#include "base/functional/callback.h"
#include "base/run_loop.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "chrome/browser/browser_features.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/external_provider_impl.h"
#include "chrome/browser/extensions/external_provider_manager.h"
#include "chrome/browser/extensions/external_testing_loader.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/chrome_switches.h"
#include "components/crx_file/id_util.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/pref_names.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/browser/updater/extension_downloader_test_helper.h"
#include "extensions/common/extension_urls.h"
#include "extensions/common/extensions_client.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
namespace extensions {
namespace {
constexpr char kTestExtensionId[] = "ogjcoiohnmldgjemafoockdghcjciccf";
// Paths under the temporary directory that the EmbeddedTestServer will serve.
constexpr char kAutoupdateDir[] = "autoupdate";
constexpr char kUnpackedDir[] = "unpacked";
constexpr char kManifestRelpath[] = "manifest";
constexpr char kManifestJson[] = "manifest.json";
// This test verifies the initial external extension flow across browser
// restarts. The scenario:
// 1) PRE_PRE_: Simulate first run by seeding `kInitialInstallList` with a
// known extension ID and verifying installation via the webstore update
// URL.
// 2) PRE_: Verify the extension is present, then uninstall it and record
// that it is an *external* uninstallation (so the loader should not
// reinstall it on next restart).
// 3) Main: Ensure the extension remains uninstalled after a further
// restart even if the updater runs again.
class InitialExternalExtensionLoaderRestartBrowserTest
: public ExtensionBrowserTest {
protected:
InitialExternalExtensionLoaderRestartBrowserTest() = default;
~InitialExternalExtensionLoaderRestartBrowserTest() override = default;
void SetUpDefaultCommandLine(base::CommandLine* command_line) override {
// Browser tests disable default apps; re-enable as this test relies on the
// initial external extension installation path.
ExtensionBrowserTest::SetUpDefaultCommandLine(command_line);
command_line->RemoveSwitch(switches::kDisableDefaultApps);
}
std::string GetExtensionUpdateURLSpec() {
return embedded_test_server()
->GetURL(base::StrCat({"/", kAutoupdateDir, "/", kManifestRelpath}))
.spec();
}
// Returns the installed extension under test if present, otherwise nullptr.
Extension* GetInstalledExtension() {
return const_cast<Extension*>(
extension_registry()->GetInstalledExtension(kTestExtensionId));
}
// Blocks until `extension_id` is reported as installed. The installation
// trigger is executed only after the observer is attached to avoid races.
void WaitForInstallOf(const std::string& extension_id) {
extensions::TestExtensionRegistryObserver observer(extension_registry(),
extension_id);
// Trigger an updater cycle and wait for the specific extension ID.
ExtensionUpdater::Get(profile())->CheckNow(ExtensionUpdater::CheckParams());
// If the action completed synchronously and the extension is already there,
// this returns immediately; otherwise it waits for the install event.
if (!extension_registry()->GetInstalledExtension(extension_id)) {
observer.WaitForExtensionInstalled();
}
}
// Creates a CRX from test data (under test_data_dir_/autoupdate/<source>)
// while rewriting its manifest to point to our EmbeddedTestServer update URL.
void SetUpExtensionUpdatePackage(const base::FilePath& temp_dir,
const std::string& source_dir_name,
const std::string& crx_name) {
ASSERT_TRUE(base::CreateDirectory(temp_dir.AppendASCII(kUnpackedDir)));
const base::FilePath basedir = test_data_dir_.AppendASCII("autoupdate");
ASSERT_TRUE(base::CopyDirectory(basedir.AppendASCII(source_dir_name),
temp_dir.AppendASCII(kUnpackedDir),
/*recursive=*/false));
// Render manifest.json.template -> manifest.json with the server's update
// URL.
std::string manifest_template;
ASSERT_TRUE(
base::ReadFileToString(basedir.AppendASCII(source_dir_name)
.AppendASCII("manifest.json.template"),
&manifest_template));
const std::string manifest = base::ReplaceStringPlaceholders(
manifest_template, {GetExtensionUpdateURLSpec()},
/*offsets=*/nullptr);
ASSERT_TRUE(base::WriteFile(
temp_dir.AppendASCII(kUnpackedDir).AppendASCII(kManifestJson),
manifest));
// Pack to CRX placed under /autoupdate so the server can serve it.
ASSERT_TRUE(base::CreateDirectory(temp_dir.AppendASCII(kAutoupdateDir)));
base::FilePath crx_path = PackExtensionWithOptions(
temp_dir.AppendASCII(kUnpackedDir),
temp_dir.AppendASCII(kAutoupdateDir).AppendASCII(crx_name),
basedir.AppendASCII("key.pem"), /*pem_out_path=*/base::FilePath());
ASSERT_FALSE(crx_path.empty());
}
// Emits the "update manifest" the updater fetches at /autoupdate/manifest,
// pointing to the CRX we created above.
void SetUpExtensionUpdateResponse(const base::FilePath& temp_dir,
const std::string& crx_name,
const std::string& manifest_template_name) {
std::string manifest_template;
ASSERT_TRUE(
base::ReadFileToString(test_data_dir_.AppendASCII(kAutoupdateDir)
.AppendASCII(manifest_template_name),
&manifest_template));
const GURL crx_url = embedded_test_server()->GetURL(
base::StrCat({"/", kAutoupdateDir, "/", crx_name}));
const std::string manifest = base::ReplaceStringPlaceholders(
manifest_template, {crx_url.spec()}, /*offsets=*/nullptr);
ASSERT_TRUE(base::CreateDirectory(temp_dir.AppendASCII(kAutoupdateDir)));
ASSERT_TRUE(base::WriteFile(
temp_dir.AppendASCII(kAutoupdateDir).AppendASCII(kManifestRelpath),
manifest));
}
// Returns current initial install list from the prefs.
const base::Value::List& InitialInstallList() {
return profile()->GetPrefs()->GetList(pref_names::kInitialInstallList);
}
// Adds `kTestExtensionId` to the initial install list (overwriting any
// previous list to ensure deterministic state).
void SeedInitialInstallListWithTestExtension() {
base::Value::List ids;
ids.Append(kTestExtensionId);
profile()->GetPrefs()->SetList(pref_names::kInitialInstallList,
std::move(ids));
profile()->GetPrefs()->CommitPendingWrite();
}
// Setups extension installation environment to that a request to install
// `kTestExtensionId` may succeed when it should.
void PrepareExtensionInstallation() {
base::ScopedAllowBlockingForTesting allow_blocking;
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
// Serve all files we place into `temp_dir_`, especially /autoupdate/*
// resources needed by the CRX update manifest.
embedded_test_server()->ServeFilesFromDirectory(temp_dir_.GetPath());
ASSERT_TRUE(embedded_test_server()->Start());
// Point the "webstore" update URL to the EmbeddedTestServer.
auto* command_line = base::CommandLine::ForCurrentProcess();
command_line->AppendSwitchASCII(switches::kAppsGalleryUpdateURL,
GetExtensionUpdateURLSpec());
ExtensionsClient::Get()->InitializeWebStoreUrls(command_line);
// Prepare a CRX package and the update response the updater will fetch.
ASSERT_NO_FATAL_FAILURE(SetUpExtensionUpdatePackage(
temp_dir_.GetPath(), /*source_dir_name=*/"v2", /*crx_name=*/"v2.crx"));
ASSERT_NO_FATAL_FAILURE(SetUpExtensionUpdateResponse(
temp_dir_.GetPath(), /*crx_name=*/"v2.crx",
/*manifest_template_name=*/"manifest_v2.xml.template"));
}
protected:
base::test::ScopedFeatureList feature_list_{
features::kInitialExternalExtensions};
FeatureSwitch::ScopedOverride feature_override{
FeatureSwitch::prompt_for_external_extensions(), true};
base::ScopedTempDir temp_dir_;
};
// Phase 1 (First run): seed initial preferences and verify installation occurs.
IN_PROC_BROWSER_TEST_F(InitialExternalExtensionLoaderRestartBrowserTest,
PRE_PRE_InitialExternalExtension) {
PrepareExtensionInstallation();
SeedInitialInstallListWithTestExtension();
const size_t enabled_before =
extension_registry()->enabled_extensions().size();
EXPECT_TRUE(extension_registry()->disabled_extensions().empty());
WaitForInstallOf(kTestExtensionId);
Extension* extension = GetInstalledExtension();
ASSERT_TRUE(extension);
EXPECT_EQ(extension->id(), kTestExtensionId);
// Verify the extension is in the expected state (disabled for being
// unacknowledged).
EXPECT_FALSE(
extension_registry()->enabled_extensions().Contains(kTestExtensionId));
EXPECT_EQ(enabled_before, extension_registry()->enabled_extensions().size());
ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
EXPECT_FALSE(prefs->IsExternalExtensionAcknowledged(kTestExtensionId));
EXPECT_THAT(prefs->GetDisableReasons(kTestExtensionId),
testing::UnorderedElementsAre(
disable_reason::DISABLE_EXTERNAL_EXTENSION));
}
// Phase 2 (Restart): confirm installation, then uninstall so a future restart
// must *not* reinstall it.
IN_PROC_BROWSER_TEST_F(InitialExternalExtensionLoaderRestartBrowserTest,
PRE_InitialExternalExtension) {
EXPECT_TRUE(
base::Contains(InitialInstallList(), base::Value(kTestExtensionId)));
Extension* extension = GetInstalledExtension();
ASSERT_TRUE(extension);
EXPECT_EQ(extension->id(), kTestExtensionId);
UninstallExtension(extension->id());
EXPECT_FALSE(GetInstalledExtension());
auto* extension_prefs = extensions::ExtensionPrefs::Get(profile());
EXPECT_TRUE(
extension_prefs->IsExternalExtensionUninstalled(kTestExtensionId));
}
// Phase 3 (Second restart): the loader/updater must respect the external
// uninstall (user preference) and not reinstall the extension.
IN_PROC_BROWSER_TEST_F(InitialExternalExtensionLoaderRestartBrowserTest,
InitialExternalExtension) {
// Ensures that extension installation environment is correctly set to verify
// that the user preference is respected.
PrepareExtensionInstallation();
EXPECT_TRUE(
base::Contains(InitialInstallList(), base::Value(kTestExtensionId)));
EXPECT_FALSE(GetInstalledExtension());
auto* extension_prefs = extensions::ExtensionPrefs::Get(profile());
EXPECT_TRUE(
extension_prefs->IsExternalExtensionUninstalled(kTestExtensionId));
// Even if the updater runs, the external-uninstalled marker should prevent
// reinstallation to respect user choice.
ExtensionUpdater::CheckParams params;
params.install_immediately = true;
ExtensionUpdater::Get(profile())->CheckNow(std::move(params));
EXPECT_FALSE(GetInstalledExtension());
EXPECT_TRUE(
extension_prefs->IsExternalExtensionUninstalled(kTestExtensionId));
}
} // namespace
} // namespace extensions