blob: a30f54c0c41acef556000411ed24d1bc3a62f840 [file] [log] [blame]
// 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 "chrome/browser/extensions/manifest_v2_experiment_manager.h"
#include "base/one_shot_event.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_management_internal.h"
#include "chrome/browser/extensions/external_provider_manager.h"
#include "chrome/browser/extensions/mv2_experiment_stage.h"
#include "chrome/browser/extensions/unpacked_installer.h"
#include "chrome/browser/profiles/profile.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/browser/mock_external_provider.h"
#include "extensions/browser/pref_names.h"
#include "extensions/browser/test_extension_registry_observer.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/feature_switch.h"
#include "extensions/common/mojom/manifest.mojom.h"
#include "extensions/test/test_extension_dir.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
namespace extensions {
namespace {
const Extension* GetExtensionByName(std::string_view name,
const ExtensionSet& extensions) {
const Extension* extension = nullptr;
for (const auto& e : extensions) {
if (e->name() == name) {
extension = e.get();
break;
}
}
return extension;
}
// Each test may have a different desired stage. Store them here so the test
// harness properly instantiates them.
MV2ExperimentStage GetExperimentStageForTest(std::string_view test_name) {
struct {
const char* test_name;
MV2ExperimentStage stage;
} test_stages[] = {
{"PRE_PRE_ExtensionsAreDisabledOnStartup", MV2ExperimentStage::kWarning},
{"PRE_ExtensionsAreDisabledOnStartup", MV2ExperimentStage::kWarning},
{"ExtensionsAreDisabledOnStartup",
MV2ExperimentStage::kDisableWithReEnable},
{"PRE_PRE_ExtensionsCanBeReEnabledByUsers", MV2ExperimentStage::kWarning},
{"PRE_ExtensionsCanBeReEnabledByUsers",
MV2ExperimentStage::kDisableWithReEnable},
{"ExtensionsCanBeReEnabledByUsers",
MV2ExperimentStage::kDisableWithReEnable},
{"ExtensionsAreReEnabledWhenUpdatedToMV3",
MV2ExperimentStage::kDisableWithReEnable},
{"PRE_MarkingNoticeAsAcknowledged", MV2ExperimentStage::kWarning},
{"MarkingNoticeAsAcknowledged", MV2ExperimentStage::kDisableWithReEnable},
{"PRE_MarkingGlobalNoticeAsAcknowledged", MV2ExperimentStage::kWarning},
{"MarkingGlobalNoticeAsAcknowledged",
MV2ExperimentStage::kDisableWithReEnable},
{"PRE_PRE_ExtensionsAreReEnabledIfExperimentDisabled",
MV2ExperimentStage::kWarning},
{"PRE_ExtensionsAreReEnabledIfExperimentDisabled",
MV2ExperimentStage::kDisableWithReEnable},
{"ExtensionsAreReEnabledIfExperimentDisabled",
MV2ExperimentStage::kWarning},
{"ExternalExtensionsCanBeInstalledButAreAlsoDisabled",
MV2ExperimentStage::kDisableWithReEnable},
{"UkmIsEmittedForExtensionWhenUninstalled",
MV2ExperimentStage::kDisableWithReEnable},
{"UkmIsNotEmittedForOtherUninstallations",
MV2ExperimentStage::kDisableWithReEnable},
{"PRE_MV2ExtensionsAreNotDisabledIfLegacyExtensionSwitchIsApplied",
MV2ExperimentStage::kWarning},
{"MV2ExtensionsAreNotDisabledIfLegacyExtensionSwitchIsApplied",
MV2ExperimentStage::kUnsupported},
{"PRE_PRE_FlowFromWarningToUnsupported", MV2ExperimentStage::kWarning},
{"PRE_FlowFromWarningToUnsupported",
MV2ExperimentStage::kDisableWithReEnable},
{"FlowFromWarningToUnsupported", MV2ExperimentStage::kUnsupported},
{"UnpackedExtensionsCanBeInstalledInDisabledPhase",
MV2ExperimentStage::kDisableWithReEnable},
{"UnpackedExtensionsCannotBeInstalledInUnsupportedPhase",
MV2ExperimentStage::kUnsupported},
};
for (const auto& test_stage : test_stages) {
if (test_stage.test_name == test_name) {
return test_stage.stage;
}
}
NOTREACHED()
<< "Unknown test name '" << test_name << "'. "
<< "You need to add a new test stage entry into this collection.";
}
} // namespace
class ManifestV2ExperimentManagerBrowserTest : public ExtensionBrowserTest {
public:
ManifestV2ExperimentManagerBrowserTest() = default;
~ManifestV2ExperimentManagerBrowserTest() override = default;
void SetUp() override {
std::vector<base::test::FeatureRef> enabled_features;
std::vector<base::test::FeatureRef> disabled_features;
// Each test may need a different value for the experiment stages, since
// many need some kind of pre-experiment set up, then test the behavior on
// subsequent startups. Initialize each test according to its preferred
// stage.
MV2ExperimentStage experiment_stage = GetExperimentStageForTest(
testing::UnitTest::GetInstance()->current_test_info()->name());
switch (experiment_stage) {
case MV2ExperimentStage::kWarning:
disabled_features.push_back(
extensions_features::kExtensionManifestV2Disabled);
disabled_features.push_back(
extensions_features::kExtensionManifestV2Unsupported);
break;
case MV2ExperimentStage::kDisableWithReEnable:
enabled_features.push_back(
extensions_features::kExtensionManifestV2Disabled);
disabled_features.push_back(
extensions_features::kExtensionManifestV2Unsupported);
break;
case MV2ExperimentStage::kUnsupported:
enabled_features.push_back(
extensions_features::kExtensionManifestV2Unsupported);
disabled_features.push_back(
extensions_features::kExtensionManifestV2Disabled);
break;
}
PopulateAdditionalFeatures(enabled_features, disabled_features);
feature_list_.InitWithFeatures(enabled_features, disabled_features);
ExtensionBrowserTest::SetUp();
}
void TearDown() override {
ExtensionBrowserTest::TearDown();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
ExtensionBrowserTest::SetUpCommandLine(command_line);
}
void SetUpOnMainThread() override {
ExtensionBrowserTest::SetUpOnMainThread();
ukm_recorder_.emplace();
// UKM only emits for webstore extensions. Pretend any extension is a store
// extension for this test.
ukm_recorder_->SetIsWebstoreExtensionCallback(
base::BindRepeating([](std::string_view) { return true; }));
}
// Since this is testing the MV2 deprecation experiments, we don't want to
// bypass their disabling for testing.
bool ShouldAllowMV2Extensions() override { return false; }
void WaitForExtensionSystemReady() {
base::RunLoop run_loop;
ExtensionSystem::Get(profile())->ready().Post(
FROM_HERE, run_loop.QuitWhenIdleClosure());
run_loop.Run();
}
// Uninstalls the extension with the given `extension_id` and for the given
// `uninstall_reason`, waiting until uninstallation has finished.
void UninstallExtension(const ExtensionId& extension_id,
UninstallReason uninstall_reason) {
base::RunLoop run_loop;
extension_registrar()->UninstallExtension(extension_id, uninstall_reason,
/*error=*/nullptr,
run_loop.QuitWhenIdleClosure());
run_loop.Run();
}
// Adds a new MV2 extension with the given `name` to the profile, returning
// it afterwards.
const Extension* AddMV2Extension(std::string_view name) {
return AddExtensionWithManifestVersion(name, 2);
}
const Extension* AddExtensionWithManifestVersion(std::string_view name,
int manifest_version) {
static constexpr char kManifest[] =
R"({
"name": "%s",
"manifest_version": %d,
"version": "0.1"
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(
base::StringPrintf(kManifest, name.data(), manifest_version));
return InstallExtension(test_dir.UnpackedPath(), /*expected_change=*/1,
mojom::ManifestLocation::kInternal);
}
// Returns true if the extension was explicitly re-enabled by the user after
// being disabled by the MV2 experiment.
bool WasExtensionReEnabledByUser(const ExtensionId& extension_id) {
return experiment_manager()->DidUserReEnableExtensionForTesting(
extension_id);
}
// Returns the UKM entries for the Extensions.MV2ExtensionHandledInSoftDisable
// event.
std::vector<raw_ptr<const ukm::mojom::UkmEntry, VectorExperimental>>
GetUkmEntries() {
return ukm_recorder().GetEntriesByName(
ukm::builders::Extensions_MV2ExtensionHandledInSoftDisable::kEntryName);
}
MV2ExperimentStage GetActiveExperimentStage() {
return experiment_manager()->GetCurrentExperimentStage();
}
ExtensionPrefs* extension_prefs() { return ExtensionPrefs::Get(profile()); }
ManifestV2ExperimentManager* experiment_manager() {
return ManifestV2ExperimentManager::Get(profile());
}
base::HistogramTester& histogram_tester() { return histogram_tester_; }
ukm::TestAutoSetUkmRecorder& ukm_recorder() { return *ukm_recorder_; }
private:
// Allows subclasses to add additional features to enable / disable.
virtual void PopulateAdditionalFeatures(
std::vector<base::test::FeatureRef>& enabled_features,
std::vector<base::test::FeatureRef>& disabled_features) {}
base::test::ScopedFeatureList feature_list_;
base::HistogramTester histogram_tester_;
std::optional<ukm::TestAutoSetUkmRecorder> ukm_recorder_;
};
// A test series to verify MV2 extensions are disabled on startup.
// Step 1 (Warning Only Stage): Install an MV2 extension.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
PRE_PRE_ExtensionsAreDisabledOnStartup) {
EXPECT_EQ(MV2ExperimentStage::kWarning, GetActiveExperimentStage());
const Extension* extension = AddMV2Extension("Test MV2 Extension");
ASSERT_TRUE(extension);
}
// Step 2 (Warning Only Stage): Verify the MV2 extension is still enabled after
// restarting the browser. Since this is still a PRE_ stage, the disabling
// experiment isn't active, and MV2 extensions should be unaffected.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
PRE_ExtensionsAreDisabledOnStartup) {
EXPECT_EQ(MV2ExperimentStage::kWarning, GetActiveExperimentStage());
WaitForExtensionSystemReady();
const Extension* extension = GetExtensionByName(
"Test MV2 Extension", extension_registry()->enabled_extensions());
ASSERT_TRUE(extension);
const ExtensionId extension_id = extension->id();
EXPECT_TRUE(
extension_registry()->enabled_extensions().Contains(extension_id));
EXPECT_TRUE(extension_prefs()->GetDisableReasons(extension_id).empty());
histogram_tester().ExpectTotalCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal", 0);
}
// Step 3 (Disable Stage): Verify the extension is disabled. Now the disabling
// experiment is active, and any old MV2 extensions are disabled.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
ExtensionsAreDisabledOnStartup) {
EXPECT_EQ(MV2ExperimentStage::kDisableWithReEnable,
GetActiveExperimentStage());
WaitForExtensionSystemReady();
const Extension* extension = GetExtensionByName(
"Test MV2 Extension",
extension_registry()->GenerateInstalledExtensionsSet());
ASSERT_TRUE(extension);
const ExtensionId extension_id = extension->id();
EXPECT_FALSE(
extension_registry()->enabled_extensions().Contains(extension_id));
EXPECT_TRUE(
extension_registry()->disabled_extensions().Contains(extension_id));
EXPECT_THAT(extension_prefs()->GetDisableReasons(extension_id),
testing::UnorderedElementsAre(
disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION));
// The extension is recorded as "soft disabled".
histogram_tester().ExpectTotalCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal", 1);
histogram_tester().ExpectBucketCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal",
ManifestV2ExperimentManager::MV2ExtensionState::kSoftDisabled, 1);
}
// A test series to verify extensions that are re-enabled by the user do not
// get re-disabled on subsequent starts.
// Step 1 (Warning Only Stage): Install an MV2 extension.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
PRE_PRE_ExtensionsCanBeReEnabledByUsers) {
EXPECT_EQ(MV2ExperimentStage::kWarning, GetActiveExperimentStage());
const Extension* extension = AddMV2Extension("Test MV2 Extension");
ASSERT_TRUE(extension);
}
// Step 2 (Disable Stage): The extension will be disabled by the experiment.
// Re-enable the extension.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
PRE_ExtensionsCanBeReEnabledByUsers) {
EXPECT_EQ(MV2ExperimentStage::kDisableWithReEnable,
GetActiveExperimentStage());
WaitForExtensionSystemReady();
const Extension* extension = GetExtensionByName(
"Test MV2 Extension", extension_registry()->disabled_extensions());
ASSERT_TRUE(extension);
const ExtensionId extension_id = extension->id();
// Before re-enabling the extension, there should be no UKM entries.
EXPECT_TRUE(GetUkmEntries().empty());
// Re-enable the disabled extension.
extension_registrar()->EnableExtension(extension_id);
// The extension should be properly re-enabled, the disable reasons cleared,
// and the extension should be marked as explicitly re-enabled.
EXPECT_TRUE(
extension_registry()->enabled_extensions().Contains(extension_id));
EXPECT_TRUE(extension_prefs()->GetDisableReasons(extension_id).empty());
EXPECT_TRUE(WasExtensionReEnabledByUser(extension_id));
// We should emit a UKM record for the re-enabling.
auto entries = GetUkmEntries();
ASSERT_EQ(1u, entries.size());
auto* entry = entries.front().get();
ukm_recorder().ExpectEntrySourceHasUrl(entry, extension->url());
ukm_recorder().ExpectEntryMetric(
entry,
ukm::builders::Extensions_MV2ExtensionHandledInSoftDisable::kActionName,
static_cast<int64_t>(ManifestV2ExperimentManager::
ExtensionMV2DeprecationAction::kReEnabled));
}
// Step 3 (Disable Stage): The extension should still be enabled on a subsequent
// start since the user explicitly chose to re-enable it.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
ExtensionsCanBeReEnabledByUsers) {
EXPECT_EQ(MV2ExperimentStage::kDisableWithReEnable,
GetActiveExperimentStage());
WaitForExtensionSystemReady();
const Extension* extension = GetExtensionByName(
"Test MV2 Extension", extension_registry()->enabled_extensions());
ASSERT_TRUE(extension);
const ExtensionId extension_id = extension->id();
EXPECT_TRUE(extension_prefs()->GetDisableReasons(extension_id).empty());
EXPECT_TRUE(WasExtensionReEnabledByUser(extension_id));
// The extension is reported as re-enabled by the user.
histogram_tester().ExpectTotalCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal", 1);
histogram_tester().ExpectBucketCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal",
ManifestV2ExperimentManager::MV2ExtensionState::kUserReEnabled, 1);
}
// Tests that extensions are re-enabled automatically if they update to MV3.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
ExtensionsAreReEnabledWhenUpdatedToMV3) {
EXPECT_EQ(MV2ExperimentStage::kDisableWithReEnable,
GetActiveExperimentStage());
WaitForExtensionSystemReady();
static constexpr char kManifestTemplate[] =
R"({
"name": "Test Extension",
"manifest_version": %d,
"version": "%s"
})";
std::string mv2_manifest = base::StringPrintf(kManifestTemplate, 2, "1.0");
std::string mv3_manifest = base::StringPrintf(kManifestTemplate, 3, "2.0");
TestExtensionDir test_dir;
test_dir.WriteManifest(mv2_manifest);
base::FilePath mv2_crx = test_dir.Pack("mv2.crx");
test_dir.WriteManifest(mv3_manifest);
base::FilePath mv3_crx = test_dir.Pack("mv3.crx");
const Extension* extension = InstallExtension(
mv2_crx, /*expected_change=*/1, mojom::ManifestLocation::kInternal);
ASSERT_TRUE(extension);
const ExtensionId extension_id = extension->id();
// Technically, this could be accomplished using a PRE_ test, similar to
// other browser tests in this file. However, that makes it much more
// difficult to update the extension to an MV3 version, since we couldn't
// construct the extension dynamically.
experiment_manager()->DisableAffectedExtensionsForTesting();
// The MV2 extension is disabled.
EXPECT_TRUE(
extension_registry()->disabled_extensions().Contains(extension_id));
EXPECT_THAT(extension_prefs()->GetDisableReasons(extension_id),
testing::UnorderedElementsAre(
disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION));
// Update the extension to MV3. Note: Even though this doesn't result in a
// _new_ extension, the `expected_change` is 1 here because this results in
// the extension being added to the enabled set (so the enabled extension
// count is 1 higher than it was before).
const Extension* updated_extension = UpdateExtension(extension_id, mv3_crx,
/*expected_change=*/1);
ASSERT_TRUE(updated_extension);
EXPECT_EQ(updated_extension->id(), extension_id);
// The new MV3 extension should be enabled.
EXPECT_EQ(3, updated_extension->manifest_version());
EXPECT_TRUE(
extension_registry()->enabled_extensions().Contains(extension_id));
EXPECT_TRUE(extension_prefs()->GetDisableReasons(extension_id).empty());
// The user didn't re-enable the extension, so it shouldn't be marked as such.
EXPECT_FALSE(WasExtensionReEnabledByUser(extension_id));
}
// Tests that the MV2 deprecation notice for an extension is only acknowledged
// for the current stage.
// Step 1 (Warning Stage): Mark an extension's notice as acknowledged on this
// stage.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
PRE_MarkingNoticeAsAcknowledged) {
EXPECT_EQ(MV2ExperimentStage::kWarning, GetActiveExperimentStage());
WaitForExtensionSystemReady();
// Add an extension and verify it's notice is not marked as acknowledged on
// this stage.
const Extension* extension = AddMV2Extension("Test MV2 Extension");
ASSERT_TRUE(extension);
EXPECT_FALSE(experiment_manager()->DidUserAcknowledgeNotice(extension->id()));
// Mark the notice as acknowledged for this stage. Verify it's acknowledged.
experiment_manager()->MarkNoticeAsAcknowledged(extension->id());
EXPECT_TRUE(experiment_manager()->DidUserAcknowledgeNotice(extension->id()));
}
// Step 2 (Disable Stage): Verify extension's notice is not acknowledged on this
// stage. Mark notice as acknowledged on this stage.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
MarkingNoticeAsAcknowledged) {
EXPECT_EQ(MV2ExperimentStage::kDisableWithReEnable,
GetActiveExperimentStage());
WaitForExtensionSystemReady();
// Verify extension's notice is not marked as acknowledged on this stage, even
// if it was acknowledged on the previous stage.
const Extension* extension = GetExtensionByName(
"Test MV2 Extension", extension_registry()->disabled_extensions());
ASSERT_TRUE(extension);
EXPECT_FALSE(experiment_manager()->DidUserAcknowledgeNotice(extension->id()));
// Mark the notice as acknowledged for this stage. Verify it's acknowledged.
experiment_manager()->MarkNoticeAsAcknowledged(extension->id());
EXPECT_TRUE(experiment_manager()->DidUserAcknowledgeNotice(extension->id()));
}
// Tests that the MV2 deprecation global notice is only acknowledged for the
// current stage.
// Step 1 (Warning Stage): Mark the global notice as acknowledged
// on this stage.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
PRE_MarkingGlobalNoticeAsAcknowledged) {
EXPECT_EQ(MV2ExperimentStage::kWarning, GetActiveExperimentStage());
WaitForExtensionSystemReady();
// Add an extension that should make the MV2 deprecation notice visible.
// Verify global notice is not marked as acknowledged on this stage.
const Extension* extension = AddMV2Extension("Test MV2 Extension");
ASSERT_TRUE(extension);
EXPECT_FALSE(experiment_manager()->DidUserAcknowledgeNoticeGlobally());
// Mark the global notice as acknowledged for this stage. Verify it's
// acknowledged.
experiment_manager()->MarkNoticeAsAcknowledgedGlobally();
EXPECT_TRUE(experiment_manager()->DidUserAcknowledgeNoticeGlobally());
}
// Step 2 (Disable Stage): Verify global notice is not acknowledged on this
// stage. Mark notice as acknowledged on this stage.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
MarkingGlobalNoticeAsAcknowledged) {
EXPECT_EQ(MV2ExperimentStage::kDisableWithReEnable,
GetActiveExperimentStage());
WaitForExtensionSystemReady();
// Verify global notice is not marked as acknowledged on this stage, even if
// it was acknowledged on the previous stage.
const Extension* extension = GetExtensionByName(
"Test MV2 Extension", extension_registry()->disabled_extensions());
ASSERT_TRUE(extension);
EXPECT_FALSE(experiment_manager()->DidUserAcknowledgeNoticeGlobally());
// Mark the global notice as acknowledged for this stage. Verify it's
// acknowledged.
experiment_manager()->MarkNoticeAsAcknowledgedGlobally();
EXPECT_TRUE(experiment_manager()->DidUserAcknowledgeNoticeGlobally());
}
// Tests that if a user moves from a later experiment stage (disable with
// re-enable) to an earlier one (warning), any disabled extensions will be
// automatically re-enabled.
// First stage: install an MV2 extension.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
PRE_PRE_ExtensionsAreReEnabledIfExperimentDisabled) {
EXPECT_EQ(MV2ExperimentStage::kWarning, GetActiveExperimentStage());
const Extension* extension = AddMV2Extension("Test MV2 Extension");
ASSERT_TRUE(extension);
}
// Second stage: MV2 deprecation experiment takes effect; extension is disabled.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
PRE_ExtensionsAreReEnabledIfExperimentDisabled) {
EXPECT_EQ(MV2ExperimentStage::kDisableWithReEnable,
GetActiveExperimentStage());
WaitForExtensionSystemReady();
const Extension* extension = GetExtensionByName(
"Test MV2 Extension", extension_registry()->disabled_extensions());
ASSERT_TRUE(extension);
const ExtensionId extension_id = extension->id();
EXPECT_THAT(extension_prefs()->GetDisableReasons(extension_id),
testing::UnorderedElementsAre(
disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION));
}
// Third stage: Move the user back to the warning stage. The extension should be
// re-enabled.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
ExtensionsAreReEnabledIfExperimentDisabled) {
EXPECT_EQ(MV2ExperimentStage::kWarning, GetActiveExperimentStage());
WaitForExtensionSystemReady();
const Extension* extension = GetExtensionByName(
"Test MV2 Extension", extension_registry()->enabled_extensions());
ASSERT_TRUE(extension);
const ExtensionId extension_id = extension->id();
EXPECT_TRUE(extension_prefs()->GetDisableReasons(extension_id).empty());
// The user didn't re-enable the extension, so it shouldn't be marked as such.
EXPECT_FALSE(WasExtensionReEnabledByUser(extension_id));
// Since the user is no longer in the disable phase, no metrics should be
// reported.
histogram_tester().ExpectTotalCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal", 0);
}
// Tests that externally-installed extensions are allowed to be installed, but
// will still be disabled by the MV2 experiments.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
ExternalExtensionsCanBeInstalledButAreAlsoDisabled) {
// External extensions are default-disabled on Windows and Mac. This won't
// be affected by the MV2 deprecation, but for consistency of testing, we
// disable this prompting in the test.
FeatureSwitch::ScopedOverride feature_override(
FeatureSwitch::prompt_for_external_extensions(), false);
// TODO(devlin): Update this to a different extension so we use one dedicated
// to this test ("good.crx" should likely be updated to MV3).
static constexpr char kExtensionId[] = "ldnnhddmnhbkjipkidpdiheffobcpfmf";
base::FilePath crx_path = test_data_dir_.AppendASCII("good.crx");
// Install a new external extension.
ExternalProviderManager* external_provider_manager =
ExternalProviderManager::Get(profile());
TestExtensionRegistryObserver observer(extension_registry());
auto provider = std::make_unique<MockExternalProvider>(
external_provider_manager, mojom::ManifestLocation::kExternalPref);
provider->UpdateOrAddExtension(kExtensionId, "1.0.0.0", crx_path);
external_provider_manager->AddProviderForTesting(std::move(provider));
external_provider_manager->CheckForExternalUpdates();
auto extension = observer.WaitForExtensionInstalled();
EXPECT_EQ(extension->id(), kExtensionId);
// The extension should install and be enabled. We allow installation of
// external extensions (unlike webstore extensions) because we can't know if
// the extension is MV2 or MV3 until we install it.
// We could theoretically disable it immediately if it's MV2, but it'll get
// disabled on the next run of Chrome.
EXPECT_TRUE(
extension_registry()->enabled_extensions().Contains(kExtensionId));
EXPECT_TRUE(extension_prefs()->GetDisableReasons(kExtensionId).empty());
// The extension should still be counted as "affected" by the MV2 deprecation.
EXPECT_TRUE(experiment_manager()->IsExtensionAffected(*extension));
// And should also be disabled when we check again.
experiment_manager()->DisableAffectedExtensionsForTesting();
EXPECT_TRUE(
extension_registry()->disabled_extensions().Contains(kExtensionId));
EXPECT_THAT(extension_prefs()->GetDisableReasons(kExtensionId),
testing::UnorderedElementsAre(
disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION));
}
// Tests that a UKM event is emitted when the user uninstalls a disabled
// extension.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
UkmIsEmittedForExtensionWhenUninstalled) {
EXPECT_EQ(MV2ExperimentStage::kDisableWithReEnable,
GetActiveExperimentStage());
WaitForExtensionSystemReady();
const Extension* extension = AddMV2Extension("Test MV2 Extension");
ASSERT_TRUE(extension);
experiment_manager()->DisableAffectedExtensionsForTesting();
EXPECT_TRUE(GetUkmEntries().empty());
// Since the extension will be uninstalled (and the pointer will become unsafe
// to use), cache its URL.
const GURL extension_url = extension->url();
UninstallExtension(extension->id(),
UninstallReason::UNINSTALL_REASON_USER_INITIATED);
auto entries = GetUkmEntries();
ASSERT_EQ(1u, entries.size());
auto* entry = entries.front().get();
ukm_recorder().ExpectEntrySourceHasUrl(entry, extension_url);
ukm_recorder().ExpectEntryMetric(
entry,
ukm::builders::Extensions_MV2ExtensionHandledInSoftDisable::kActionName,
static_cast<int64_t>(ManifestV2ExperimentManager::
ExtensionMV2DeprecationAction::kRemoved));
}
// Tests that UKM events are not emitted for unrelated uninstallations.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
UkmIsNotEmittedForOtherUninstallations) {
EXPECT_EQ(MV2ExperimentStage::kDisableWithReEnable,
GetActiveExperimentStage());
WaitForExtensionSystemReady();
const Extension* mv2_extension = AddMV2Extension("Test MV2 Extension");
ASSERT_TRUE(mv2_extension);
const Extension* mv3_extension =
AddExtensionWithManifestVersion("Test MV3 Extension", 3);
ASSERT_TRUE(mv3_extension);
experiment_manager()->DisableAffectedExtensionsForTesting();
EXPECT_TRUE(GetUkmEntries().empty());
// Uninstalling an MV2 extension for a reason other than user uninstallation
// should not trigger a UKM event.
UninstallExtension(mv2_extension->id(),
UninstallReason::UNINSTALL_REASON_MANAGEMENT_API);
EXPECT_TRUE(GetUkmEntries().empty());
// Uninstalling extensions that aren't affected by the MV2 experiments should
// not trigger a UKM event.
UninstallExtension(mv3_extension->id(),
UninstallReason::UNINSTALL_REASON_USER_INITIATED);
EXPECT_TRUE(GetUkmEntries().empty());
}
// Tests the flow from the "warning" phase to the "unsupported" phase.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
PRE_PRE_FlowFromWarningToUnsupported) {
EXPECT_EQ(MV2ExperimentStage::kWarning, GetActiveExperimentStage());
// Install two MV2 extensions.
const Extension* extension1 = AddMV2Extension("Test MV2 Extension 1");
ASSERT_TRUE(extension1);
const Extension* extension2 = AddMV2Extension("Test MV2 Extension 2");
ASSERT_TRUE(extension2);
}
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
PRE_FlowFromWarningToUnsupported) {
EXPECT_EQ(MV2ExperimentStage::kDisableWithReEnable,
GetActiveExperimentStage());
WaitForExtensionSystemReady();
// Both extensions should be disabled.
const Extension* extension1 = GetExtensionByName(
"Test MV2 Extension 1", extension_registry()->disabled_extensions());
ASSERT_TRUE(extension1);
const ExtensionId extension_id1 = extension1->id();
EXPECT_THAT(extension_prefs()->GetDisableReasons(extension_id1),
testing::UnorderedElementsAre(
disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION));
const Extension* extension2 = GetExtensionByName(
"Test MV2 Extension 2", extension_registry()->disabled_extensions());
ASSERT_TRUE(extension1);
const ExtensionId extension_id2 = extension2->id();
EXPECT_THAT(extension_prefs()->GetDisableReasons(extension_id2),
testing::UnorderedElementsAre(
disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION));
// The extensions should be recorded as "soft disabled".
histogram_tester().ExpectTotalCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal", 2);
histogram_tester().ExpectBucketCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal",
ManifestV2ExperimentManager::MV2ExtensionState::kSoftDisabled, 2);
// The user should be allowed to re-enable the extensions.
ExtensionSystem* system = ExtensionSystem::Get(profile());
{
disable_reason::DisableReason disable_reason = disable_reason::DISABLE_NONE;
EXPECT_FALSE(system->management_policy()->MustRemainDisabled(
extension1, &disable_reason));
EXPECT_EQ(disable_reason::DISABLE_NONE, disable_reason);
}
{
disable_reason::DisableReason disable_reason = disable_reason::DISABLE_NONE;
EXPECT_FALSE(system->management_policy()->MustRemainDisabled(
extension2, &disable_reason));
EXPECT_EQ(disable_reason::DISABLE_NONE, disable_reason);
}
// Re-enable the first MV2 extension (this is allowed in this phase).
extension_registrar()->EnableExtension(extension_id1);
// The first extension should be properly re-enabled, the disable reasons
// cleared, and the extension should be marked as explicitly re-enabled.
EXPECT_TRUE(
extension_registry()->enabled_extensions().Contains(extension_id1));
EXPECT_TRUE(extension_prefs()->GetDisableReasons(extension_id1).empty());
EXPECT_TRUE(WasExtensionReEnabledByUser(extension_id1));
}
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
FlowFromWarningToUnsupported) {
EXPECT_EQ(MV2ExperimentStage::kUnsupported, GetActiveExperimentStage());
WaitForExtensionSystemReady();
// Both extensions should be disabled.
// In the "unsupported" phase, both extensions should be disabled again, even
// though the first was re-enabled in a previous phase.
const Extension* extension1 = GetExtensionByName(
"Test MV2 Extension 1", extension_registry()->disabled_extensions());
ASSERT_TRUE(extension1);
const ExtensionId extension_id1 = extension1->id();
EXPECT_TRUE(WasExtensionReEnabledByUser(extension_id1));
EXPECT_THAT(extension_prefs()->GetDisableReasons(extension_id1),
testing::UnorderedElementsAre(
disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION));
const Extension* extension2 = GetExtensionByName(
"Test MV2 Extension 2", extension_registry()->disabled_extensions());
ASSERT_TRUE(extension2);
const ExtensionId extension_id2 = extension2->id();
EXPECT_FALSE(WasExtensionReEnabledByUser(extension_id2));
EXPECT_THAT(extension_prefs()->GetDisableReasons(extension_id2),
testing::UnorderedElementsAre(
disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION));
// The extensions should now be recorded as "hard disabled".
histogram_tester().ExpectTotalCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal", 2);
histogram_tester().ExpectBucketCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal",
ManifestV2ExperimentManager::MV2ExtensionState::kHardDisabled, 2);
// The user should no longer be allowed to re-enable the extensions.
ExtensionSystem* system = ExtensionSystem::Get(profile());
{
disable_reason::DisableReason disable_reason = disable_reason::DISABLE_NONE;
EXPECT_TRUE(system->management_policy()->MustRemainDisabled(
extension1, &disable_reason));
EXPECT_EQ(disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION,
disable_reason);
}
{
disable_reason::DisableReason disable_reason = disable_reason::DISABLE_NONE;
EXPECT_TRUE(system->management_policy()->MustRemainDisabled(
extension2, &disable_reason));
EXPECT_EQ(disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION,
disable_reason);
}
}
// Tests that unpacked extensions can be installed in the disabled experiment
// phase.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
UnpackedExtensionsCanBeInstalledInDisabledPhase) {
EXPECT_EQ(MV2ExperimentStage::kDisableWithReEnable,
GetActiveExperimentStage());
WaitForExtensionSystemReady();
static constexpr char kMv2Manifest[] =
R"({
"name": "Simple MV2",
"manifest_version": 2,
"version": "0.1"
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kMv2Manifest);
base::RunLoop run_loop;
std::string id;
scoped_refptr<UnpackedInstaller> installer =
UnpackedInstaller::Create(profile());
auto on_complete = [&run_loop, &id](const Extension* extension,
const base::FilePath& file_path,
const std::string& error) {
EXPECT_TRUE(extension);
EXPECT_EQ("", error);
id = extension->id();
run_loop.Quit();
};
installer->set_completion_callback(base::BindLambdaForTesting(on_complete));
installer->set_be_noisy_on_failure(false);
installer->Load(test_dir.UnpackedPath());
run_loop.Run();
EXPECT_TRUE(extension_registry()->enabled_extensions().Contains(id));
}
// Tests that unpacked extensions cannot be installed in the unsupported
// experiment phase.
IN_PROC_BROWSER_TEST_F(ManifestV2ExperimentManagerBrowserTest,
UnpackedExtensionsCannotBeInstalledInUnsupportedPhase) {
EXPECT_EQ(MV2ExperimentStage::kUnsupported, GetActiveExperimentStage());
WaitForExtensionSystemReady();
static constexpr char kMv2Manifest[] =
R"({
"name": "Simple MV2",
"manifest_version": 2,
"version": "0.1"
})";
TestExtensionDir test_dir;
test_dir.WriteManifest(kMv2Manifest);
base::RunLoop run_loop;
std::string install_error;
scoped_refptr<UnpackedInstaller> installer =
UnpackedInstaller::Create(profile());
auto on_complete = [&run_loop, &install_error](
const Extension* extension,
const base::FilePath& file_path,
const std::string& error) {
install_error = error;
run_loop.Quit();
};
installer->set_completion_callback(base::BindLambdaForTesting(on_complete));
installer->set_be_noisy_on_failure(false);
installer->Load(test_dir.UnpackedPath());
run_loop.Run();
EXPECT_EQ(
"Cannot install extension because it uses an unsupported "
"manifest version.",
install_error);
}
class ManifestV2ExperimentWithLegacyExtensionSupportTest
: public ManifestV2ExperimentManagerBrowserTest {
public:
ManifestV2ExperimentWithLegacyExtensionSupportTest() = default;
~ManifestV2ExperimentWithLegacyExtensionSupportTest() override = default;
private:
void PopulateAdditionalFeatures(
std::vector<base::test::FeatureRef>& enabled_features,
std::vector<base::test::FeatureRef>& disabled_features) override {
enabled_features.push_back(extensions_features::kAllowLegacyMV2Extensions);
}
};
// Tests that legacy unpacked MV2 extensions are still allowed (and aren't
// auto-disabled) if the kAllowLegacyMV2Extensions feature is enabled.
IN_PROC_BROWSER_TEST_F(
ManifestV2ExperimentWithLegacyExtensionSupportTest,
PRE_MV2ExtensionsAreNotDisabledIfLegacyExtensionSwitchIsApplied) {
EXPECT_EQ(MV2ExperimentStage::kWarning, GetActiveExperimentStage());
// Load two extensions: a packed extension and an unpacked extension.
const Extension* packed_extension =
AddMV2Extension("Test Packed MV2 Extension");
ASSERT_TRUE(packed_extension);
EXPECT_EQ(mojom::ManifestLocation::kInternal, packed_extension->location());
const Extension* unpacked_extension =
LoadExtension(test_data_dir_.AppendASCII("simple_mv2"));
ASSERT_TRUE(unpacked_extension);
EXPECT_EQ(mojom::ManifestLocation::kUnpacked, unpacked_extension->location());
}
IN_PROC_BROWSER_TEST_F(
ManifestV2ExperimentWithLegacyExtensionSupportTest,
MV2ExtensionsAreNotDisabledIfLegacyExtensionSwitchIsApplied) {
EXPECT_EQ(MV2ExperimentStage::kUnsupported, GetActiveExperimentStage());
WaitForExtensionSystemReady();
// The packed extension should have been disabled.
const Extension* packed_extension = GetExtensionByName(
"Test Packed MV2 Extension", extension_registry()->disabled_extensions());
ASSERT_TRUE(packed_extension);
const ExtensionId packed_extension_id = packed_extension->id();
EXPECT_THAT(extension_prefs()->GetDisableReasons(packed_extension_id),
testing::UnorderedElementsAre(
disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION));
// The user didn't re-enable the extension, so it shouldn't be marked as such.
EXPECT_FALSE(WasExtensionReEnabledByUser(packed_extension_id));
// The user is not allowed to re-enable the packed extension; the flag only
// applies to unpacked extensions.
ExtensionSystem* system = ExtensionSystem::Get(profile());
disable_reason::DisableReason disable_reason = disable_reason::DISABLE_NONE;
EXPECT_TRUE(system->management_policy()->MustRemainDisabled(packed_extension,
&disable_reason));
EXPECT_EQ(disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION,
disable_reason);
// The unpacked extension should still be enabled.
const Extension* unpacked_extension = GetExtensionByName(
"Simple MV2 Extension", extension_registry()->enabled_extensions());
ASSERT_TRUE(unpacked_extension);
const ExtensionId unpacked_extension_id = unpacked_extension->id();
EXPECT_TRUE(
extension_prefs()->GetDisableReasons(unpacked_extension_id).empty());
// The user didn't re-enable the extension, so it shouldn't be marked as such.
EXPECT_FALSE(WasExtensionReEnabledByUser(unpacked_extension_id));
// The user is allowed to re-enable the unpacked extension.
disable_reason = disable_reason::DISABLE_NONE;
EXPECT_FALSE(system->management_policy()->MustRemainDisabled(
unpacked_extension, &disable_reason));
EXPECT_EQ(disable_reason::DISABLE_NONE, disable_reason);
}
} // namespace extensions