blob: cf2f843c7dd6f068879a5b3e13883fd4c66dcd92 [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/memory/raw_ptr.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/extension_management_internal.h"
#include "chrome/browser/extensions/extension_service_user_test_base.h"
#include "chrome/browser/extensions/mv2_experiment_stage.h"
#include "chrome/browser/profiles/profile.h"
#include "components/crx_file/id_util.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "extensions/browser/disable_reason.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/pref_names.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/extension_features.h"
#include "extensions/common/mojom/manifest.mojom.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "components/account_id/account_id.h"
#include "components/user_manager/user.h"
#endif // BUILDFLAG(IS_CHROMEOS)
namespace extensions {
class ManifestV2ExperimentManagerUnitTestBase
: public ExtensionServiceUserTestBase {
public:
ManifestV2ExperimentManagerUnitTestBase(
const std::vector<base::test::FeatureRef>& enabled_features,
const std::vector<base::test::FeatureRef>& disabled_features);
~ManifestV2ExperimentManagerUnitTestBase() override = default;
void SetUp() override {
ExtensionServiceUserTestBase::SetUp();
// Note: This is (subtly) different from
// `InitializeEmptyExtensionService()`, which doesn't initialize a
// testing PrefService.
InitializeExtensionService(ExtensionServiceInitParams{});
#if BUILDFLAG(IS_CHROMEOS)
// Log in the user on CrOS. This is necessary for the profile to be
// considered one that can install extensions, which itself is
// necessary for metrics testing.
ASSERT_NO_FATAL_FAILURE(LoginChromeOSUser(
GetFakeUserManager()->AddUser(account_id_), account_id_));
#endif
experiment_manager_ = ManifestV2ExperimentManager::Get(profile());
}
void TearDown() override {
experiment_manager_ = nullptr;
ExtensionServiceUserTestBase::TearDown();
}
// Since this is testing the MV2 deprecation experiments, we don't want to
// bypass their disabling for testing.
bool ShouldAllowMV2Extensions() override { return false; }
ManifestV2ExperimentManager* experiment_manager() {
return experiment_manager_.get();
}
private:
base::test::ScopedFeatureList feature_list_;
raw_ptr<ManifestV2ExperimentManager> experiment_manager_;
};
ManifestV2ExperimentManagerUnitTestBase::
ManifestV2ExperimentManagerUnitTestBase(
const std::vector<base::test::FeatureRef>& enabled_features,
const std::vector<base::test::FeatureRef>& disabled_features) {
feature_list_.InitWithFeatures(enabled_features, disabled_features);
}
// Test suite for cases where the user is in the "warning" experiment phase.
class ManifestV2ExperimentManagerWarningUnitTest
: public ManifestV2ExperimentManagerUnitTestBase {
public:
ManifestV2ExperimentManagerWarningUnitTest()
: ManifestV2ExperimentManagerUnitTestBase(
{},
{extensions_features::kExtensionManifestV2Disabled,
extensions_features::kExtensionManifestV2Unsupported}) {}
~ManifestV2ExperimentManagerWarningUnitTest() override = default;
};
// Test suite for cases where the user is in the "disable with re-enable"
// experiment phase.
class ManifestV2ExperimentManagerDisableWithReEnableUnitTest
: public ManifestV2ExperimentManagerUnitTestBase {
public:
ManifestV2ExperimentManagerDisableWithReEnableUnitTest()
: ManifestV2ExperimentManagerUnitTestBase(
{extensions_features::kExtensionManifestV2Disabled},
{extensions_features::kExtensionManifestV2Unsupported}) {}
~ManifestV2ExperimentManagerDisableWithReEnableUnitTest() override = default;
};
// Test suite for cases where the user is in the "disable with re-enable"
// experiment phase *and* the warning experiment is still active.
class ManifestV2ExperimentManagerDisableWithReEnableAndWarningUnitTest
: public ManifestV2ExperimentManagerUnitTestBase {
public:
ManifestV2ExperimentManagerDisableWithReEnableAndWarningUnitTest()
: ManifestV2ExperimentManagerUnitTestBase(
{extensions_features::kExtensionManifestV2Disabled},
{extensions_features::kExtensionManifestV2Unsupported}) {}
~ManifestV2ExperimentManagerDisableWithReEnableAndWarningUnitTest() override =
default;
};
// Test suite for cases where the user is in the "unsupported" experiment phase.
class ManifestV2ExperimentManagerUnsupportedUnitTest
: public ManifestV2ExperimentManagerUnitTestBase {
public:
ManifestV2ExperimentManagerUnsupportedUnitTest()
: ManifestV2ExperimentManagerUnitTestBase(
{extensions_features::kExtensionManifestV2Unsupported},
{}) {}
~ManifestV2ExperimentManagerUnsupportedUnitTest() override = default;
};
// Tests that the experiment stage is properly set when the manifest V2
// deprecation warning experiment is enabled.
TEST_F(ManifestV2ExperimentManagerWarningUnitTest,
ExperimentStageIsSetToWarning) {
EXPECT_EQ(MV2ExperimentStage::kWarning,
experiment_manager()->GetCurrentExperimentStage());
}
// Sanity check that MV2 extensions are considered affected when the
// experiment is enabled. The "is affected" logic is much more heavily tested
// in mv2_deprecation_impact_checker_unittest.cc.
TEST_F(ManifestV2ExperimentManagerWarningUnitTest, MV2ExtensionsAreAffected) {
struct {
mojom::ManifestLocation manifest_location;
const char* name;
} test_cases[] = {
{mojom::ManifestLocation::kInternal, "internal"},
{mojom::ManifestLocation::kExternalPref, "external pref"},
{mojom::ManifestLocation::kExternalRegistry, "external registry"},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.name);
scoped_refptr<const Extension> mv2_extension =
ExtensionBuilder(test_case.name)
.SetManifestVersion(2)
.SetLocation(test_case.manifest_location)
.Build();
EXPECT_TRUE(experiment_manager()->IsExtensionAffected(*mv2_extension));
// Even though the MV2 extension is affected by the experiment, it should
// *not* be blocked from installation or enablement in the warning phase.
EXPECT_FALSE(experiment_manager()->ShouldBlockExtensionInstallation(
mv2_extension->id(), mv2_extension->manifest_version(),
mv2_extension->GetType(), mv2_extension->location(),
mv2_extension->hashed_id()));
EXPECT_FALSE(
experiment_manager()->ShouldBlockExtensionEnable(*mv2_extension));
// Modern extensions are not affected by the experiment.
scoped_refptr<const Extension> extension =
ExtensionBuilder(test_case.name)
.SetLocation(test_case.manifest_location)
.Build();
EXPECT_FALSE(experiment_manager()->IsExtensionAffected(*extension));
EXPECT_FALSE(experiment_manager()->ShouldBlockExtensionInstallation(
extension->id(), extension->manifest_version(), extension->GetType(),
extension->location(), extension->hashed_id()));
EXPECT_FALSE(experiment_manager()->ShouldBlockExtensionEnable(*extension));
}
}
TEST_F(ManifestV2ExperimentManagerWarningUnitTest,
MarkingNoticeAsAcknowledged) {
scoped_refptr<const Extension> ext1 =
ExtensionBuilder("one")
.SetManifestVersion(2)
.SetLocation(mojom::ManifestLocation::kInternal)
.Build();
scoped_refptr<const Extension> ext2 =
ExtensionBuilder("two")
.SetManifestVersion(2)
.SetLocation(mojom::ManifestLocation::kInternal)
.Build();
registrar()->AddExtension(ext1.get());
registrar()->AddExtension(ext2.get());
EXPECT_FALSE(experiment_manager()->DidUserAcknowledgeNotice(ext1->id()));
EXPECT_FALSE(experiment_manager()->DidUserAcknowledgeNotice(ext2->id()));
experiment_manager()->MarkNoticeAsAcknowledged(ext1->id());
EXPECT_TRUE(experiment_manager()->DidUserAcknowledgeNotice(ext1->id()));
EXPECT_FALSE(experiment_manager()->DidUserAcknowledgeNotice(ext2->id()));
}
TEST_F(ManifestV2ExperimentManagerWarningUnitTest,
MarkingGlobalNoticeAsAcknowledged) {
EXPECT_FALSE(experiment_manager()->DidUserAcknowledgeNoticeGlobally());
experiment_manager()->MarkNoticeAsAcknowledgedGlobally();
EXPECT_TRUE(experiment_manager()->DidUserAcknowledgeNoticeGlobally());
}
// Tests that the experiment phase is properly set for a user in the
// "disable with re-enable" experiment phase.
TEST_F(ManifestV2ExperimentManagerDisableWithReEnableUnitTest,
ExperimentStageIsSetToDisableWithReEnable) {
EXPECT_EQ(MV2ExperimentStage::kDisableWithReEnable,
experiment_manager()->GetCurrentExperimentStage());
}
// Tests that the experiment phase is properly set for a user in the
// "disable with re-enable" experiment phase if the "warning" experiment is
// still active. That is, verifies that the "latest" stage takes precedence.
TEST_F(ManifestV2ExperimentManagerDisableWithReEnableAndWarningUnitTest,
ExperimentStageIsSetToDisableWithReEnable) {
EXPECT_EQ(MV2ExperimentStage::kDisableWithReEnable,
experiment_manager()->GetCurrentExperimentStage());
}
// Sanity check that MV2 extensions are considered affected when the
// experiment is enabled in the "disable with re-enable" phase. The
// "is affected" logic is much more heavily tested
// in mv2_deprecation_impact_checker_unittest.cc.
TEST_F(ManifestV2ExperimentManagerDisableWithReEnableUnitTest,
MV2ExtensionsAreAffected) {
struct {
mojom::ManifestLocation manifest_location;
const char* name;
} test_cases[] = {
{mojom::ManifestLocation::kInternal, "internal"},
{mojom::ManifestLocation::kExternalPref, "external pref"},
{mojom::ManifestLocation::kExternalRegistry, "external registry"},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.name);
scoped_refptr<const Extension> mv2_extension =
ExtensionBuilder(test_case.name)
.SetManifestVersion(2)
.SetLocation(test_case.manifest_location)
.Build();
EXPECT_TRUE(experiment_manager()->IsExtensionAffected(*mv2_extension));
// Modern extensions are not affected by the experiment.
scoped_refptr<const Extension> extension =
ExtensionBuilder(test_case.name)
.SetLocation(test_case.manifest_location)
.Build();
EXPECT_FALSE(experiment_manager()->IsExtensionAffected(*extension));
}
}
// Tests the manager properly indicates when to block user-installed extensions
// while the "soft disable" experiment stage is active.
TEST_F(ManifestV2ExperimentManagerDisableWithReEnableUnitTest,
ShouldBlockInstallation_UserInstalledExtensions) {
constexpr bool kInstallShouldBeBlocked = true;
constexpr bool kInstallShouldBeAllowed = false;
struct {
mojom::ManifestLocation manifest_location;
int manifest_version;
const char* name;
bool should_block_install;
} test_cases[] = {
// Unpacked extensions (including commandline-loaded extensions) should
// still be installable. This allows developers to continue testing their
// extensions during the experiment periods.
{mojom::ManifestLocation::kUnpacked, 2, "unpacked - mv2",
kInstallShouldBeAllowed},
{mojom::ManifestLocation::kUnpacked, 3, "unpacked - mv3",
kInstallShouldBeAllowed},
{mojom::ManifestLocation::kCommandLine, 2, "command line - mv2",
kInstallShouldBeAllowed},
{mojom::ManifestLocation::kCommandLine, 3, "command line - mv3",
kInstallShouldBeAllowed},
// Other user-visible extension types should only be blocked if they are
// MV2.
{mojom::ManifestLocation::kInternal, 2, "internal - mv2",
kInstallShouldBeBlocked},
{mojom::ManifestLocation::kInternal, 3, "internal - mv3",
kInstallShouldBeAllowed},
{mojom::ManifestLocation::kExternalPref, 2, "external pref - mv2",
kInstallShouldBeBlocked},
{mojom::ManifestLocation::kExternalPref, 3, "external pref - mv3",
kInstallShouldBeAllowed},
{mojom::ManifestLocation::kExternalPref, 2, "external registry - mv2",
kInstallShouldBeBlocked},
{mojom::ManifestLocation::kExternalRegistry, 3, "external registry - mv3",
kInstallShouldBeAllowed},
{mojom::ManifestLocation::kExternalPrefDownload, 2,
"external download - mv2", kInstallShouldBeBlocked},
{mojom::ManifestLocation::kExternalPrefDownload, 3,
"external download - mv3", kInstallShouldBeAllowed},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.name);
scoped_refptr<const Extension> extension =
ExtensionBuilder(test_case.name)
.SetManifestVersion(test_case.manifest_version)
.SetLocation(test_case.manifest_location)
.Build();
EXPECT_EQ(test_case.should_block_install,
experiment_manager()->ShouldBlockExtensionInstallation(
extension->id(), extension->manifest_version(),
extension->GetType(), extension->location(),
extension->hashed_id()));
// During this stage, extension *enablement* should never be blocked.
EXPECT_FALSE(experiment_manager()->ShouldBlockExtensionEnable(*extension));
}
}
// Tests the manager never blocks component extensions from installing.
TEST_F(ManifestV2ExperimentManagerDisableWithReEnableUnitTest,
ShouldBlockInstallation_ComponentExtensions) {
struct {
mojom::ManifestLocation manifest_location;
int manifest_version;
const char* name;
} test_cases[] = {
{mojom::ManifestLocation::kComponent, 2, "component - mv2"},
{mojom::ManifestLocation::kComponent, 3, "component - mv3"},
{mojom::ManifestLocation::kExternalComponent, 2,
"external component - mv2"},
{mojom::ManifestLocation::kExternalComponent, 3,
"external component - mv3"},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.name);
scoped_refptr<const Extension> extension =
ExtensionBuilder(test_case.name)
.SetManifestVersion(test_case.manifest_version)
.SetLocation(test_case.manifest_location)
.Build();
// Component extensions are built-in parts of Chrome that are extensions as
// an implementation detail. They should always be allowed to install and
// remain enabled.
EXPECT_FALSE(experiment_manager()->ShouldBlockExtensionInstallation(
extension->id(), extension->manifest_version(), extension->GetType(),
extension->location(), extension->hashed_id()));
EXPECT_FALSE(experiment_manager()->ShouldBlockExtensionEnable(*extension));
}
}
TEST_F(ManifestV2ExperimentManagerDisableWithReEnableUnitTest,
MarkingNoticeAsAcknowledged) {
scoped_refptr<const Extension> ext1 =
ExtensionBuilder("one")
.SetManifestVersion(2)
.SetLocation(mojom::ManifestLocation::kInternal)
.Build();
scoped_refptr<const Extension> ext2 =
ExtensionBuilder("two")
.SetManifestVersion(2)
.SetLocation(mojom::ManifestLocation::kInternal)
.Build();
registrar()->AddExtension(ext1.get());
registrar()->AddExtension(ext2.get());
EXPECT_FALSE(experiment_manager()->DidUserAcknowledgeNotice(ext1->id()));
EXPECT_FALSE(experiment_manager()->DidUserAcknowledgeNotice(ext2->id()));
experiment_manager()->MarkNoticeAsAcknowledged(ext1->id());
EXPECT_TRUE(experiment_manager()->DidUserAcknowledgeNotice(ext1->id()));
EXPECT_FALSE(experiment_manager()->DidUserAcknowledgeNotice(ext2->id()));
}
// Tests that the proper manifest group is used when emitting metrics for
// disabled extensions.
TEST_F(ManifestV2ExperimentManagerDisableWithReEnableUnitTest,
ProfileMetrics_ExtensionLocationsAreProperlyGrouped) {
struct {
mojom::ManifestLocation manifest_location;
std::string name;
std::string expected_histogram;
} test_cases[] = {
{mojom::ManifestLocation::kInternal, "Internal",
"Extensions.MV2Deprecation.MV2ExtensionState.Internal"},
// Note: component extensions aren't considered in the metrics, so
// shouldn't have any emitted histograms.
{mojom::ManifestLocation::kComponent, "Component", ""},
{mojom::ManifestLocation::kExternalPolicy, "Policy",
"Extensions.MV2Deprecation.MV2ExtensionState.Policy"},
{mojom::ManifestLocation::kExternalPolicyDownload, "Policy Download",
"Extensions.MV2Deprecation.MV2ExtensionState.Policy"},
{mojom::ManifestLocation::kExternalPref, "Pref",
"Extensions.MV2Deprecation.MV2ExtensionState.External"},
{mojom::ManifestLocation::kExternalPrefDownload, "Pref Download",
"Extensions.MV2Deprecation.MV2ExtensionState.External"},
{mojom::ManifestLocation::kExternalRegistry, "Registry",
"Extensions.MV2Deprecation.MV2ExtensionState.External"},
{mojom::ManifestLocation::kUnpacked, "Unpacked",
"Extensions.MV2Deprecation.MV2ExtensionState.Unpacked"},
{mojom::ManifestLocation::kCommandLine, "Command Line",
"Extensions.MV2Deprecation.MV2ExtensionState.Unpacked"},
};
const char* histograms[] = {
"Extensions.MV2Deprecation.MV2ExtensionState.Internal",
"Extensions.MV2Deprecation.MV2ExtensionState.Component",
"Extensions.MV2Deprecation.MV2ExtensionState.Unpacked",
"Extensions.MV2Deprecation.MV2ExtensionState.Policy",
"Extensions.MV2Deprecation.MV2ExtensionState.External",
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.name);
base::HistogramTester histogram_tester;
// Install the extension, disable affected extensions (which should usually
// include this extension), and record histograms.
scoped_refptr<const Extension> extension =
ExtensionBuilder(test_case.name)
.SetManifestVersion(2)
.SetLocation(test_case.manifest_location)
.Build();
registrar()->AddExtension(extension.get());
experiment_manager()->DisableAffectedExtensionsForTesting();
experiment_manager()->EmitMetricsForProfileReadyForTesting();
// In each case, at most one histogram should have any records, and it
// should have exactly one entry: the extension is soft-disabled.
for (const char* histogram : histograms) {
if (test_case.expected_histogram == histogram) {
histogram_tester.ExpectBucketCount(
histogram,
ManifestV2ExperimentManager::MV2ExtensionState::kSoftDisabled, 1);
} else {
histogram_tester.ExpectTotalCount(histogram, 0);
}
}
// Unload the extension so it doesn't interfere in later cases.
registrar()->RemoveExtension(extension->id(),
UnloadedExtensionReason::UNINSTALL);
}
}
// Tests that modern extensions don't emit any metrics.
TEST_F(ManifestV2ExperimentManagerDisableWithReEnableUnitTest,
ProfileMetrics_ModernExtensionsArentIncluded) {
base::HistogramTester histogram_tester;
scoped_refptr<const Extension> extension =
ExtensionBuilder("Test Extension")
.SetLocation(mojom::ManifestLocation::kInternal)
.Build();
registrar()->AddExtension(extension.get());
experiment_manager()->DisableAffectedExtensionsForTesting();
experiment_manager()->EmitMetricsForProfileReadyForTesting();
histogram_tester.ExpectTotalCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal", 0);
}
// Tests that extensions that are re-enabled by the user are properly emitted
// as `kUserReEnabled`.
TEST_F(ManifestV2ExperimentManagerDisableWithReEnableUnitTest,
ProfileMetrics_UserReEnabledAreProperlyEmitted) {
base::HistogramTester histogram_tester;
scoped_refptr<const Extension> extension =
ExtensionBuilder("Test Extension")
.SetManifestVersion(2)
.SetLocation(mojom::ManifestLocation::kInternal)
.Build();
registrar()->AddExtension(extension.get());
experiment_manager()->DisableAffectedExtensionsForTesting();
registrar()->EnableExtension(extension->id());
experiment_manager()->EmitMetricsForProfileReadyForTesting();
histogram_tester.ExpectTotalCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal", 1);
histogram_tester.ExpectBucketCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal",
ManifestV2ExperimentManager::MV2ExtensionState::kUserReEnabled, 1);
}
// Tests that extensions that are disabled for other reasons (such as by user
// action) emit `kOther` for their state.
TEST_F(ManifestV2ExperimentManagerDisableWithReEnableUnitTest,
ProfileMetrics_ExtensionsThatAreDisabledForOtherReasonsEmitOther) {
base::HistogramTester histogram_tester;
scoped_refptr<const Extension> extension =
ExtensionBuilder("Test Extension")
.SetManifestVersion(2)
.SetLocation(mojom::ManifestLocation::kInternal)
.Build();
registrar()->AddExtension(extension.get());
experiment_manager()->DisableAffectedExtensionsForTesting();
registrar()->EnableExtension(extension->id());
registrar()->DisableExtension(extension->id(),
{disable_reason::DISABLE_USER_ACTION});
experiment_manager()->EmitMetricsForProfileReadyForTesting();
histogram_tester.ExpectTotalCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal", 1);
histogram_tester.ExpectBucketCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal",
ManifestV2ExperimentManager::MV2ExtensionState::kOther, 1);
}
enum class MV2PolicyLevel {
kAllowed,
kDisallowed,
kAllowedForAdminInstalledOnly,
};
// A test suite to allow setting various MV2-related policies.
class ManifestV2ExperimentManagerDisableWithReEnableAndPolicyUnitTest
: public ManifestV2ExperimentManagerDisableWithReEnableUnitTest {
public:
ManifestV2ExperimentManagerDisableWithReEnableAndPolicyUnitTest() = default;
~ManifestV2ExperimentManagerDisableWithReEnableAndPolicyUnitTest() override =
default;
// Sets the current level of the MV2 admin policy.
void SetMV2PolicyLevel(MV2PolicyLevel policy_level) {
internal::GlobalSettings::ManifestV2Setting pref_value;
switch (policy_level) {
case MV2PolicyLevel::kAllowed:
pref_value = internal::GlobalSettings::ManifestV2Setting::kEnabled;
break;
case MV2PolicyLevel::kDisallowed:
pref_value = internal::GlobalSettings::ManifestV2Setting::kDisabled;
break;
case MV2PolicyLevel::kAllowedForAdminInstalledOnly:
pref_value = internal::GlobalSettings::ManifestV2Setting::
kEnabledForForceInstalled;
break;
}
sync_preferences::TestingPrefServiceSyncable* pref_service =
testing_pref_service();
pref_service->SetManagedPref(pref_names::kManifestV2Availability,
base::Value(static_cast<int>(pref_value)));
}
// Clears the MV2 policy.
void ClearMV2Policy() {
sync_preferences::TestingPrefServiceSyncable* pref_service =
testing_pref_service();
pref_service->RemoveManagedPref(pref_names::kManifestV2Availability);
}
void AddPolicyInstalledMV2Extension(const ExtensionId& id,
mojom::ManifestLocation location) {
sync_preferences::TestingPrefServiceSyncable* pref_service =
testing_pref_service();
const base::Value* existing_value =
pref_service->GetManagedPref(pref_names::kExtensionManagement);
base::Value::Dict new_value;
if (existing_value) {
new_value = existing_value->Clone().TakeDict();
}
new_value.Set(id, base::Value::Dict()
.Set("installation_mode", "force_installed")
.Set("update_url", "http://example.com/"));
pref_service->SetManagedPref(pref_names::kExtensionManagement,
std::move(new_value));
}
};
// Tests that installation of all extensions is allowed if MV2 is allowed by
// policy.
TEST_F(ManifestV2ExperimentManagerDisableWithReEnableAndPolicyUnitTest,
ShouldBlockInstallation_DontBlockWhenAllMV2Allowed) {
SetMV2PolicyLevel(MV2PolicyLevel::kAllowed);
struct {
mojom::ManifestLocation manifest_location;
const char* name;
} test_cases[] = {
{mojom::ManifestLocation::kInternal, "internal"},
{mojom::ManifestLocation::kExternalPref, "pref"},
{mojom::ManifestLocation::kExternalPrefDownload, "pref download"},
{mojom::ManifestLocation::kExternalPolicy, "policy"},
{mojom::ManifestLocation::kExternalPolicyDownload, "policy download"},
{mojom::ManifestLocation::kUnpacked, "unpacked"},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.name);
scoped_refptr<const Extension> extension =
ExtensionBuilder(test_case.name)
.SetManifestVersion(2)
.SetLocation(test_case.manifest_location)
.Build();
// If MV2 is allowed by policy, all extensions should be allowed to install
// and be enabled.
EXPECT_FALSE(experiment_manager()->ShouldBlockExtensionInstallation(
extension->id(), extension->manifest_version(), extension->GetType(),
extension->location(), extension->hashed_id()));
EXPECT_FALSE(experiment_manager()->ShouldBlockExtensionEnable(*extension));
}
}
// Tests installation of all extensions (other than component extensions) is
// disallowed if disallowed by policy.
TEST_F(ManifestV2ExperimentManagerDisableWithReEnableAndPolicyUnitTest,
ShouldBlockInstallation_AllAreBlockedWhenMV2Disallowed) {
SetMV2PolicyLevel(MV2PolicyLevel::kDisallowed);
struct {
mojom::ManifestLocation manifest_location;
const char* name;
} test_cases[] = {
{mojom::ManifestLocation::kInternal, "internal"},
{mojom::ManifestLocation::kExternalPref, "pref"},
{mojom::ManifestLocation::kExternalPrefDownload, "pref download"},
{mojom::ManifestLocation::kExternalPolicy, "policy"},
{mojom::ManifestLocation::kExternalPolicyDownload, "policy download"},
{mojom::ManifestLocation::kUnpacked, "unpacked"},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.name);
ExtensionId extension_id = crx_file::id_util::GenerateId(test_case.name);
EXPECT_TRUE(experiment_manager()->ShouldBlockExtensionInstallation(
extension_id, /*manifest_version=*/2, Manifest::TYPE_EXTENSION,
test_case.manifest_location, HashedExtensionId(extension_id)));
}
}
// Tests admin-installed extensions may be installed, while others may not be,
// if the MV2 policy is set to admin-installed-only.
TEST_F(
ManifestV2ExperimentManagerDisableWithReEnableAndPolicyUnitTest,
ShouldBlockInstallation_UserInstalledAreBlockedWhenForceInstalledAllowed) {
SetMV2PolicyLevel(MV2PolicyLevel::kAllowedForAdminInstalledOnly);
constexpr bool kInstallShouldBeBlocked = true;
constexpr bool kInstallShouldBeAllowed = false;
constexpr bool kForceInstalled = true;
constexpr bool kUserInstalled = false;
struct {
mojom::ManifestLocation manifest_location;
const char* name;
bool force_installed;
bool should_block_install;
} test_cases[] = {
{mojom::ManifestLocation::kInternal, "internal", kUserInstalled,
kInstallShouldBeBlocked},
{mojom::ManifestLocation::kExternalPref, "pref", kUserInstalled,
kInstallShouldBeBlocked},
{mojom::ManifestLocation::kUnpacked, "unpacked", kUserInstalled,
kInstallShouldBeBlocked},
{mojom::ManifestLocation::kExternalPolicy, "policy", kForceInstalled,
kInstallShouldBeAllowed},
{mojom::ManifestLocation::kExternalPolicyDownload, "policy download",
kForceInstalled, kInstallShouldBeAllowed},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.name);
ExtensionId extension_id = crx_file::id_util::GenerateId(test_case.name);
if (test_case.force_installed) {
AddPolicyInstalledMV2Extension(extension_id, test_case.manifest_location);
}
EXPECT_EQ(
test_case.should_block_install,
experiment_manager()->ShouldBlockExtensionInstallation(
extension_id, /*manifest_version=*/2, Manifest::TYPE_EXTENSION,
test_case.manifest_location, HashedExtensionId(extension_id)));
}
}
TEST_F(ManifestV2ExperimentManagerDisableWithReEnableAndPolicyUnitTest,
ExtensionsAreReEnabledOrDisabledOnPolicyChange) {
// Install an MV2 extension and bootstrap the manager by disabling affected
// extensions.
scoped_refptr<const Extension> extension =
ExtensionBuilder("test extension").SetManifestVersion(2).Build();
registrar()->AddExtension(extension.get());
const ExtensionId extension_id = extension->id();
experiment_manager()->DisableAffectedExtensionsForTesting();
// The extension should be disabled.
ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(profile());
EXPECT_TRUE(registry()->disabled_extensions().Contains(extension_id));
EXPECT_THAT(extension_prefs->GetDisableReasons(extension_id),
testing::UnorderedElementsAre(
disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION));
// Set the MV2 policy to allow all MV2 extensions.
SetMV2PolicyLevel(MV2PolicyLevel::kAllowed);
// The extension should be enabled, since it's now allowed.
EXPECT_TRUE(registry()->enabled_extensions().Contains(extension_id));
EXPECT_TRUE(extension_prefs->GetDisableReasons(extension_id).empty());
// Clear the MV2 policy. The extension should now be disabled again.
ClearMV2Policy();
EXPECT_TRUE(registry()->disabled_extensions().Contains(extension_id));
EXPECT_THAT(extension_prefs->GetDisableReasons(extension_id),
testing::UnorderedElementsAre(
disable_reason::DISABLE_UNSUPPORTED_MANIFEST_VERSION));
}
// Tests that MV2 extensions that are allowed by policy emit `kUnaffected` for
// their state.
TEST_F(ManifestV2ExperimentManagerDisableWithReEnableAndPolicyUnitTest,
ProfileMetrics_ExtensionsAllowedByPolicyEmitUnaffected) {
SetMV2PolicyLevel(MV2PolicyLevel::kAllowed);
base::HistogramTester histogram_tester;
scoped_refptr<const Extension> extension =
ExtensionBuilder("Test Extension")
.SetManifestVersion(2)
.SetLocation(mojom::ManifestLocation::kInternal)
.Build();
registrar()->AddExtension(extension.get());
experiment_manager()->DisableAffectedExtensionsForTesting();
experiment_manager()->EmitMetricsForProfileReadyForTesting();
histogram_tester.ExpectTotalCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal", 1);
histogram_tester.ExpectBucketCount(
"Extensions.MV2Deprecation.MV2ExtensionState.Internal",
ManifestV2ExperimentManager::MV2ExtensionState::kUnaffected, 1);
}
// Tests that the experiment phase is properly set for a user in the
// "unsupported" experiment phase.
TEST_F(ManifestV2ExperimentManagerUnsupportedUnitTest,
ExperimentStageIsSetToUnsupported) {
EXPECT_EQ(MV2ExperimentStage::kUnsupported,
experiment_manager()->GetCurrentExperimentStage());
}
// Tests that MV2 extensions cannot be re-enabled in the "unsupported"
// experiment phase.
TEST_F(ManifestV2ExperimentManagerUnsupportedUnitTest,
MV2ExtensionsCannotBeEnabled) {
constexpr bool kEnableShouldBeBlocked = true;
constexpr bool kEnableShouldBeAllowed = false;
struct {
mojom::ManifestLocation manifest_location;
int manifest_version;
const char* name;
bool should_block_enable;
} test_cases[] = {
// The vast majority of extensions should be not be enable-able if they
// are MV2.
{mojom::ManifestLocation::kUnpacked, 2, "unpacked - mv2",
kEnableShouldBeBlocked},
{mojom::ManifestLocation::kUnpacked, 3, "unpacked - mv3",
kEnableShouldBeAllowed},
{mojom::ManifestLocation::kCommandLine, 2, "command line - mv2",
kEnableShouldBeBlocked},
{mojom::ManifestLocation::kCommandLine, 3, "command line - mv3",
kEnableShouldBeAllowed},
{mojom::ManifestLocation::kInternal, 2, "internal - mv2",
kEnableShouldBeBlocked},
{mojom::ManifestLocation::kInternal, 3, "internal - mv3",
kEnableShouldBeAllowed},
{mojom::ManifestLocation::kExternalPref, 2, "external pref - mv2",
kEnableShouldBeBlocked},
{mojom::ManifestLocation::kExternalPref, 3, "external pref - mv3",
kEnableShouldBeAllowed},
{mojom::ManifestLocation::kExternalPref, 2, "external registry - mv2",
kEnableShouldBeBlocked},
{mojom::ManifestLocation::kExternalRegistry, 3, "external registry - mv3",
kEnableShouldBeAllowed},
{mojom::ManifestLocation::kExternalPrefDownload, 2,
"external download - mv2", kEnableShouldBeBlocked},
{mojom::ManifestLocation::kExternalPrefDownload, 3,
"external download - mv3", kEnableShouldBeAllowed},
{mojom::ManifestLocation::kExternalPolicy, 2, "external policy - mv2",
kEnableShouldBeBlocked},
{mojom::ManifestLocation::kExternalPolicy, 3, "external policy - mv3",
kEnableShouldBeAllowed},
{mojom::ManifestLocation::kComponent, 2, "component - mv2",
kEnableShouldBeAllowed},
{mojom::ManifestLocation::kComponent, 3, "component - mv3",
kEnableShouldBeAllowed},
{mojom::ManifestLocation::kExternalComponent, 2, "component - mv2",
kEnableShouldBeAllowed},
{mojom::ManifestLocation::kExternalComponent, 3, "component - mv3",
kEnableShouldBeAllowed},
};
for (const auto& test_case : test_cases) {
SCOPED_TRACE(test_case.name);
scoped_refptr<const Extension> extension =
ExtensionBuilder(test_case.name)
.SetManifestVersion(test_case.manifest_version)
.SetLocation(test_case.manifest_location)
.Build();
EXPECT_EQ(test_case.should_block_enable,
experiment_manager()->ShouldBlockExtensionEnable(*extension));
}
}
} // namespace extensions