| // 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 "base/json/json_file_value_serializer.h" |
| #include "base/json/json_writer.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/extensions/api/developer_private/extension_info_generator.h" |
| #include "chrome/browser/extensions/api/developer_private/inspectable_views_finder.h" |
| #include "chrome/browser/extensions/error_console/error_console.h" |
| #include "chrome/browser/extensions/extension_service.h" |
| #include "chrome/browser/extensions/extension_service_test_base.h" |
| #include "chrome/browser/extensions/extension_util.h" |
| #include "chrome/browser/extensions/permissions_updater.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/common/extensions/api/developer_private.h" |
| #include "chrome/common/pref_names.h" |
| #include "components/crx_file/id_util.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/common/constants.h" |
| #include "extensions/common/extension.h" |
| #include "extensions/common/extension_builder.h" |
| #include "extensions/common/feature_switch.h" |
| #include "extensions/common/value_builder.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace extensions { |
| |
| namespace developer = api::developer_private; |
| |
| namespace { |
| |
| const char kAllHostsPermission[] = "*://*/*"; |
| |
| scoped_ptr<base::DictionaryValue> DeserializeJSONTestData( |
| const base::FilePath& path, |
| std::string *error) { |
| base::Value* value = nullptr; |
| JSONFileValueDeserializer deserializer(path); |
| value = deserializer.Deserialize(nullptr, error); |
| return make_scoped_ptr(static_cast<base::DictionaryValue*>(value)); |
| } |
| |
| } // namespace |
| |
| class ExtensionInfoGeneratorUnitTest : public ExtensionServiceTestBase { |
| public: |
| ExtensionInfoGeneratorUnitTest() {} |
| ~ExtensionInfoGeneratorUnitTest() override {} |
| |
| protected: |
| void SetUp() override { |
| ExtensionServiceTestBase::SetUp(); |
| InitializeEmptyExtensionService(); |
| } |
| |
| void OnInfosGenerated(linked_ptr<developer::ExtensionInfo>* info_out, |
| const ExtensionInfoGenerator::ExtensionInfoList& list) { |
| EXPECT_EQ(1u, list.size()); |
| if (!list.empty()) |
| *info_out = list[0]; |
| quit_closure_.Run(); |
| quit_closure_.Reset(); |
| } |
| |
| scoped_ptr<developer::ExtensionInfo> GenerateExtensionInfo( |
| const std::string& extension_id) { |
| linked_ptr<developer::ExtensionInfo> info; |
| base::RunLoop run_loop; |
| quit_closure_ = run_loop.QuitClosure(); |
| scoped_ptr<ExtensionInfoGenerator> generator( |
| new ExtensionInfoGenerator(browser_context())); |
| generator->CreateExtensionInfo( |
| extension_id, |
| base::Bind(&ExtensionInfoGeneratorUnitTest::OnInfosGenerated, |
| base::Unretained(this), |
| base::Unretained(&info))); |
| run_loop.Run(); |
| return make_scoped_ptr(info.release()); |
| } |
| |
| const scoped_refptr<const Extension> CreateExtension( |
| const std::string& name, |
| ListBuilder& permissions) { |
| const std::string kId = crx_file::id_util::GenerateId(name); |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder().SetManifest( |
| DictionaryBuilder() |
| .Set("name", name) |
| .Set("description", "an extension") |
| .Set("manifest_version", 2) |
| .Set("version", "1.0.0") |
| .Set("permissions", permissions)) |
| .SetLocation(Manifest::INTERNAL) |
| .SetID(kId) |
| .Build(); |
| |
| ExtensionRegistry::Get(profile())->AddEnabled(extension); |
| PermissionsUpdater(profile()).InitializePermissions(extension.get()); |
| return extension; |
| } |
| |
| scoped_ptr<developer::ExtensionInfo> CreateExtensionInfoFromPath( |
| const base::FilePath& extension_path, |
| Manifest::Location location) { |
| std::string error; |
| |
| base::FilePath manifest_path = extension_path.Append(kManifestFilename); |
| scoped_ptr<base::DictionaryValue> extension_data = |
| DeserializeJSONTestData(manifest_path, &error); |
| EXPECT_EQ(std::string(), error); |
| |
| scoped_refptr<Extension> extension(Extension::Create( |
| extension_path, location, *extension_data, Extension::REQUIRE_KEY, |
| &error)); |
| CHECK(extension.get()); |
| service()->AddExtension(extension.get()); |
| EXPECT_EQ(std::string(), error); |
| |
| return GenerateExtensionInfo(extension->id()); |
| } |
| |
| void CompareExpectedAndActualOutput( |
| const base::FilePath& extension_path, |
| const InspectableViewsFinder::ViewList& views, |
| const base::FilePath& expected_output_path) { |
| std::string error; |
| scoped_ptr<base::DictionaryValue> expected_output_data( |
| DeserializeJSONTestData(expected_output_path, &error)); |
| EXPECT_EQ(std::string(), error); |
| |
| // Produce test output. |
| scoped_ptr<developer::ExtensionInfo> info = |
| CreateExtensionInfoFromPath(extension_path, Manifest::INVALID_LOCATION); |
| info->views = views; |
| scoped_ptr<base::DictionaryValue> actual_output_data = info->ToValue(); |
| ASSERT_TRUE(actual_output_data); |
| |
| // Compare the outputs. |
| // Ignore unknown fields in the actual output data. |
| std::string paths_details = " - expected (" + |
| expected_output_path.MaybeAsASCII() + ") vs. actual (" + |
| extension_path.MaybeAsASCII() + ")"; |
| std::string expected_string; |
| std::string actual_string; |
| for (base::DictionaryValue::Iterator field(*expected_output_data); |
| !field.IsAtEnd(); field.Advance()) { |
| const base::Value& expected_value = field.value(); |
| base::Value* actual_value = nullptr; |
| EXPECT_TRUE(actual_output_data->Get(field.key(), &actual_value)) << |
| field.key() + " is missing" + paths_details; |
| if (!actual_value) |
| continue; |
| if (!actual_value->Equals(&expected_value)) { |
| base::JSONWriter::Write(expected_value, &expected_string); |
| base::JSONWriter::Write(*actual_value, &actual_string); |
| EXPECT_EQ(expected_string, actual_string) << |
| field.key() << paths_details; |
| } |
| } |
| } |
| |
| private: |
| base::Closure quit_closure_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ExtensionInfoGeneratorUnitTest); |
| }; |
| |
| // Test some of the basic fields. |
| TEST_F(ExtensionInfoGeneratorUnitTest, BasicInfoTest) { |
| // Enable error console for testing. |
| ResetThreadBundle(content::TestBrowserThreadBundle::DEFAULT); |
| FeatureSwitch::ScopedOverride error_console_override( |
| FeatureSwitch::error_console(), true); |
| profile()->GetPrefs()->SetBoolean(prefs::kExtensionsUIDeveloperMode, true); |
| |
| const char kName[] = "extension name"; |
| const char kVersion[] = "1.0.0.1"; |
| std::string id = crx_file::id_util::GenerateId("alpha"); |
| scoped_ptr<base::DictionaryValue> manifest = |
| DictionaryBuilder().Set("name", kName) |
| .Set("version", kVersion) |
| .Set("manifest_version", 2) |
| .Set("description", "an extension") |
| .Set("permissions", |
| ListBuilder().Append("file://*/*")).Build(); |
| scoped_ptr<base::DictionaryValue> manifest_copy(manifest->DeepCopy()); |
| scoped_refptr<const Extension> extension = |
| ExtensionBuilder().SetManifest(manifest.Pass()) |
| .SetLocation(Manifest::UNPACKED) |
| .SetPath(data_dir()) |
| .SetID(id) |
| .Build(); |
| service()->AddExtension(extension.get()); |
| ErrorConsole* error_console = ErrorConsole::Get(profile()); |
| error_console->ReportError( |
| make_scoped_ptr(new RuntimeError( |
| extension->id(), |
| false, |
| base::UTF8ToUTF16("source"), |
| base::UTF8ToUTF16("message"), |
| StackTrace(1, StackFrame(1, |
| 1, |
| base::UTF8ToUTF16("source"), |
| base::UTF8ToUTF16("function"))), |
| GURL("url"), |
| logging::LOG_ERROR, |
| 1, |
| 1))); |
| error_console->ReportError( |
| make_scoped_ptr(new ManifestError(extension->id(), |
| base::UTF8ToUTF16("message"), |
| base::UTF8ToUTF16("key"), |
| base::string16()))); |
| error_console->ReportError( |
| make_scoped_ptr(new RuntimeError( |
| extension->id(), |
| false, |
| base::UTF8ToUTF16("source"), |
| base::UTF8ToUTF16("message"), |
| StackTrace(1, StackFrame(1, |
| 1, |
| base::UTF8ToUTF16("source"), |
| base::UTF8ToUTF16("function"))), |
| GURL("url"), |
| logging::LOG_VERBOSE, |
| 1, |
| 1))); |
| |
| // It's not feasible to validate every field here, because that would be |
| // a duplication of the logic in the method itself. Instead, test a handful |
| // of fields for sanity. |
| scoped_ptr<api::developer_private::ExtensionInfo> info = |
| GenerateExtensionInfo(extension->id()); |
| ASSERT_TRUE(info.get()); |
| EXPECT_EQ(kName, info->name); |
| EXPECT_EQ(id, info->id); |
| EXPECT_EQ(kVersion, info->version); |
| EXPECT_EQ(info->location, developer::LOCATION_UNPACKED); |
| ASSERT_TRUE(info->path); |
| EXPECT_EQ(data_dir(), base::FilePath::FromUTF8Unsafe(*info->path)); |
| EXPECT_EQ(api::developer_private::EXTENSION_STATE_ENABLED, info->state); |
| EXPECT_EQ(api::developer_private::EXTENSION_TYPE_EXTENSION, info->type); |
| EXPECT_TRUE(info->file_access.is_enabled); |
| EXPECT_FALSE(info->file_access.is_active); |
| EXPECT_TRUE(info->incognito_access.is_enabled); |
| EXPECT_FALSE(info->incognito_access.is_active); |
| ASSERT_EQ(2u, info->runtime_errors.size()); |
| const api::developer_private::RuntimeError& runtime_error = |
| *info->runtime_errors[0]; |
| EXPECT_EQ(extension->id(), runtime_error.extension_id); |
| EXPECT_EQ(api::developer_private::ERROR_TYPE_RUNTIME, runtime_error.type); |
| EXPECT_EQ(api::developer_private::ERROR_LEVEL_ERROR, |
| runtime_error.severity); |
| EXPECT_EQ(1u, runtime_error.stack_trace.size()); |
| ASSERT_EQ(1u, info->manifest_errors.size()); |
| const api::developer_private::RuntimeError& runtime_error_verbose = |
| *info->runtime_errors[1]; |
| EXPECT_EQ(api::developer_private::ERROR_LEVEL_LOG, |
| runtime_error_verbose.severity); |
| const api::developer_private::ManifestError& manifest_error = |
| *info->manifest_errors[0]; |
| EXPECT_EQ(extension->id(), manifest_error.extension_id); |
| |
| // Test an extension that isn't unpacked. |
| manifest_copy->SetString("update_url", |
| "https://clients2.google.com/service/update2/crx"); |
| id = crx_file::id_util::GenerateId("beta"); |
| extension = ExtensionBuilder().SetManifest(manifest_copy.Pass()) |
| .SetLocation(Manifest::EXTERNAL_PREF) |
| .SetID(id) |
| .Build(); |
| service()->AddExtension(extension.get()); |
| info = GenerateExtensionInfo(extension->id()); |
| EXPECT_EQ(developer::LOCATION_THIRD_PARTY, info->location); |
| EXPECT_FALSE(info->path); |
| } |
| |
| // Test three generated json outputs. |
| TEST_F(ExtensionInfoGeneratorUnitTest, GenerateExtensionsJSONData) { |
| // Test Extension1 |
| base::FilePath extension_path = |
| data_dir().AppendASCII("good") |
| .AppendASCII("Extensions") |
| .AppendASCII("behllobkkfkfnphdnhnkndlbkcpglgmj") |
| .AppendASCII("1.0.0.0"); |
| |
| InspectableViewsFinder::ViewList views; |
| views.push_back(InspectableViewsFinder::ConstructView( |
| GURL("chrome-extension://behllobkkfkfnphdnhnkndlbkcpglgmj/bar.html"), |
| 42, 88, false, VIEW_TYPE_TAB_CONTENTS)); |
| views.push_back(InspectableViewsFinder::ConstructView( |
| GURL("chrome-extension://behllobkkfkfnphdnhnkndlbkcpglgmj/dog.html"), |
| 0, 0, false, VIEW_TYPE_TAB_CONTENTS)); |
| |
| base::FilePath expected_outputs_path = |
| data_dir().AppendASCII("api_test") |
| .AppendASCII("developer") |
| .AppendASCII("generated_output"); |
| |
| CompareExpectedAndActualOutput( |
| extension_path, |
| views, |
| expected_outputs_path.AppendASCII( |
| "behllobkkfkfnphdnhnkndlbkcpglgmj.json")); |
| |
| #if !defined(OS_CHROMEOS) |
| // Test Extension2 |
| extension_path = data_dir().AppendASCII("good") |
| .AppendASCII("Extensions") |
| .AppendASCII("hpiknbiabeeppbpihjehijgoemciehgk") |
| .AppendASCII("2"); |
| |
| // It's OK to have duplicate URLs, so long as the IDs are different. |
| views[0]->url = |
| "chrome-extension://hpiknbiabeeppbpihjehijgoemciehgk/bar.html"; |
| views[1]->url = views[0]->url; |
| |
| CompareExpectedAndActualOutput( |
| extension_path, |
| views, |
| expected_outputs_path.AppendASCII( |
| "hpiknbiabeeppbpihjehijgoemciehgk.json")); |
| #endif |
| |
| // Test Extension3 |
| extension_path = data_dir().AppendASCII("good") |
| .AppendASCII("Extensions") |
| .AppendASCII("bjafgdebaacbbbecmhlhpofkepfkgcpa") |
| .AppendASCII("1.0"); |
| views.clear(); |
| |
| CompareExpectedAndActualOutput( |
| extension_path, |
| views, |
| expected_outputs_path.AppendASCII( |
| "bjafgdebaacbbbecmhlhpofkepfkgcpa.json")); |
| } |
| |
| // Test that the all_urls checkbox only shows up for extensions that want all |
| // urls, and only when the switch is on. |
| TEST_F(ExtensionInfoGeneratorUnitTest, ExtensionInfoRunOnAllUrls) { |
| // Start with the switch enabled. |
| scoped_ptr<FeatureSwitch::ScopedOverride> enable_scripts_switch( |
| new FeatureSwitch::ScopedOverride( |
| FeatureSwitch::scripts_require_action(), true)); |
| // Two extensions - one with all urls, one without. |
| scoped_refptr<const Extension> all_urls_extension = CreateExtension( |
| "all_urls", ListBuilder().Append(kAllHostsPermission).Pass()); |
| scoped_refptr<const Extension> no_urls_extension = |
| CreateExtension("no urls", ListBuilder().Pass()); |
| |
| scoped_ptr<developer::ExtensionInfo> info = |
| GenerateExtensionInfo(all_urls_extension->id()); |
| |
| // The extension should want all urls, but not currently have it. |
| EXPECT_TRUE(info->run_on_all_urls.is_enabled); |
| EXPECT_FALSE(info->run_on_all_urls.is_active); |
| |
| // Give the extension all urls. |
| util::SetAllowedScriptingOnAllUrls(all_urls_extension->id(), profile(), true); |
| |
| // Now the extension should both want and have all urls. |
| info = GenerateExtensionInfo(all_urls_extension->id()); |
| EXPECT_TRUE(info->run_on_all_urls.is_enabled); |
| EXPECT_TRUE(info->run_on_all_urls.is_active); |
| |
| // The other extension should neither want nor have all urls. |
| info = GenerateExtensionInfo(no_urls_extension->id()); |
| EXPECT_FALSE(info->run_on_all_urls.is_enabled); |
| EXPECT_FALSE(info->run_on_all_urls.is_active); |
| |
| // Revoke the first extension's permissions. |
| util::SetAllowedScriptingOnAllUrls( |
| all_urls_extension->id(), profile(), false); |
| |
| // Turn off the switch and load another extension (so permissions are |
| // re-initialized). |
| enable_scripts_switch.reset(); |
| |
| // Since the extension doesn't have access to all urls (but normally would), |
| // the extension should have the "want" flag even with the switch off. |
| info = GenerateExtensionInfo(all_urls_extension->id()); |
| EXPECT_TRUE(info->run_on_all_urls.is_enabled); |
| EXPECT_FALSE(info->run_on_all_urls.is_active); |
| |
| // If we grant the extension all urls, then the checkbox should still be |
| // there, since it has an explicitly-set user preference. |
| util::SetAllowedScriptingOnAllUrls(all_urls_extension->id(), profile(), true); |
| info = GenerateExtensionInfo(all_urls_extension->id()); |
| EXPECT_TRUE(info->run_on_all_urls.is_enabled); |
| EXPECT_TRUE(info->run_on_all_urls.is_active); |
| |
| // Load another extension with all urls (so permissions get re-init'd). |
| all_urls_extension = CreateExtension( |
| "all_urls_II", ListBuilder().Append(kAllHostsPermission).Pass()); |
| |
| // Even though the extension has all_urls permission, the checkbox shouldn't |
| // show up without the switch. |
| info = GenerateExtensionInfo(all_urls_extension->id()); |
| EXPECT_FALSE(info->run_on_all_urls.is_enabled); |
| EXPECT_TRUE(info->run_on_all_urls.is_active); |
| } |
| |
| } // namespace extensions |