| // Copyright 2015 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/extensions/api/developer_private/developer_private_api.h" |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/files/file_util.h" |
| #include "base/macros.h" |
| #include "base/scoped_observer.h" |
| #include "base/stl_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "chrome/browser/chrome_notification_types.h" |
| #include "chrome/browser/extensions/chrome_test_extension_loader.h" |
| #include "chrome/browser/extensions/error_console/error_console.h" |
| #include "chrome/browser/extensions/extension_function_test_utils.h" |
| #include "chrome/browser/extensions/extension_management.h" |
| #include "chrome/browser/extensions/extension_management_test_util.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/extensions/extension_service_test_with_install.h" |
| #include "chrome/browser/extensions/extension_util.h" |
| #include "chrome/browser/extensions/permissions_updater.h" |
| #include "chrome/browser/extensions/scripting_permissions_modifier.h" |
| #include "chrome/browser/extensions/test_extension_system.h" |
| #include "chrome/browser/extensions/unpacked_installer.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/common/extensions/api/developer_private.h" |
| #include "chrome/common/pref_names.h" |
| #include "chrome/test/base/test_browser_window.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "components/crx_file/id_util.h" |
| #include "components/policy/core/browser/browser_policy_connector.h" |
| #include "components/policy/core/common/mock_configuration_policy_provider.h" |
| #include "components/policy/core/common/policy_map.h" |
| #include "components/policy/core/common/policy_service_impl.h" |
| #include "components/policy/core/common/policy_types.h" |
| #include "components/policy/policy_constants.h" |
| #include "components/services/unzip/unzip_service.h" |
| #include "components/sync_preferences/testing_pref_service_syncable.h" |
| #include "content/public/browser/notification_service.h" |
| #include "content/public/test/web_contents_tester.h" |
| #include "extensions/browser/api_test_utils.h" |
| #include "extensions/browser/event_router.h" |
| #include "extensions/browser/event_router_factory.h" |
| #include "extensions/browser/extension_dialog_auto_confirm.h" |
| #include "extensions/browser/extension_error_test_util.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_registry_observer.h" |
| #include "extensions/browser/extension_system.h" |
| #include "extensions/browser/extension_util.h" |
| #include "extensions/browser/install/extension_install_ui.h" |
| #include "extensions/browser/mock_external_provider.h" |
| #include "extensions/browser/test_event_router_observer.h" |
| #include "extensions/browser/test_extension_registry_observer.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_builder.h" |
| #include "extensions/common/extension_features.h" |
| #include "extensions/common/extension_set.h" |
| #include "extensions/common/manifest_constants.h" |
| #include "extensions/common/permissions/permission_set.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "extensions/common/value_builder.h" |
| #include "extensions/test/test_extension_dir.h" |
| #include "services/data_decoder/data_decoder_service.h" |
| #include "services/service_manager/public/cpp/connector.h" |
| #include "services/service_manager/public/cpp/test/test_connector_factory.h" |
| |
| using testing::Return; |
| using testing::_; |
| |
| namespace extensions { |
| |
| namespace { |
| |
| const char kGoodCrx[] = "ldnnhddmnhbkjipkidpdiheffobcpfmf"; |
| constexpr char kInvalidHost[] = "invalid host"; |
| constexpr char kInvalidHostError[] = "Invalid host."; |
| |
| std::unique_ptr<KeyedService> BuildAPI(content::BrowserContext* context) { |
| return std::make_unique<DeveloperPrivateAPI>(context); |
| } |
| |
| std::unique_ptr<KeyedService> BuildEventRouter( |
| content::BrowserContext* profile) { |
| return std::make_unique<EventRouter>(profile, ExtensionPrefs::Get(profile)); |
| } |
| |
| bool HasPrefsPermission(bool (*has_pref)(const std::string&, |
| content::BrowserContext*), |
| content::BrowserContext* context, |
| const std::string& id) { |
| return has_pref(id, context); |
| } |
| |
| bool WasPermissionsUpdatedEventDispatched( |
| const TestEventRouterObserver& observer, |
| const ExtensionId& extension_id) { |
| const std::string kEventName = |
| api::developer_private::OnItemStateChanged::kEventName; |
| const auto& event_map = observer.events(); |
| auto iter = event_map.find(kEventName); |
| if (iter == event_map.end()) |
| return false; |
| |
| const Event& event = *iter->second; |
| CHECK(event.event_args); |
| CHECK_GE(1u, event.event_args->GetList().size()); |
| std::unique_ptr<api::developer_private::EventData> event_data = |
| api::developer_private::EventData::FromValue( |
| event.event_args->GetList()[0]); |
| if (!event_data) |
| return false; |
| |
| if (event_data->item_id != extension_id || |
| event_data->event_type != |
| api::developer_private::EVENT_TYPE_PERMISSIONS_CHANGED) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace |
| |
| class DeveloperPrivateApiUnitTest : public ExtensionServiceTestWithInstall { |
| protected: |
| DeveloperPrivateApiUnitTest() {} |
| ~DeveloperPrivateApiUnitTest() override {} |
| |
| void AddMockExternalProvider( |
| std::unique_ptr<ExternalProviderInterface> provider) { |
| service()->AddProviderForTesting(std::move(provider)); |
| } |
| |
| // A wrapper around extension_function_test_utils::RunFunction that runs with |
| // the associated browser, no flags, and can take stack-allocated arguments. |
| bool RunFunction(const scoped_refptr<UIThreadExtensionFunction>& function, |
| const base::ListValue& args); |
| |
| // Loads an unpacked extension that is backed by a real directory, allowing |
| // it to be reloaded. |
| const Extension* LoadUnpackedExtension(); |
| |
| // Loads an extension with no real directory; this is faster, but means the |
| // extension can't be reloaded. |
| const Extension* LoadSimpleExtension(); |
| |
| // Tests modifying the extension's configuration. |
| void TestExtensionPrefSetting(const base::Callback<bool()>& has_pref, |
| const std::string& key, |
| const std::string& extension_id); |
| |
| testing::AssertionResult TestPackExtensionFunction( |
| const base::ListValue& args, |
| api::developer_private::PackStatus expected_status, |
| int expected_flags); |
| |
| // Execute the updateProfileConfiguration API call with a specified |
| // dev_mode. This is done from the webui when the user checks the |
| // "Developer Mode" checkbox. |
| void UpdateProfileConfigurationDevMode(bool dev_mode); |
| |
| // Execute the getProfileConfiguration API and parse its result into a |
| // ProfileInfo structure for further verification in the calling test. |
| // Will reset the profile_info unique_ptr. |
| // Uses ASSERT_* inside - callers should use ASSERT_NO_FATAL_FAILURE. |
| void GetProfileConfiguration( |
| std::unique_ptr<api::developer_private::ProfileInfo>* profile_info); |
| |
| // Runs the API function to update host access for the given |extension| to |
| // |new_access|. |
| void RunUpdateHostAccess(const Extension& extension, |
| base::StringPiece new_access); |
| |
| virtual bool ProfileIsSupervised() const { return false; } |
| |
| Browser* browser() { return browser_.get(); } |
| |
| private: |
| // ExtensionServiceTestBase: |
| void SetUp() override; |
| void TearDown() override; |
| |
| // The browser (and accompanying window). |
| std::unique_ptr<TestBrowserWindow> browser_window_; |
| std::unique_ptr<Browser> browser_; |
| |
| std::vector<std::unique_ptr<TestExtensionDir>> test_extension_dirs_; |
| policy::MockConfigurationPolicyProvider mock_policy_provider_; |
| |
| DISALLOW_COPY_AND_ASSIGN(DeveloperPrivateApiUnitTest); |
| }; |
| |
| bool DeveloperPrivateApiUnitTest::RunFunction( |
| const scoped_refptr<UIThreadExtensionFunction>& function, |
| const base::ListValue& args) { |
| return extension_function_test_utils::RunFunction( |
| function.get(), args.CreateDeepCopy(), browser(), api_test_utils::NONE); |
| } |
| |
| const Extension* DeveloperPrivateApiUnitTest::LoadUnpackedExtension() { |
| const char kManifest[] = |
| "{" |
| " \"name\": \"foo\"," |
| " \"version\": \"1.0\"," |
| " \"manifest_version\": 2," |
| " \"permissions\": [\"*://*/*\"]" |
| "}"; |
| |
| test_extension_dirs_.push_back(std::make_unique<TestExtensionDir>()); |
| TestExtensionDir* dir = test_extension_dirs_.back().get(); |
| dir->WriteManifest(kManifest); |
| |
| // TODO(devlin): We should extract out methods to load an unpacked extension |
| // synchronously. We do it in ExtensionBrowserTest, but that's not helpful |
| // for unittests. |
| TestExtensionRegistryObserver registry_observer(registry()); |
| scoped_refptr<UnpackedInstaller> installer( |
| UnpackedInstaller::Create(service())); |
| installer->Load(dir->UnpackedPath()); |
| base::FilePath extension_path = |
| base::MakeAbsoluteFilePath(dir->UnpackedPath()); |
| const Extension* extension = nullptr; |
| do { |
| extension = registry_observer.WaitForExtensionLoaded(); |
| } while (extension->path() != extension_path); |
| // The fact that unpacked extensions get file access by default is an |
| // irrelevant detail to these tests. Disable it. |
| ExtensionPrefs::Get(browser_context())->SetAllowFileAccess(extension->id(), |
| false); |
| return extension; |
| } |
| |
| const Extension* DeveloperPrivateApiUnitTest::LoadSimpleExtension() { |
| const char kName[] = "extension name"; |
| const char kVersion[] = "1.0.0.1"; |
| std::string id = crx_file::id_util::GenerateId(kName); |
| DictionaryBuilder manifest; |
| manifest.Set("name", kName) |
| .Set("version", kVersion) |
| .Set("manifest_version", 2) |
| .Set("description", "an extension"); |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder() |
| .SetManifest(manifest.Build()) |
| .SetLocation(Manifest::INTERNAL) |
| .SetID(id) |
| .Build(); |
| service()->AddExtension(extension.get()); |
| return extension.get(); |
| } |
| |
| void DeveloperPrivateApiUnitTest::TestExtensionPrefSetting( |
| const base::Callback<bool()>& has_pref, |
| const std::string& key, |
| const std::string& extension_id) { |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateUpdateExtensionConfigurationFunction()); |
| |
| EXPECT_FALSE(has_pref.Run()) << key; |
| |
| { |
| auto parameters = std::make_unique<base::DictionaryValue>(); |
| parameters->SetString("extensionId", extension_id); |
| parameters->SetBoolean(key, true); |
| |
| base::ListValue args; |
| args.Append(std::move(parameters)); |
| EXPECT_FALSE(RunFunction(function, args)) << key; |
| EXPECT_EQ("This action requires a user gesture.", function->GetError()); |
| |
| function = new api::DeveloperPrivateUpdateExtensionConfigurationFunction(); |
| function->set_source_context_type(Feature::WEBUI_CONTEXT); |
| EXPECT_TRUE(RunFunction(function, args)) << key; |
| EXPECT_TRUE(has_pref.Run()) << key; |
| |
| ExtensionFunction::ScopedUserGestureForTests scoped_user_gesture; |
| function = new api::DeveloperPrivateUpdateExtensionConfigurationFunction(); |
| EXPECT_TRUE(RunFunction(function, args)) << key; |
| EXPECT_TRUE(has_pref.Run()) << key; |
| } |
| |
| { |
| auto parameters = std::make_unique<base::DictionaryValue>(); |
| parameters->SetString("extensionId", extension_id); |
| parameters->SetBoolean(key, false); |
| |
| base::ListValue args; |
| args.Append(std::move(parameters)); |
| |
| ExtensionFunction::ScopedUserGestureForTests scoped_user_gesture; |
| function = new api::DeveloperPrivateUpdateExtensionConfigurationFunction(); |
| EXPECT_TRUE(RunFunction(function, args)) << key; |
| EXPECT_FALSE(has_pref.Run()) << key; |
| } |
| } |
| |
| testing::AssertionResult DeveloperPrivateApiUnitTest::TestPackExtensionFunction( |
| const base::ListValue& args, |
| api::developer_private::PackStatus expected_status, |
| int expected_flags) { |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivatePackDirectoryFunction()); |
| if (!RunFunction(function, args)) |
| return testing::AssertionFailure() << "Could not run function."; |
| |
| // Extract the result. We don't have to test this here, since it's verified as |
| // part of the general extension api system. |
| const base::Value* response_value = nullptr; |
| CHECK(function->GetResultList()->Get(0u, &response_value)); |
| std::unique_ptr<api::developer_private::PackDirectoryResponse> response = |
| api::developer_private::PackDirectoryResponse::FromValue(*response_value); |
| CHECK(response); |
| |
| if (response->status != expected_status) { |
| return testing::AssertionFailure() << "Expected status: " << |
| expected_status << ", found status: " << response->status << |
| ", message: " << response->message; |
| } |
| |
| if (response->override_flags != expected_flags) { |
| return testing::AssertionFailure() << "Expected flags: " << |
| expected_flags << ", found flags: " << response->override_flags; |
| } |
| |
| return testing::AssertionSuccess(); |
| } |
| |
| void DeveloperPrivateApiUnitTest::UpdateProfileConfigurationDevMode( |
| bool dev_mode) { |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateUpdateProfileConfigurationFunction()); |
| std::unique_ptr<base::ListValue> args = |
| ListBuilder() |
| .Append(DictionaryBuilder().Set("inDeveloperMode", dev_mode).Build()) |
| .Build(); |
| EXPECT_TRUE(RunFunction(function, *args)) << function->GetError(); |
| } |
| |
| void DeveloperPrivateApiUnitTest::GetProfileConfiguration( |
| std::unique_ptr<api::developer_private::ProfileInfo>* profile_info) { |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateGetProfileConfigurationFunction()); |
| base::ListValue args; |
| EXPECT_TRUE(RunFunction(function, args)) << function->GetError(); |
| |
| ASSERT_TRUE(function->GetResultList()); |
| ASSERT_EQ(1u, function->GetResultList()->GetSize()); |
| const base::Value* response_value = nullptr; |
| function->GetResultList()->Get(0u, &response_value); |
| *profile_info = |
| api::developer_private::ProfileInfo::FromValue(*response_value); |
| } |
| |
| void DeveloperPrivateApiUnitTest::RunUpdateHostAccess( |
| const Extension& extension, |
| base::StringPiece new_access) { |
| SCOPED_TRACE(new_access); |
| ExtensionFunction::ScopedUserGestureForTests scoped_user_gesture; |
| scoped_refptr<UIThreadExtensionFunction> function = base::MakeRefCounted< |
| api::DeveloperPrivateUpdateExtensionConfigurationFunction>(); |
| std::string args = |
| base::StringPrintf(R"([{"extensionId": "%s", "hostAccess": "%s"}])", |
| extension.id().c_str(), new_access.data()); |
| EXPECT_TRUE(api_test_utils::RunFunction(function.get(), args, profile())) |
| << function->GetError(); |
| } |
| |
| void DeveloperPrivateApiUnitTest::SetUp() { |
| ExtensionServiceTestBase::SetUp(); |
| |
| // By not specifying a pref_file filepath, we get a |
| // sync_preferences::TestingPrefServiceSyncable |
| // - see BuildTestingProfile in extension_service_test_base.cc. |
| ExtensionServiceInitParams init_params = CreateDefaultInitParams(); |
| init_params.pref_file.clear(); |
| init_params.profile_is_supervised = ProfileIsSupervised(); |
| InitializeExtensionService(init_params); |
| |
| browser_window_.reset(new TestBrowserWindow()); |
| Browser::CreateParams params(profile(), true); |
| params.type = Browser::TYPE_TABBED; |
| params.window = browser_window_.get(); |
| browser_.reset(new Browser(params)); |
| |
| // Allow the API to be created. |
| EventRouterFactory::GetInstance()->SetTestingFactory(profile(), |
| &BuildEventRouter); |
| |
| DeveloperPrivateAPI::GetFactoryInstance()->SetTestingFactory( |
| profile(), &BuildAPI); |
| |
| // Loading unpacked extensions through the developerPrivate API requires |
| // developer mode to be enabled. |
| profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, true); |
| } |
| |
| void DeveloperPrivateApiUnitTest::TearDown() { |
| test_extension_dirs_.clear(); |
| browser_.reset(); |
| browser_window_.reset(); |
| ExtensionServiceTestBase::TearDown(); |
| } |
| |
| // Test developerPrivate.updateExtensionConfiguration. |
| TEST_F(DeveloperPrivateApiUnitTest, |
| DeveloperPrivateUpdateExtensionConfiguration) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature( |
| extensions_features::kRuntimeHostPermissions); |
| // Sadly, we need a "real" directory here, because toggling prefs causes |
| // a reload (which needs a path). |
| const Extension* extension = LoadUnpackedExtension(); |
| const std::string& id = extension->id(); |
| |
| ScriptingPermissionsModifier(profile(), base::WrapRefCounted(extension)) |
| .SetWithholdHostPermissions(true); |
| |
| TestExtensionPrefSetting( |
| base::Bind(&HasPrefsPermission, &util::IsIncognitoEnabled, profile(), id), |
| "incognitoAccess", id); |
| TestExtensionPrefSetting( |
| base::Bind(&HasPrefsPermission, &util::AllowFileAccess, profile(), id), |
| "fileAccess", id); |
| } |
| |
| // Test developerPrivate.reload. |
| TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivateReload) { |
| const Extension* extension = LoadUnpackedExtension(); |
| std::string extension_id = extension->id(); |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateReloadFunction()); |
| base::ListValue reload_args; |
| reload_args.AppendString(extension_id); |
| |
| TestExtensionRegistryObserver registry_observer(registry()); |
| EXPECT_TRUE(RunFunction(function, reload_args)); |
| const Extension* unloaded_extension = |
| registry_observer.WaitForExtensionUnloaded(); |
| EXPECT_EQ(extension, unloaded_extension); |
| const Extension* reloaded_extension = |
| registry_observer.WaitForExtensionLoaded(); |
| EXPECT_EQ(extension_id, reloaded_extension->id()); |
| } |
| |
| // Test developerPrivate.packDirectory. |
| TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivatePackFunction) { |
| // Use a temp dir isolating the extension dir and its generated files. |
| base::ScopedTempDir temp_dir; |
| ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); |
| base::FilePath root_path = data_dir().AppendASCII("simple_with_popup"); |
| ASSERT_TRUE(base::CopyDirectory(root_path, temp_dir.GetPath(), true)); |
| |
| base::FilePath temp_root_path = |
| temp_dir.GetPath().Append(root_path.BaseName()); |
| base::FilePath crx_path = |
| temp_dir.GetPath().AppendASCII("simple_with_popup.crx"); |
| base::FilePath pem_path = |
| temp_dir.GetPath().AppendASCII("simple_with_popup.pem"); |
| |
| EXPECT_FALSE(base::PathExists(crx_path)) |
| << "crx should not exist before the test is run!"; |
| EXPECT_FALSE(base::PathExists(pem_path)) |
| << "pem should not exist before the test is run!"; |
| |
| // First, test a directory that should pack properly. |
| base::ListValue pack_args; |
| pack_args.AppendString(temp_root_path.AsUTF8Unsafe()); |
| EXPECT_TRUE(TestPackExtensionFunction( |
| pack_args, api::developer_private::PACK_STATUS_SUCCESS, 0)); |
| |
| // Should have created crx file and pem file. |
| EXPECT_TRUE(base::PathExists(crx_path)); |
| EXPECT_TRUE(base::PathExists(pem_path)); |
| |
| // Deliberately don't cleanup the files, and append the pem path. |
| pack_args.AppendString(pem_path.AsUTF8Unsafe()); |
| |
| // Try to pack again - we should get a warning abot overwriting the crx. |
| EXPECT_TRUE(TestPackExtensionFunction( |
| pack_args, |
| api::developer_private::PACK_STATUS_WARNING, |
| ExtensionCreator::kOverwriteCRX)); |
| |
| // Try to pack again, with the overwrite flag; this should succeed. |
| pack_args.AppendInteger(ExtensionCreator::kOverwriteCRX); |
| EXPECT_TRUE(TestPackExtensionFunction( |
| pack_args, api::developer_private::PACK_STATUS_SUCCESS, 0)); |
| |
| // Try to pack a final time when omitting (an existing) pem file. We should |
| // get an error. |
| base::DeleteFile(crx_path, false); |
| EXPECT_TRUE(pack_args.Remove(1u, nullptr)); // Remove the pem key argument. |
| EXPECT_TRUE(pack_args.Remove(1u, nullptr)); // Remove the flags argument. |
| EXPECT_TRUE(TestPackExtensionFunction( |
| pack_args, api::developer_private::PACK_STATUS_ERROR, 0)); |
| } |
| |
| // Test developerPrivate.choosePath. |
| TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivateChoosePath) { |
| std::unique_ptr<content::WebContents> web_contents( |
| content::WebContentsTester::CreateTestWebContents(profile(), nullptr)); |
| |
| base::FilePath expected_dir_path = |
| data_dir().AppendASCII("simple_with_popup"); |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&expected_dir_path); |
| |
| // Try selecting a directory. |
| base::ListValue choose_args; |
| choose_args.AppendString("FOLDER"); |
| choose_args.AppendString("LOAD"); |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateChoosePathFunction()); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| EXPECT_TRUE(RunFunction(function, choose_args)) << function->GetError(); |
| std::string path; |
| EXPECT_TRUE(function->GetResultList() && |
| function->GetResultList()->GetString(0, &path)); |
| EXPECT_EQ(path, expected_dir_path.AsUTF8Unsafe()); |
| |
| // Try selecting a pem file. |
| base::FilePath expected_file_path = |
| data_dir().AppendASCII("simple_with_popup.pem"); |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&expected_file_path); |
| choose_args.Clear(); |
| choose_args.AppendString("FILE"); |
| choose_args.AppendString("PEM"); |
| function = new api::DeveloperPrivateChoosePathFunction(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| EXPECT_TRUE(RunFunction(function, choose_args)) << function->GetError(); |
| EXPECT_TRUE(function->GetResultList() && |
| function->GetResultList()->GetString(0, &path)); |
| EXPECT_EQ(path, expected_file_path.AsUTF8Unsafe()); |
| |
| // Try canceling the file dialog. |
| api::EntryPicker::SkipPickerAndAlwaysCancelForTest(); |
| function = new api::DeveloperPrivateChoosePathFunction(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| EXPECT_FALSE(RunFunction(function, choose_args)); |
| EXPECT_EQ(std::string("File selection was canceled."), function->GetError()); |
| } |
| |
| // Test developerPrivate.loadUnpacked. |
| TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivateLoadUnpacked) { |
| std::unique_ptr<content::WebContents> web_contents( |
| content::WebContentsTester::CreateTestWebContents(profile(), nullptr)); |
| |
| base::FilePath path = data_dir().AppendASCII("simple_with_popup"); |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&path); |
| |
| // Try loading a good extension (it should succeed, and the extension should |
| // be added). |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateLoadUnpackedFunction()); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| ExtensionIdSet current_ids = registry()->enabled_extensions().GetIDs(); |
| EXPECT_TRUE(RunFunction(function, base::ListValue())) << function->GetError(); |
| // We should have added one new extension. |
| ExtensionIdSet id_difference = base::STLSetDifference<ExtensionIdSet>( |
| registry()->enabled_extensions().GetIDs(), current_ids); |
| ASSERT_EQ(1u, id_difference.size()); |
| // The new extension should have the same path. |
| EXPECT_EQ( |
| path, |
| registry()->enabled_extensions().GetByID(*id_difference.begin())->path()); |
| |
| path = data_dir().AppendASCII("empty_manifest"); |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&path); |
| |
| // Try loading a bad extension (it should fail, and we should get an error). |
| function = new api::DeveloperPrivateLoadUnpackedFunction(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| base::ListValue unpacked_args; |
| std::unique_ptr<base::DictionaryValue> options(new base::DictionaryValue()); |
| options->SetBoolean("failQuietly", true); |
| unpacked_args.Append(std::move(options)); |
| current_ids = registry()->enabled_extensions().GetIDs(); |
| EXPECT_FALSE(RunFunction(function, unpacked_args)); |
| EXPECT_EQ(manifest_errors::kManifestUnreadable, function->GetError()); |
| // We should have no new extensions installed. |
| EXPECT_EQ(0u, base::STLSetDifference<ExtensionIdSet>( |
| registry()->enabled_extensions().GetIDs(), |
| current_ids).size()); |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivateLoadUnpackedLoadError) { |
| std::unique_ptr<content::WebContents> web_contents( |
| content::WebContentsTester::CreateTestWebContents(profile(), nullptr)); |
| |
| { |
| // Load an extension with a clear manifest error ('version' is invalid). |
| TestExtensionDir dir; |
| dir.WriteManifest( |
| R"({ |
| "name": "foo", |
| "description": "bar", |
| "version": 1, |
| "manifest_version": 2 |
| })"); |
| base::FilePath path = dir.UnpackedPath(); |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&path); |
| |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateLoadUnpackedFunction()); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| std::unique_ptr<base::Value> result = |
| api_test_utils::RunFunctionAndReturnSingleResult( |
| function.get(), |
| "[{\"failQuietly\": true, \"populateError\": true}]", profile()); |
| // The loadError result should be populated. |
| ASSERT_TRUE(result); |
| std::unique_ptr<api::developer_private::LoadError> error = |
| api::developer_private::LoadError::FromValue(*result); |
| ASSERT_TRUE(error); |
| ASSERT_TRUE(error->source); |
| // The source should have *something* (rely on file highlighter tests for |
| // the correct population). |
| EXPECT_FALSE(error->source->before_highlight.empty()); |
| // The error should be appropriate (mentioning that version was invalid). |
| EXPECT_TRUE(error->error.find("version") != std::string::npos) |
| << error->error; |
| } |
| |
| { |
| // Load an extension with no manifest. |
| TestExtensionDir dir; |
| base::FilePath path = dir.UnpackedPath(); |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&path); |
| |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateLoadUnpackedFunction()); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| std::unique_ptr<base::Value> result = |
| api_test_utils::RunFunctionAndReturnSingleResult( |
| function.get(), |
| "[{\"failQuietly\": true, \"populateError\": true}]", profile()); |
| // The load error should be populated. |
| ASSERT_TRUE(result); |
| std::unique_ptr<api::developer_private::LoadError> error = |
| api::developer_private::LoadError::FromValue(*result); |
| ASSERT_TRUE(error); |
| // The file source should be empty. |
| ASSERT_TRUE(error->source); |
| EXPECT_TRUE(error->source->before_highlight.empty()); |
| EXPECT_TRUE(error->source->highlight.empty()); |
| EXPECT_TRUE(error->source->after_highlight.empty()); |
| } |
| |
| { |
| // Load a valid extension. |
| TestExtensionDir dir; |
| dir.WriteManifest( |
| R"({ |
| "name": "foo", |
| "description": "bar", |
| "version": "1.0", |
| "manifest_version": 2 |
| })"); |
| base::FilePath path = dir.UnpackedPath(); |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&path); |
| |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateLoadUnpackedFunction()); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| std::unique_ptr<base::Value> result = |
| api_test_utils::RunFunctionAndReturnSingleResult( |
| function.get(), |
| "[{\"failQuietly\": true, \"populateError\": true}]", profile()); |
| // There should be no load error. |
| ASSERT_FALSE(result); |
| } |
| } |
| |
| // Test that the retryGuid supplied by loadUnpacked works correctly. |
| TEST_F(DeveloperPrivateApiUnitTest, LoadUnpackedRetryId) { |
| std::unique_ptr<content::WebContents> web_contents( |
| content::WebContentsTester::CreateTestWebContents(profile(), nullptr)); |
| |
| // Load an extension with a clear manifest error ('version' is invalid). |
| TestExtensionDir dir; |
| dir.WriteManifest( |
| R"({ |
| "name": "foo", |
| "description": "bar", |
| "version": 1, |
| "manifest_version": 2 |
| })"); |
| base::FilePath path = dir.UnpackedPath(); |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&path); |
| |
| DeveloperPrivateAPI::UnpackedRetryId retry_guid; |
| { |
| // Trying to load the extension should result in a load error with the |
| // retry id populated. |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateLoadUnpackedFunction()); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| std::unique_ptr<base::Value> result = |
| api_test_utils::RunFunctionAndReturnSingleResult( |
| function.get(), |
| "[{\"failQuietly\": true, \"populateError\": true}]", profile()); |
| ASSERT_TRUE(result); |
| std::unique_ptr<api::developer_private::LoadError> error = |
| api::developer_private::LoadError::FromValue(*result); |
| ASSERT_TRUE(error); |
| EXPECT_FALSE(error->retry_guid.empty()); |
| retry_guid = error->retry_guid; |
| } |
| |
| { |
| // Trying to reload the same extension, again to fail, should result in the |
| // same retry id. This is somewhat an implementation detail, but is |
| // important to ensure we don't allocate crazy numbers of ids if the user |
| // just retries continously. |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateLoadUnpackedFunction()); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| std::unique_ptr<base::Value> result = |
| api_test_utils::RunFunctionAndReturnSingleResult( |
| function.get(), |
| "[{\"failQuietly\": true, \"populateError\": true}]", profile()); |
| ASSERT_TRUE(result); |
| std::unique_ptr<api::developer_private::LoadError> error = |
| api::developer_private::LoadError::FromValue(*result); |
| ASSERT_TRUE(error); |
| EXPECT_EQ(retry_guid, error->retry_guid); |
| } |
| |
| { |
| // Try loading a different directory. The retry id should be different; this |
| // also tests loading a second extension with one retry currently |
| // "in-flight" (i.e., unresolved). |
| TestExtensionDir second_dir; |
| second_dir.WriteManifest( |
| R"({ |
| "name": "foo", |
| "description": "bar", |
| "version": 1, |
| "manifest_version": 2 |
| })"); |
| base::FilePath second_path = second_dir.UnpackedPath(); |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&second_path); |
| |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateLoadUnpackedFunction()); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| std::unique_ptr<base::Value> result = |
| api_test_utils::RunFunctionAndReturnSingleResult( |
| function.get(), |
| "[{\"failQuietly\": true, \"populateError\": true}]", profile()); |
| // The loadError result should be populated. |
| ASSERT_TRUE(result); |
| std::unique_ptr<api::developer_private::LoadError> error = |
| api::developer_private::LoadError::FromValue(*result); |
| ASSERT_TRUE(error); |
| EXPECT_NE(retry_guid, error->retry_guid); |
| } |
| |
| // Correct the manifest to make the extension valid. |
| dir.WriteManifest( |
| R"({ |
| "name": "foo", |
| "description": "bar", |
| "version": "1.0", |
| "manifest_version": 2 |
| })"); |
| |
| // Set the picker to choose an invalid path (the picker should be skipped if |
| // we supply a retry id). |
| base::FilePath empty_path; |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&empty_path); |
| |
| { |
| // Try reloading the extension by supplying the retry id. It should succeed. |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateLoadUnpackedFunction()); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| TestExtensionRegistryObserver observer(registry()); |
| api_test_utils::RunFunction(function.get(), |
| base::StringPrintf("[{\"failQuietly\": true," |
| "\"populateError\": true," |
| "\"retryGuid\": \"%s\"}]", |
| retry_guid.c_str()), |
| profile()); |
| const Extension* extension = observer.WaitForExtensionLoaded(); |
| ASSERT_TRUE(extension); |
| EXPECT_EQ(extension->path(), path); |
| } |
| |
| { |
| // Try supplying an invalid retry id. It should fail with an error. |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateLoadUnpackedFunction()); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| std::string error = api_test_utils::RunFunctionAndReturnError( |
| function.get(), |
| "[{\"failQuietly\": true," |
| "\"populateError\": true," |
| "\"retryGuid\": \"invalid id\"}]", |
| profile()); |
| EXPECT_EQ("Invalid retry id", error); |
| } |
| } |
| |
| // Tests calling "reload" on an unpacked extension with a manifest error, |
| // resulting in the reload failing. The reload call should then respond with |
| // the load error, which includes a retry GUID to be passed to loadUnpacked(). |
| TEST_F(DeveloperPrivateApiUnitTest, ReloadBadExtensionToLoadUnpackedRetry) { |
| std::unique_ptr<content::WebContents> web_contents( |
| content::WebContentsTester::CreateTestWebContents(profile(), nullptr)); |
| |
| // A broken manifest (version's value should be a string). |
| constexpr const char kBadManifest[] = |
| R"({ |
| "name": "foo", |
| "description": "bar", |
| "version": 1, |
| "manifest_version": 2 |
| })"; |
| constexpr const char kGoodManifest[] = |
| R"({ |
| "name": "foo", |
| "description": "bar", |
| "version": "1", |
| "manifest_version": 2 |
| })"; |
| |
| // Create a good unpacked extension. |
| TestExtensionDir dir; |
| dir.WriteManifest(kGoodManifest); |
| base::FilePath path = dir.UnpackedPath(); |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&path); |
| |
| scoped_refptr<const Extension> extension; |
| { |
| ChromeTestExtensionLoader loader(profile()); |
| loader.set_pack_extension(false); |
| extension = loader.LoadExtension(path); |
| } |
| ASSERT_TRUE(extension); |
| const ExtensionId id = extension->id(); |
| |
| std::string reload_args = base::StringPrintf( |
| R"(["%s", {"failQuietly": true, "populateErrorForUnpacked":true}])", |
| id.c_str()); |
| |
| { |
| // Try reloading while the manifest is still good. This should succeed, and |
| // the extension should still be enabled. Additionally, the function should |
| // wait for the reload to complete, so we should see an unload and reload. |
| class UnloadedRegistryObserver : public ExtensionRegistryObserver { |
| public: |
| UnloadedRegistryObserver(const base::FilePath& expected_path, |
| ExtensionRegistry* registry) |
| : expected_path_(expected_path), observer_(this) { |
| observer_.Add(registry); |
| } |
| |
| void OnExtensionUnloaded(content::BrowserContext* browser_context, |
| const Extension* extension, |
| UnloadedExtensionReason reason) override { |
| ASSERT_FALSE(saw_unload_); |
| saw_unload_ = extension->path() == expected_path_; |
| } |
| |
| bool saw_unload() const { return saw_unload_; } |
| |
| private: |
| bool saw_unload_ = false; |
| base::FilePath expected_path_; |
| ScopedObserver<ExtensionRegistry, ExtensionRegistryObserver> observer_; |
| |
| DISALLOW_COPY_AND_ASSIGN(UnloadedRegistryObserver); |
| }; |
| |
| UnloadedRegistryObserver unload_observer(path, registry()); |
| auto function = |
| base::MakeRefCounted<api::DeveloperPrivateReloadFunction>(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| api_test_utils::RunFunction(function.get(), reload_args, profile()); |
| // Note: no need to validate a saw_load()-type method because the presence |
| // in enabled_extensions() indicates the extension was loaded. |
| EXPECT_TRUE(unload_observer.saw_unload()); |
| EXPECT_TRUE(registry()->enabled_extensions().Contains(id)); |
| } |
| |
| dir.WriteManifest(kBadManifest); |
| |
| DeveloperPrivateAPI::UnpackedRetryId retry_guid; |
| { |
| // Trying to load the extension should result in a load error with the |
| // retry GUID populated. |
| auto function = base::MakeRefCounted<api::DeveloperPrivateReloadFunction>(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| std::unique_ptr<base::Value> result = |
| api_test_utils::RunFunctionAndReturnSingleResult( |
| function.get(), reload_args, profile()); |
| ASSERT_TRUE(result); |
| std::unique_ptr<api::developer_private::LoadError> error = |
| api::developer_private::LoadError::FromValue(*result); |
| ASSERT_TRUE(error); |
| EXPECT_FALSE(error->retry_guid.empty()); |
| retry_guid = error->retry_guid; |
| EXPECT_TRUE(registry()->disabled_extensions().Contains(id)); |
| } |
| |
| dir.WriteManifest(kGoodManifest); |
| { |
| // Try reloading the extension by supplying the retry id. It should succeed, |
| // and the extension should be enabled again. |
| auto function = |
| base::MakeRefCounted<api::DeveloperPrivateLoadUnpackedFunction>(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| TestExtensionRegistryObserver observer(registry()); |
| std::string args = |
| base::StringPrintf(R"([{"failQuietly": true, "populateError": true, |
| "retryGuid": "%s"}])", |
| retry_guid.c_str()); |
| api_test_utils::RunFunction(function.get(), args, profile()); |
| const Extension* extension = observer.WaitForExtensionLoaded(); |
| ASSERT_TRUE(extension); |
| EXPECT_EQ(extension->path(), path); |
| EXPECT_TRUE(registry()->enabled_extensions().Contains(id)); |
| } |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, |
| DeveloperPrivateNotifyDragInstallInProgress) { |
| std::unique_ptr<content::WebContents> web_contents( |
| content::WebContentsTester::CreateTestWebContents(profile(), nullptr)); |
| |
| TestExtensionDir dir; |
| dir.WriteManifest( |
| R"({ |
| "name": "foo", |
| "description": "bar", |
| "version": "1", |
| "manifest_version": 2 |
| })"); |
| base::FilePath path = dir.UnpackedPath(); |
| api::DeveloperPrivateNotifyDragInstallInProgressFunction:: |
| SetDropPathForTesting(&path); |
| |
| { |
| auto function = base::MakeRefCounted< |
| api::DeveloperPrivateNotifyDragInstallInProgressFunction>(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| api_test_utils::RunFunction(function.get(), "[]", profile()); |
| } |
| |
| // Set the picker to choose an invalid path (the picker should be skipped if |
| // we supply a retry id). |
| base::FilePath empty_path; |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&empty_path); |
| |
| constexpr char kLoadUnpackedArgs[] = |
| R"([{"failQuietly": true, |
| "populateError": true, |
| "useDraggedPath": true}])"; |
| |
| { |
| // Try reloading the extension by supplying the retry id. It should succeed. |
| auto function = |
| base::MakeRefCounted<api::DeveloperPrivateLoadUnpackedFunction>(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| TestExtensionRegistryObserver observer(registry()); |
| api_test_utils::RunFunction(function.get(), kLoadUnpackedArgs, profile()); |
| const Extension* extension = observer.WaitForExtensionLoaded(); |
| ASSERT_TRUE(extension); |
| EXPECT_EQ(extension->path(), path); |
| } |
| |
| // Next, ensure that nothing catastrophic happens if the file that was dropped |
| // was not a directory. In theory, this shouldn't happen (the JS validates the |
| // file), but it could in the case of a compromised renderer, JS bug, etc. |
| base::FilePath invalid_path = path.AppendASCII("manifest.json"); |
| api::DeveloperPrivateNotifyDragInstallInProgressFunction:: |
| SetDropPathForTesting(&invalid_path); |
| { |
| auto function = base::MakeRefCounted< |
| api::DeveloperPrivateNotifyDragInstallInProgressFunction>(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| std::unique_ptr<base::Value> result = |
| api_test_utils::RunFunctionAndReturnSingleResult(function.get(), "[]", |
| profile()); |
| } |
| |
| { |
| // Trying to load the bad extension (the path points to the manifest, not |
| // the directory) should result in a load error. |
| auto function = |
| base::MakeRefCounted<api::DeveloperPrivateLoadUnpackedFunction>(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| TestExtensionRegistryObserver observer(registry()); |
| std::unique_ptr<base::Value> result = |
| api_test_utils::RunFunctionAndReturnSingleResult( |
| function.get(), kLoadUnpackedArgs, profile()); |
| ASSERT_TRUE(result); |
| EXPECT_TRUE(api::developer_private::LoadError::FromValue(*result)); |
| } |
| |
| // Cleanup. |
| api::DeveloperPrivateNotifyDragInstallInProgressFunction:: |
| SetDropPathForTesting(nullptr); |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(nullptr); |
| } |
| |
| // Test developerPrivate.requestFileSource. |
| TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivateRequestFileSource) { |
| // Testing of this function seems light, but that's because it basically just |
| // forwards to reading a file to a string, and highlighting it - both of which |
| // are already tested separately. |
| const Extension* extension = LoadUnpackedExtension(); |
| const char kErrorMessage[] = "Something went wrong"; |
| api::developer_private::RequestFileSourceProperties properties; |
| properties.extension_id = extension->id(); |
| properties.path_suffix = "manifest.json"; |
| properties.message = kErrorMessage; |
| properties.manifest_key.reset(new std::string("name")); |
| |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateRequestFileSourceFunction()); |
| base::ListValue file_source_args; |
| file_source_args.Append(properties.ToValue()); |
| EXPECT_TRUE(RunFunction(function, file_source_args)) << function->GetError(); |
| |
| const base::Value* response_value = nullptr; |
| ASSERT_TRUE(function->GetResultList()->Get(0u, &response_value)); |
| std::unique_ptr<api::developer_private::RequestFileSourceResponse> response = |
| api::developer_private::RequestFileSourceResponse::FromValue( |
| *response_value); |
| EXPECT_FALSE(response->before_highlight.empty()); |
| EXPECT_EQ("\"name\": \"foo\"", response->highlight); |
| EXPECT_FALSE(response->after_highlight.empty()); |
| EXPECT_EQ("foo: manifest.json", response->title); |
| EXPECT_EQ(kErrorMessage, response->message); |
| } |
| |
| // Test developerPrivate.getExtensionsInfo. |
| TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivateGetExtensionsInfo) { |
| LoadSimpleExtension(); |
| |
| // The test here isn't so much about the generated value (that's tested in |
| // ExtensionInfoGenerator's unittest), but rather just to make sure we can |
| // serialize/deserialize the result - which implicity tests that everything |
| // has a sane value. |
| scoped_refptr<UIThreadExtensionFunction> function( |
| new api::DeveloperPrivateGetExtensionsInfoFunction()); |
| EXPECT_TRUE(RunFunction(function, base::ListValue())) << function->GetError(); |
| const base::ListValue* results = function->GetResultList(); |
| ASSERT_EQ(1u, results->GetSize()); |
| const base::ListValue* list = nullptr; |
| ASSERT_TRUE(results->GetList(0u, &list)); |
| ASSERT_EQ(1u, list->GetSize()); |
| const base::Value* value = nullptr; |
| ASSERT_TRUE(list->Get(0u, &value)); |
| std::unique_ptr<api::developer_private::ExtensionInfo> info = |
| api::developer_private::ExtensionInfo::FromValue(*value); |
| ASSERT_TRUE(info); |
| |
| // As a sanity check, also run the GetItemsInfo and make sure it returns a |
| // sane value. |
| function = new api::DeveloperPrivateGetItemsInfoFunction(); |
| base::ListValue args; |
| args.AppendBoolean(false); |
| args.AppendBoolean(false); |
| EXPECT_TRUE(RunFunction(function, args)) << function->GetError(); |
| results = function->GetResultList(); |
| ASSERT_EQ(1u, results->GetSize()); |
| ASSERT_TRUE(results->GetList(0u, &list)); |
| ASSERT_EQ(1u, list->GetSize()); |
| ASSERT_TRUE(list->Get(0u, &value)); |
| std::unique_ptr<api::developer_private::ItemInfo> item_info = |
| api::developer_private::ItemInfo::FromValue(*value); |
| ASSERT_TRUE(item_info); |
| } |
| |
| // Test developerPrivate.deleteExtensionErrors. |
| TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivateDeleteExtensionErrors) { |
| FeatureSwitch::ScopedOverride error_console_override( |
| FeatureSwitch::error_console(), true); |
| profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, true); |
| const Extension* extension = LoadSimpleExtension(); |
| |
| // Report some errors. |
| ErrorConsole* error_console = ErrorConsole::Get(profile()); |
| error_console->SetReportingAllForExtension(extension->id(), true); |
| error_console->ReportError( |
| error_test_util::CreateNewRuntimeError(extension->id(), "foo")); |
| error_console->ReportError( |
| error_test_util::CreateNewRuntimeError(extension->id(), "bar")); |
| error_console->ReportError( |
| error_test_util::CreateNewManifestError(extension->id(), "baz")); |
| EXPECT_EQ(3u, error_console->GetErrorsForExtension(extension->id()).size()); |
| |
| // Start by removing all errors for the extension of a given type (manifest). |
| std::string type_string = api::developer_private::ToString( |
| api::developer_private::ERROR_TYPE_MANIFEST); |
| std::unique_ptr<base::ListValue> args = |
| ListBuilder() |
| .Append(DictionaryBuilder() |
| .Set("extensionId", extension->id()) |
| .Set("type", type_string) |
| .Build()) |
| .Build(); |
| scoped_refptr<UIThreadExtensionFunction> function = |
| new api::DeveloperPrivateDeleteExtensionErrorsFunction(); |
| EXPECT_TRUE(RunFunction(function, *args)) << function->GetError(); |
| // Two errors should remain. |
| const ErrorList& error_list = |
| error_console->GetErrorsForExtension(extension->id()); |
| ASSERT_EQ(2u, error_list.size()); |
| |
| // Next remove errors by id. |
| int error_id = error_list[0]->id(); |
| args = |
| ListBuilder() |
| .Append(DictionaryBuilder() |
| .Set("extensionId", extension->id()) |
| .Set("errorIds", ListBuilder().Append(error_id).Build()) |
| .Build()) |
| .Build(); |
| function = new api::DeveloperPrivateDeleteExtensionErrorsFunction(); |
| EXPECT_TRUE(RunFunction(function, *args)) << function->GetError(); |
| // And then there was one. |
| EXPECT_EQ(1u, error_console->GetErrorsForExtension(extension->id()).size()); |
| |
| // Finally remove all errors for the extension. |
| args = |
| ListBuilder() |
| .Append( |
| DictionaryBuilder().Set("extensionId", extension->id()).Build()) |
| .Build(); |
| function = new api::DeveloperPrivateDeleteExtensionErrorsFunction(); |
| EXPECT_TRUE(RunFunction(function, *args)) << function->GetError(); |
| // No more errors! |
| EXPECT_TRUE(error_console->GetErrorsForExtension(extension->id()).empty()); |
| } |
| |
| // Tests that developerPrivate.repair does not succeed for a non-corrupted |
| // extension. |
| TEST_F(DeveloperPrivateApiUnitTest, RepairNotBrokenExtension) { |
| base::FilePath extension_path = data_dir().AppendASCII("good.crx"); |
| const Extension* extension = InstallCRX(extension_path, INSTALL_NEW); |
| |
| // Attempt to repair the good extension, expect failure. |
| std::unique_ptr<base::ListValue> args = |
| ListBuilder().Append(extension->id()).Build(); |
| scoped_refptr<UIThreadExtensionFunction> function = |
| new api::DeveloperPrivateRepairExtensionFunction(); |
| EXPECT_FALSE(RunFunction(function, *args)); |
| EXPECT_EQ("Cannot repair a healthy extension.", function->GetError()); |
| } |
| |
| // Tests that developerPrivate.private cannot repair a policy-installed |
| // extension. |
| // Regression test for https://crbug.com/577959. |
| TEST_F(DeveloperPrivateApiUnitTest, RepairPolicyExtension) { |
| std::string extension_id(kGoodCrx); |
| |
| // Set up a mock provider with a policy extension. |
| std::unique_ptr<MockExternalProvider> mock_provider = |
| std::make_unique<MockExternalProvider>( |
| service(), Manifest::EXTERNAL_POLICY_DOWNLOAD); |
| MockExternalProvider* mock_provider_ptr = mock_provider.get(); |
| AddMockExternalProvider(std::move(mock_provider)); |
| mock_provider_ptr->UpdateOrAddExtension(extension_id, "1.0.0.0", |
| data_dir().AppendASCII("good.crx")); |
| // Reloading extensions should find our externally registered extension |
| // and install it. |
| content::WindowedNotificationObserver observer( |
| extensions::NOTIFICATION_CRX_INSTALLER_DONE, |
| content::NotificationService::AllSources()); |
| service()->CheckForExternalUpdates(); |
| observer.Wait(); |
| |
| // Attempt to repair the good extension, expect failure. |
| std::unique_ptr<base::ListValue> args = |
| ListBuilder().Append(extension_id).Build(); |
| scoped_refptr<UIThreadExtensionFunction> function = |
| new api::DeveloperPrivateRepairExtensionFunction(); |
| EXPECT_FALSE(RunFunction(function, *args)); |
| EXPECT_EQ("Cannot repair a healthy extension.", function->GetError()); |
| |
| // Corrupt the extension , still expect repair failure because this is a |
| // policy extension. |
| service()->DisableExtension(extension_id, disable_reason::DISABLE_CORRUPTED); |
| args = ListBuilder().Append(extension_id).Build(); |
| function = new api::DeveloperPrivateRepairExtensionFunction(); |
| EXPECT_FALSE(RunFunction(function, *args)); |
| EXPECT_EQ("Cannot repair a policy-installed extension.", |
| function->GetError()); |
| } |
| |
| // Test developerPrivate.updateProfileConfiguration: Try to turn on devMode |
| // when DeveloperToolsAvailability policy disallows developer tools. |
| TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivateDevModeDisabledPolicy) { |
| testing_pref_service()->SetManagedPref(prefs::kExtensionsUIDeveloperMode, |
| std::make_unique<base::Value>(false)); |
| |
| UpdateProfileConfigurationDevMode(true); |
| |
| EXPECT_FALSE( |
| profile()->GetPrefs()->GetBoolean(prefs::kExtensionsUIDeveloperMode)); |
| |
| std::unique_ptr<api::developer_private::ProfileInfo> profile_info; |
| ASSERT_NO_FATAL_FAILURE(GetProfileConfiguration(&profile_info)); |
| EXPECT_FALSE(profile_info->in_developer_mode); |
| EXPECT_TRUE(profile_info->is_developer_mode_controlled_by_policy); |
| } |
| |
| // Test developerPrivate.updateProfileConfiguration: Try to turn on devMode |
| // (without DeveloperToolsAvailability policy). |
| TEST_F(DeveloperPrivateApiUnitTest, DeveloperPrivateDevMode) { |
| UpdateProfileConfigurationDevMode(false); |
| EXPECT_FALSE( |
| profile()->GetPrefs()->GetBoolean(prefs::kExtensionsUIDeveloperMode)); |
| { |
| std::unique_ptr<api::developer_private::ProfileInfo> profile_info; |
| ASSERT_NO_FATAL_FAILURE(GetProfileConfiguration(&profile_info)); |
| EXPECT_FALSE(profile_info->in_developer_mode); |
| EXPECT_FALSE(profile_info->is_developer_mode_controlled_by_policy); |
| } |
| |
| UpdateProfileConfigurationDevMode(true); |
| EXPECT_TRUE( |
| profile()->GetPrefs()->GetBoolean(prefs::kExtensionsUIDeveloperMode)); |
| { |
| std::unique_ptr<api::developer_private::ProfileInfo> profile_info; |
| ASSERT_NO_FATAL_FAILURE(GetProfileConfiguration(&profile_info)); |
| EXPECT_TRUE(profile_info->in_developer_mode); |
| EXPECT_FALSE(profile_info->is_developer_mode_controlled_by_policy); |
| } |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, LoadUnpackedFailsWithoutDevMode) { |
| std::unique_ptr<content::WebContents> web_contents( |
| content::WebContentsTester::CreateTestWebContents(profile(), nullptr)); |
| |
| base::FilePath path = data_dir().AppendASCII("simple_with_popup"); |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&path); |
| |
| PrefService* prefs = profile()->GetPrefs(); |
| prefs->SetBoolean(prefs::kExtensionsUIDeveloperMode, false); |
| scoped_refptr<UIThreadExtensionFunction> function = |
| base::MakeRefCounted<api::DeveloperPrivateLoadUnpackedFunction>(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| std::string error = extension_function_test_utils::RunFunctionAndReturnError( |
| function.get(), "[]", browser()); |
| EXPECT_THAT(error, testing::HasSubstr("developer mode")); |
| prefs->SetBoolean(prefs::kExtensionsUIDeveloperMode, true); |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, LoadUnpackedFailsWithBlacklistingPolicy) { |
| std::unique_ptr<content::WebContents> web_contents( |
| content::WebContentsTester::CreateTestWebContents(profile(), nullptr)); |
| |
| base::FilePath path = data_dir().AppendASCII("simple_with_popup"); |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&path); |
| |
| { |
| ExtensionManagementPrefUpdater<sync_preferences::TestingPrefServiceSyncable> |
| pref_updater(testing_profile()->GetTestingPrefService()); |
| pref_updater.SetBlacklistedByDefault(true); |
| } |
| EXPECT_TRUE( |
| ExtensionManagementFactory::GetForBrowserContext(browser_context()) |
| ->BlacklistedByDefault()); |
| |
| scoped_refptr<UIThreadExtensionFunction> function = |
| base::MakeRefCounted<api::DeveloperPrivateLoadUnpackedFunction>(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| std::string error = extension_function_test_utils::RunFunctionAndReturnError( |
| function.get(), "[]", browser()); |
| EXPECT_THAT(error, testing::HasSubstr("policy")); |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, InstallDroppedFileNoDraggedPath) { |
| extensions::ExtensionInstallUI::set_disable_ui_for_tests(); |
| ScopedTestDialogAutoConfirm auto_confirm(ScopedTestDialogAutoConfirm::ACCEPT); |
| |
| std::unique_ptr<content::WebContents> web_contents( |
| content::WebContentsTester::CreateTestWebContents(profile(), nullptr)); |
| |
| scoped_refptr<UIThreadExtensionFunction> function = |
| base::MakeRefCounted<api::DeveloperPrivateInstallDroppedFileFunction>(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| |
| TestExtensionRegistryObserver observer(registry()); |
| EXPECT_EQ("No dragged path", api_test_utils::RunFunctionAndReturnError( |
| function.get(), "[]", profile())); |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, InstallDroppedFileCrx) { |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest( |
| R"({ |
| "name": "foo", |
| "version": "1.0", |
| "manifest_version": 2 |
| })"); |
| base::FilePath crx_path = test_dir.Pack(); |
| extensions::ExtensionInstallUI::set_disable_ui_for_tests(); |
| ScopedTestDialogAutoConfirm auto_confirm(ScopedTestDialogAutoConfirm::ACCEPT); |
| |
| std::unique_ptr<content::WebContents> web_contents( |
| content::WebContentsTester::CreateTestWebContents(profile(), nullptr)); |
| DeveloperPrivateAPI::Get(profile())->SetDraggedPath(web_contents.get(), |
| crx_path); |
| |
| scoped_refptr<UIThreadExtensionFunction> function = |
| base::MakeRefCounted<api::DeveloperPrivateInstallDroppedFileFunction>(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| |
| TestExtensionRegistryObserver observer(registry()); |
| ASSERT_TRUE(api_test_utils::RunFunction(function.get(), "[]", profile())) |
| << function->GetError(); |
| const Extension* extension = observer.WaitForExtensionInstalled(); |
| ASSERT_TRUE(extension); |
| EXPECT_EQ("foo", extension->name()); |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, InstallDroppedFileUserScript) { |
| base::FilePath script_path = |
| data_dir().AppendASCII("user_script_basic.user.js"); |
| extensions::ExtensionInstallUI::set_disable_ui_for_tests(); |
| ScopedTestDialogAutoConfirm auto_confirm(ScopedTestDialogAutoConfirm::ACCEPT); |
| |
| std::unique_ptr<content::WebContents> web_contents( |
| content::WebContentsTester::CreateTestWebContents(profile(), nullptr)); |
| DeveloperPrivateAPI::Get(profile())->SetDraggedPath(web_contents.get(), |
| script_path); |
| |
| scoped_refptr<UIThreadExtensionFunction> function = |
| base::MakeRefCounted<api::DeveloperPrivateInstallDroppedFileFunction>(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| |
| TestExtensionRegistryObserver observer(registry()); |
| ASSERT_TRUE(api_test_utils::RunFunction(function.get(), "[]", profile())) |
| << function->GetError(); |
| const Extension* extension = observer.WaitForExtensionInstalled(); |
| ASSERT_TRUE(extension); |
| EXPECT_EQ("My user script", extension->name()); |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, GrantHostPermission) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| extensions_features::kRuntimeHostPermissions); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("test").AddPermission("<all_urls>").Build(); |
| service()->AddExtension(extension.get()); |
| ScriptingPermissionsModifier modifier(profile(), extension.get()); |
| EXPECT_FALSE(modifier.HasWithheldHostPermissions()); |
| modifier.SetWithholdHostPermissions(true); |
| |
| auto run_add_host_permission = [this, extension](base::StringPiece host, |
| bool should_succeed, |
| const char* expected_error) { |
| SCOPED_TRACE(host); |
| scoped_refptr<UIThreadExtensionFunction> function = |
| base::MakeRefCounted<api::DeveloperPrivateAddHostPermissionFunction>(); |
| |
| std::string args = base::StringPrintf(R"(["%s", "%s"])", |
| extension->id().c_str(), host.data()); |
| if (should_succeed) { |
| EXPECT_TRUE(api_test_utils::RunFunction(function.get(), args, profile())) |
| << function->GetError(); |
| } else { |
| EXPECT_EQ(expected_error, api_test_utils::RunFunctionAndReturnError( |
| function.get(), args, profile())); |
| } |
| }; |
| |
| const GURL kExampleCom("https://example.com/"); |
| EXPECT_FALSE(modifier.HasGrantedHostPermission(kExampleCom)); |
| run_add_host_permission("https://example.com/*", true, nullptr); |
| EXPECT_TRUE(modifier.HasGrantedHostPermission(kExampleCom)); |
| |
| const GURL kGoogleCom("https://google.com"); |
| const GURL kMapsGoogleCom("https://maps.google.com/"); |
| EXPECT_FALSE(modifier.HasGrantedHostPermission(kGoogleCom)); |
| EXPECT_FALSE(modifier.HasGrantedHostPermission(kMapsGoogleCom)); |
| run_add_host_permission("https://*.google.com/*", true, nullptr); |
| EXPECT_TRUE(modifier.HasGrantedHostPermission(kGoogleCom)); |
| EXPECT_TRUE(modifier.HasGrantedHostPermission(kMapsGoogleCom)); |
| |
| run_add_host_permission(kInvalidHost, false, kInvalidHostError); |
| // Path of the pattern must exactly match "/*". |
| run_add_host_permission("https://example.com/", false, kInvalidHostError); |
| run_add_host_permission("https://example.com/foobar", false, |
| kInvalidHostError); |
| run_add_host_permission("https://example.com/#foobar", false, |
| kInvalidHostError); |
| run_add_host_permission("https://example.com/*foobar", false, |
| kInvalidHostError); |
| |
| // Cannot grant chrome:-scheme URLs. |
| GURL chrome_host("chrome://settings/*"); |
| run_add_host_permission(chrome_host.spec(), false, kInvalidHostError); |
| |
| EXPECT_FALSE(modifier.HasGrantedHostPermission(chrome_host)); |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, RemoveHostPermission) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| extensions_features::kRuntimeHostPermissions); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("test").AddPermission("<all_urls>").Build(); |
| service()->AddExtension(extension.get()); |
| ScriptingPermissionsModifier modifier(profile(), extension.get()); |
| EXPECT_FALSE(modifier.HasWithheldHostPermissions()); |
| modifier.SetWithholdHostPermissions(true); |
| |
| auto run_remove_host_permission = [this, extension]( |
| base::StringPiece host, |
| bool should_succeed, |
| const char* expected_error) { |
| SCOPED_TRACE(host); |
| scoped_refptr<UIThreadExtensionFunction> function = base::MakeRefCounted< |
| api::DeveloperPrivateRemoveHostPermissionFunction>(); |
| std::string args = base::StringPrintf(R"(["%s", "%s"])", |
| extension->id().c_str(), host.data()); |
| if (should_succeed) { |
| EXPECT_TRUE(api_test_utils::RunFunction(function.get(), args, profile())) |
| << function->GetError(); |
| } else { |
| EXPECT_EQ(expected_error, api_test_utils::RunFunctionAndReturnError( |
| function.get(), args, profile())); |
| } |
| }; |
| |
| run_remove_host_permission("https://example.com/*", false, |
| "Cannot remove a host that hasn't been granted."); |
| |
| const GURL kExampleCom("https://example.com"); |
| modifier.GrantHostPermission(kExampleCom); |
| EXPECT_TRUE(modifier.HasGrantedHostPermission(kExampleCom)); |
| |
| // Path of the pattern must exactly match "/*". |
| run_remove_host_permission("https://example.com/", false, kInvalidHostError); |
| run_remove_host_permission("https://example.com/foobar", false, |
| kInvalidHostError); |
| run_remove_host_permission("https://example.com/#foobar", false, |
| kInvalidHostError); |
| run_remove_host_permission("https://example.com/*foobar", false, |
| kInvalidHostError); |
| run_remove_host_permission(kInvalidHost, false, kInvalidHostError); |
| EXPECT_TRUE(modifier.HasGrantedHostPermission(kExampleCom)); |
| |
| run_remove_host_permission("https://example.com/*", true, nullptr); |
| EXPECT_FALSE(modifier.HasGrantedHostPermission(kExampleCom)); |
| |
| URLPattern new_pattern(Extension::kValidHostPermissionSchemes, |
| "https://*.google.com/*"); |
| PermissionsUpdater(profile()).GrantRuntimePermissions( |
| *extension, PermissionSet(APIPermissionSet(), ManifestPermissionSet(), |
| URLPatternSet({new_pattern}), URLPatternSet())); |
| |
| const GURL kGoogleCom("https://google.com/"); |
| const GURL kMapsGoogleCom("https://maps.google.com/"); |
| EXPECT_TRUE(modifier.HasGrantedHostPermission(kGoogleCom)); |
| EXPECT_TRUE(modifier.HasGrantedHostPermission(kMapsGoogleCom)); |
| |
| run_remove_host_permission("https://*.google.com/*", true, nullptr); |
| EXPECT_FALSE(modifier.HasGrantedHostPermission(kGoogleCom)); |
| EXPECT_FALSE(modifier.HasGrantedHostPermission(kMapsGoogleCom)); |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, UpdateHostAccess) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| extensions_features::kRuntimeHostPermissions); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("test").AddPermission("<all_urls>").Build(); |
| service()->AddExtension(extension.get()); |
| ScriptingPermissionsModifier modifier(profile(), extension.get()); |
| |
| EXPECT_FALSE(modifier.HasWithheldHostPermissions()); |
| |
| RunUpdateHostAccess(*extension, "ON_CLICK"); |
| EXPECT_TRUE(modifier.HasWithheldHostPermissions()); |
| |
| RunUpdateHostAccess(*extension, "ON_ALL_SITES"); |
| EXPECT_FALSE(modifier.HasWithheldHostPermissions()); |
| |
| RunUpdateHostAccess(*extension, "ON_SPECIFIC_SITES"); |
| EXPECT_TRUE(modifier.HasWithheldHostPermissions()); |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, |
| UpdateHostAccess_SpecificSitesRemovedOnTransitionToOnClick) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| extensions_features::kRuntimeHostPermissions); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("test").AddPermission("<all_urls>").Build(); |
| service()->AddExtension(extension.get()); |
| ScriptingPermissionsModifier modifier(profile(), extension.get()); |
| modifier.SetWithholdHostPermissions(true); |
| |
| RunUpdateHostAccess(*extension, "ON_SPECIFIC_SITES"); |
| const GURL example_com("https://example.com"); |
| modifier.GrantHostPermission(example_com); |
| EXPECT_TRUE(modifier.HasWithheldHostPermissions()); |
| EXPECT_TRUE(modifier.HasGrantedHostPermission(example_com)); |
| |
| RunUpdateHostAccess(*extension, "ON_CLICK"); |
| EXPECT_TRUE(modifier.HasWithheldHostPermissions()); |
| EXPECT_FALSE(modifier.HasGrantedHostPermission(example_com)); |
| |
| // NOTE(devlin): It's a bit unfortunate that by cycling between host access |
| // settings, a user loses any stored state. This would be painful if the user |
| // had set "always run on foo" for a dozen or so sites, and accidentally |
| // changed the setting. |
| // There are ways we could address this, such as introducing a tri-state for |
| // the preference and keeping a stored set of any granted host permissions, |
| // but this then results in a funny edge case: |
| // - User has "on specific sites" set, with access to example.com and |
| // chromium.org granted. |
| // - User changes to "on click" -> no sites are granted. |
| // - User visits google.com, and says "always run on this site." This changes |
| // the setting back to "on specific sites", and will implicitly re-grant |
| // example.com and chromium.org permissions, without any additional |
| // prompting. |
| // To avoid this, we just clear any granted permissions when the user |
| // transitions between states. Since this is definitely a power-user surface, |
| // this is likely okay. |
| RunUpdateHostAccess(*extension, "ON_SPECIFIC_SITES"); |
| EXPECT_TRUE(modifier.HasWithheldHostPermissions()); |
| EXPECT_FALSE(modifier.HasGrantedHostPermission(example_com)); |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, |
| UpdateHostAccess_SpecificSitesRemovedOnTransitionToAllSites) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| extensions_features::kRuntimeHostPermissions); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("test").AddPermission("<all_urls>").Build(); |
| service()->AddExtension(extension.get()); |
| ScriptingPermissionsModifier modifier(profile(), extension.get()); |
| modifier.SetWithholdHostPermissions(true); |
| |
| RunUpdateHostAccess(*extension, "ON_SPECIFIC_SITES"); |
| const GURL example_com("https://example.com"); |
| modifier.GrantHostPermission(example_com); |
| EXPECT_TRUE(modifier.HasWithheldHostPermissions()); |
| EXPECT_TRUE(modifier.HasGrantedHostPermission(example_com)); |
| |
| RunUpdateHostAccess(*extension, "ON_ALL_SITES"); |
| EXPECT_FALSE(modifier.HasWithheldHostPermissions()); |
| EXPECT_TRUE(modifier.HasGrantedHostPermission(example_com)); |
| |
| RunUpdateHostAccess(*extension, "ON_SPECIFIC_SITES"); |
| EXPECT_TRUE(modifier.HasWithheldHostPermissions()); |
| EXPECT_FALSE(modifier.HasGrantedHostPermission(example_com)); |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, |
| UpdateHostAccess_GrantScopeGreaterThanRequestedScope) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| extensions_features::kRuntimeHostPermissions); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("test").AddPermission("http://*/*").Build(); |
| service()->AddExtension(extension.get()); |
| ScriptingPermissionsModifier modifier(profile(), extension.get()); |
| modifier.SetWithholdHostPermissions(true); |
| |
| ExtensionPrefs* extension_prefs = ExtensionPrefs::Get(profile()); |
| EXPECT_EQ(PermissionSet(), |
| extension->permissions_data()->active_permissions()); |
| EXPECT_EQ(PermissionSet(), |
| *extension_prefs->GetRuntimeGrantedPermissions(extension->id())); |
| |
| { |
| scoped_refptr<UIThreadExtensionFunction> function = |
| base::MakeRefCounted<api::DeveloperPrivateAddHostPermissionFunction>(); |
| std::string args = base::StringPrintf( |
| R"(["%s", "%s"])", extension->id().c_str(), "*://chromium.org/*"); |
| EXPECT_TRUE(api_test_utils::RunFunction(function.get(), args, profile())) |
| << function->GetError(); |
| } |
| |
| // The active permissions (which are given to the extension process) should |
| // only include the intersection of what was requested by the extension and |
| // the runtime granted permissions - which is http://chromium.org/*. |
| URLPattern http_chromium(Extension::kValidHostPermissionSchemes, |
| "http://chromium.org/*"); |
| const PermissionSet http_chromium_set( |
| APIPermissionSet(), ManifestPermissionSet(), |
| URLPatternSet({http_chromium}), URLPatternSet()); |
| EXPECT_EQ(http_chromium_set, |
| extension->permissions_data()->active_permissions()); |
| |
| // The runtime granted permissions should include all of what was approved by |
| // the user, which is *://chromium.org/*, and should be present in both the |
| // scriptable and explicit hosts. |
| URLPattern all_chromium(Extension::kValidHostPermissionSchemes, |
| "*://chromium.org/*"); |
| const PermissionSet all_chromium_set( |
| APIPermissionSet(), ManifestPermissionSet(), |
| URLPatternSet({all_chromium}), URLPatternSet({all_chromium})); |
| EXPECT_EQ(all_chromium_set, |
| *extension_prefs->GetRuntimeGrantedPermissions(extension->id())); |
| |
| { |
| scoped_refptr<UIThreadExtensionFunction> function = base::MakeRefCounted< |
| api::DeveloperPrivateRemoveHostPermissionFunction>(); |
| std::string args = base::StringPrintf( |
| R"(["%s", "%s"])", extension->id().c_str(), "*://chromium.org/*"); |
| EXPECT_TRUE(api_test_utils::RunFunction(function.get(), args, profile())) |
| << function->GetError(); |
| } |
| |
| // Removing the granted permission should remove it entirely from both |
| // the active and the stored permissions. |
| EXPECT_EQ(PermissionSet(), |
| extension->permissions_data()->active_permissions()); |
| EXPECT_EQ(PermissionSet(), |
| *extension_prefs->GetRuntimeGrantedPermissions(extension->id())); |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, |
| UpdateHostAccess_UnrequestedHostsDispatchUpdateEvents) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature( |
| extensions_features::kRuntimeHostPermissions); |
| |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder("test").AddPermission("http://google.com/*").Build(); |
| service()->AddExtension(extension.get()); |
| ScriptingPermissionsModifier modifier(profile(), extension.get()); |
| modifier.SetWithholdHostPermissions(true); |
| |
| // We need to call DeveloperPrivateAPI::Get() in order to instantiate the |
| // keyed service, since it's not created by default in unit tests. |
| DeveloperPrivateAPI::Get(profile()); |
| const ExtensionId listener_id = crx_file::id_util::GenerateId("listener"); |
| EventRouter* event_router = EventRouter::Get(profile()); |
| |
| // The DeveloperPrivateEventRouter will only dispatch events if there's at |
| // least one listener to dispatch to. Create one. |
| content::RenderProcessHost* process = nullptr; |
| const char* kEventName = |
| api::developer_private::OnItemStateChanged::kEventName; |
| event_router->AddEventListener(kEventName, process, listener_id); |
| |
| TestEventRouterObserver test_observer(event_router); |
| EXPECT_FALSE( |
| WasPermissionsUpdatedEventDispatched(test_observer, extension->id())); |
| |
| URLPatternSet hosts({URLPattern(Extension::kValidHostPermissionSchemes, |
| "https://example.com/*")}); |
| PermissionSet permissions(APIPermissionSet(), ManifestPermissionSet(), hosts, |
| hosts); |
| PermissionsUpdater(profile()).GrantRuntimePermissions(*extension, |
| permissions); |
| // The event router fetches icons from a blocking thread when sending the |
| // update event; allow it to finish before verifying the event was dispatched. |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE( |
| WasPermissionsUpdatedEventDispatched(test_observer, extension->id())); |
| |
| test_observer.ClearEvents(); |
| |
| PermissionsUpdater(profile()).RevokeRuntimePermissions(*extension, |
| permissions); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE( |
| WasPermissionsUpdatedEventDispatched(test_observer, extension->id())); |
| } |
| |
| TEST_F(DeveloperPrivateApiUnitTest, ExtensionUpdatedEventOnPermissionsChange) { |
| // We need to call DeveloperPrivateAPI::Get() in order to instantiate the |
| // keyed service, since it's not created by default in unit tests. |
| DeveloperPrivateAPI::Get(profile()); |
| const ExtensionId listener_id = crx_file::id_util::GenerateId("listener"); |
| EventRouter* event_router = EventRouter::Get(profile()); |
| |
| // The DeveloperPrivateEventRouter will only dispatch events if there's at |
| // least one listener to dispatch to. Create one. |
| content::RenderProcessHost* process = nullptr; |
| const char* kEventName = |
| api::developer_private::OnItemStateChanged::kEventName; |
| event_router->AddEventListener(kEventName, process, listener_id); |
| |
| scoped_refptr<const Extension> dummy_extension = |
| ExtensionBuilder("dummy") |
| .SetManifestKey("optional_permissions", |
| ListBuilder().Append("tabs").Build()) |
| .Build(); |
| |
| TestEventRouterObserver test_observer(event_router); |
| EXPECT_FALSE(WasPermissionsUpdatedEventDispatched(test_observer, |
| dummy_extension->id())); |
| |
| APIPermissionSet apis; |
| apis.insert(APIPermission::kTab); |
| PermissionSet permissions(apis, ManifestPermissionSet(), URLPatternSet(), |
| URLPatternSet()); |
| PermissionsUpdater(profile()).GrantOptionalPermissions(*dummy_extension, |
| permissions); |
| // The event router fetches icons from a blocking thread when sending the |
| // update event; allow it to finish before verifying the event was dispatched. |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(WasPermissionsUpdatedEventDispatched(test_observer, |
| dummy_extension->id())); |
| |
| test_observer.ClearEvents(); |
| |
| PermissionsUpdater(profile()).RevokeOptionalPermissions( |
| *dummy_extension, permissions, PermissionsUpdater::REMOVE_HARD); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(WasPermissionsUpdatedEventDispatched(test_observer, |
| dummy_extension->id())); |
| } |
| |
| class DeveloperPrivateZipInstallerUnitTest |
| : public DeveloperPrivateApiUnitTest { |
| public: |
| DeveloperPrivateZipInstallerUnitTest() { |
| service_manager::TestConnectorFactory::NameToServiceMap services; |
| services.insert(std::make_pair("data_decoder", |
| data_decoder::DataDecoderService::Create())); |
| services.insert( |
| std::make_pair("unzip_service", unzip::UnzipService::CreateService())); |
| test_connector_factory_ = |
| service_manager::TestConnectorFactory::CreateForServices( |
| std::move(services)); |
| connector_ = test_connector_factory_->CreateConnector(); |
| } |
| ~DeveloperPrivateZipInstallerUnitTest() override {} |
| |
| private: |
| std::unique_ptr<service_manager::TestConnectorFactory> |
| test_connector_factory_; |
| std::unique_ptr<service_manager::Connector> connector_; |
| |
| DISALLOW_COPY_AND_ASSIGN(DeveloperPrivateZipInstallerUnitTest); |
| }; |
| |
| TEST_F(DeveloperPrivateZipInstallerUnitTest, InstallDroppedFileZip) { |
| base::FilePath zip_path = data_dir().AppendASCII("simple_empty.zip"); |
| extensions::ExtensionInstallUI::set_disable_ui_for_tests(); |
| ScopedTestDialogAutoConfirm auto_confirm(ScopedTestDialogAutoConfirm::ACCEPT); |
| |
| std::unique_ptr<content::WebContents> web_contents( |
| content::WebContentsTester::CreateTestWebContents(profile(), nullptr)); |
| DeveloperPrivateAPI::Get(profile())->SetDraggedPath(web_contents.get(), |
| zip_path); |
| |
| scoped_refptr<UIThreadExtensionFunction> function = |
| base::MakeRefCounted<api::DeveloperPrivateInstallDroppedFileFunction>(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| |
| TestExtensionRegistryObserver observer(registry()); |
| ASSERT_TRUE(api_test_utils::RunFunction(function.get(), "[]", profile())) |
| << function->GetError(); |
| const Extension* extension = observer.WaitForExtensionInstalled(); |
| ASSERT_TRUE(extension); |
| EXPECT_EQ("Simple Empty Extension", extension->name()); |
| } |
| |
| class DeveloperPrivateApiSupervisedUserUnitTest |
| : public DeveloperPrivateApiUnitTest { |
| public: |
| DeveloperPrivateApiSupervisedUserUnitTest() = default; |
| ~DeveloperPrivateApiSupervisedUserUnitTest() override = default; |
| |
| bool ProfileIsSupervised() const override { return true; } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(DeveloperPrivateApiSupervisedUserUnitTest); |
| }; |
| |
| // Tests trying to call loadUnpacked when the profile shouldn't be allowed to. |
| TEST_F(DeveloperPrivateApiSupervisedUserUnitTest, |
| LoadUnpackedFailsForSupervisedUsers) { |
| std::unique_ptr<content::WebContents> web_contents( |
| content::WebContentsTester::CreateTestWebContents(profile(), nullptr)); |
| |
| base::FilePath path = data_dir().AppendASCII("simple_with_popup"); |
| api::EntryPicker::SkipPickerAndAlwaysSelectPathForTest(&path); |
| |
| ASSERT_TRUE(profile()->IsSupervised()); |
| |
| scoped_refptr<UIThreadExtensionFunction> function = |
| base::MakeRefCounted<api::DeveloperPrivateLoadUnpackedFunction>(); |
| function->SetRenderFrameHost(web_contents->GetMainFrame()); |
| std::string error = extension_function_test_utils::RunFunctionAndReturnError( |
| function.get(), "[]", browser()); |
| EXPECT_THAT(error, testing::HasSubstr("Supervised")); |
| } |
| |
| } // namespace extensions |