| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <memory> |
| |
| #include "base/command_line.h" |
| #include "base/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "chrome/browser/extensions/extension_action_dispatcher.h" |
| #include "chrome/browser/extensions/extension_apitest.h" |
| #include "chrome/browser/extensions/extension_util.h" |
| #include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "components/sessions/content/session_tab_helper.h" |
| #include "content/public/test/browser_test.h" |
| #include "content/public/test/browser_test_utils.h" |
| #include "extensions/browser/api/file_system/file_system_api.h" |
| #include "extensions/browser/background_script_executor.h" |
| #include "extensions/browser/event_router.h" |
| #include "extensions/browser/extension_action.h" |
| #include "extensions/browser/extension_action_manager.h" |
| #include "extensions/browser/extension_function_histogram_value.h" |
| #include "extensions/browser/extension_host_test_helper.h" |
| #include "extensions/browser/extension_util.h" |
| #include "extensions/browser/process_manager.h" |
| #include "extensions/browser/script_result_queue.h" |
| #include "extensions/common/extension_features.h" |
| #include "extensions/common/mojom/view_type.mojom.h" |
| #include "extensions/common/switches.h" |
| #include "extensions/test/extension_test_message_listener.h" |
| #include "extensions/test/result_catcher.h" |
| #include "extensions/test/test_extension_dir.h" |
| #include "net/dns/mock_host_resolver.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // A script that can verify whether a developer-mode-restricted API is |
| // available. Note that we use separate verify methods here (as opposed to |
| // a boolean "is API available") so we can better verify expected errors and |
| // give more meaningful messages in the case of failure. |
| constexpr char kCheckApiAvailability[] = |
| R"( |
| async function verifyApiIsAvailable() { |
| let message; |
| try { |
| const tabs = await chrome.tabs.query({}); |
| chrome.test.assertEq(1, tabs.length); |
| const debuggee = {tabId: tabs[0].id}; |
| await chrome.debugger.attach(debuggee, '1.3'); |
| await chrome.debugger.detach(debuggee); |
| message = 'success'; |
| } catch (e) { |
| message = 'Unexpected error: ' + e.toString(); |
| } |
| chrome.test.sendScriptResult(message); |
| } |
| |
| async function verifyApiIsNotAvailable() { |
| let message; |
| try { |
| // Note: we try to call a method on the API (and not just test |
| // accessing it) since, if it was previously instantiated when the |
| // API was available, it would still be present. |
| await chrome.debugger.getTargets(); |
| message = 'API unexpectedly available.'; |
| } catch(e) { |
| const expectedError = |
| `Error: Failed to read the 'debugger' property from ` + |
| `'Object': The 'debugger' API is only available for users ` + |
| 'in developer mode.'; |
| message = e.toString() == expectedError |
| ? 'success' |
| : 'Unexpected error: ' + e.toString(); |
| } |
| chrome.test.sendScriptResult(message); |
| })"; |
| |
| constexpr char kCheckApiAvailabilityUserScripts[] = |
| R"(const script = |
| { |
| id: 'script', |
| matches: ['*://*/*'], |
| js: [{file: 'script.js'}] |
| }; |
| async function verifyApiIsAvailable() { |
| let message; |
| try { |
| await chrome.userScripts.register([script]); |
| const registered = await chrome.userScripts.getScripts(); |
| message = |
| (registered.length == 1 && |
| registered[0].id == 'script') |
| ? 'success' |
| : 'Unexpected registration result: ' + |
| JSON.stringify(registered); |
| await chrome.userScripts.unregister(); |
| } catch (e) { |
| message = 'Unexpected error: ' + e.toString(); |
| } |
| chrome.test.sendScriptResult(message); |
| } |
| |
| async function verifyApiIsNotAvailable() { |
| let message; |
| try { |
| // Note: we try to call a method on the API (and not just test |
| // accessing it) since, if it was previously instantiated when the |
| // API was available, it would still be present. |
| await chrome.userScripts.register([script]); |
| message = 'API unexpectedly available.'; |
| await chrome.userScripts.unregister(); |
| } catch(e) { |
| const expectedError = |
| `Error: Failed to read the 'userScripts' property from ` + |
| `'Object': The 'userScripts' API is only available for users ` + |
| 'in developer mode.'; |
| message = e.toString() == expectedError |
| ? 'success' |
| : 'Unexpected error: ' + e.toString(); |
| } |
| chrome.test.sendScriptResult(message); |
| })"; |
| |
| bool ApiExists(content::WebContents* web_contents, |
| const std::string& api_name) { |
| return content::EvalJs(web_contents, |
| base::StringPrintf("!!%s;", api_name.c_str())) |
| .ExtractBool(); |
| } |
| |
| bool ObjectIsDefined(content::WebContents* web_contents, |
| const std::string& object_name) { |
| return content::EvalJs(web_contents, |
| base::StringPrintf("self.hasOwnProperty('%s');", |
| object_name.c_str())) |
| .ExtractBool(); |
| } |
| |
| } // namespace |
| |
| // And end-to-end test for extension APIs using native bindings. |
| class NativeBindingsApiTest : public ExtensionApiTest { |
| public: |
| NativeBindingsApiTest() = default; |
| |
| NativeBindingsApiTest(const NativeBindingsApiTest&) = delete; |
| NativeBindingsApiTest& operator=(const NativeBindingsApiTest&) = delete; |
| |
| ~NativeBindingsApiTest() override = default; |
| |
| void SetUpCommandLine(base::CommandLine* command_line) override { |
| ExtensionApiTest::SetUpCommandLine(command_line); |
| // We allowlist the extension so that it can use the cast.streaming.* APIs, |
| // which are the only APIs that are prefixed twice. |
| command_line->AppendSwitchASCII(switches::kAllowlistedExtensionID, |
| "ddchlicdkolnonkihahngkmmmjnjlkkf"); |
| } |
| |
| void SetUpOnMainThread() override { |
| ExtensionApiTest::SetUpOnMainThread(); |
| host_resolver()->AddRule("*", "127.0.0.1"); |
| } |
| }; |
| |
| IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, SimpleEndToEndTest) { |
| embedded_test_server()->ServeFilesFromDirectory(test_data_dir_); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("native_bindings/extension")) << message_; |
| } |
| |
| // A simplistic app test for app-specific APIs. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, SimpleAppTest) { |
| ExtensionTestMessageListener ready_listener("ready", |
| ReplyBehavior::kWillReply); |
| ASSERT_TRUE(RunExtensionTest("native_bindings/platform_app", |
| {.launch_as_platform_app = true})) |
| << message_; |
| ASSERT_TRUE(ready_listener.WaitUntilSatisfied()); |
| |
| // On reply, the extension will try to close the app window and send a |
| // message. |
| ExtensionTestMessageListener close_listener; |
| ready_listener.Reply(std::string()); |
| ASSERT_TRUE(close_listener.WaitUntilSatisfied()); |
| EXPECT_EQ("success", close_listener.message()); |
| } |
| |
| // Tests the declarativeContent API and declarative events. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, DeclarativeEvents) { |
| embedded_test_server()->ServeFilesFromDirectory(test_data_dir_); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| // Load an extension. On load, this extension will a) run a few simple tests |
| // using chrome.test.runTests() and b) set up rules for declarative events for |
| // a browser-driven test. Wait for both the tests to finish and the extension |
| // to be ready. |
| ExtensionTestMessageListener listener("ready"); |
| ResultCatcher catcher; |
| const Extension* extension = LoadExtension( |
| test_data_dir_.AppendASCII("native_bindings/declarative_content")); |
| ASSERT_TRUE(catcher.GetNextResult()) << catcher.message(); |
| ASSERT_TRUE(extension); |
| ASSERT_TRUE(listener.WaitUntilSatisfied()); |
| |
| // The extension's page action should currently be hidden. |
| ExtensionAction* action = |
| ExtensionActionManager::Get(profile())->GetExtensionAction(*extension); |
| content::WebContents* web_contents = GetActiveWebContents(); |
| int tab_id = sessions::SessionTabHelper::IdForTab(web_contents).id(); |
| EXPECT_FALSE(action->GetIsVisible(tab_id)); |
| EXPECT_TRUE(action->GetDeclarativeIcon(tab_id).IsEmpty()); |
| |
| // Navigating to example.com should show the page action. |
| ASSERT_TRUE(NavigateToURL( |
| web_contents, embedded_test_server()->GetURL( |
| "example.com", "/native_bindings/simple.html"))); |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(action->GetIsVisible(tab_id)); |
| EXPECT_FALSE(action->GetDeclarativeIcon(tab_id).IsEmpty()); |
| |
| // And the extension should be notified of the click. |
| ExtensionTestMessageListener clicked_listener("clicked and removed"); |
| ExtensionActionDispatcher::Get(profile())->DispatchExtensionActionClicked( |
| *action, web_contents, extension); |
| ASSERT_TRUE(clicked_listener.WaitUntilSatisfied()); |
| } |
| |
| IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, LazyListeners) { |
| ProcessManager::SetEventPageIdleTimeForTesting(1); |
| ProcessManager::SetEventPageSuspendingTimeForTesting(1); |
| |
| ExtensionHostTestHelper background_page_done(profile()); |
| background_page_done.RestrictToType( |
| mojom::ViewType::kExtensionBackgroundPage); |
| const Extension* extension = LoadExtension( |
| test_data_dir_.AppendASCII("native_bindings/lazy_listeners")); |
| ASSERT_TRUE(extension); |
| // Wait for the event page to cycle. |
| background_page_done.WaitForDocumentElementAvailable(); |
| background_page_done.WaitForHostDestroyed(); |
| |
| EventRouter* event_router = EventRouter::Get(profile()); |
| EXPECT_TRUE(event_router->ExtensionHasEventListener(extension->id(), |
| "tabs.onCreated")); |
| } |
| |
| // End-to-end test for the fileSystem API, which includes parameters with |
| // instance-of requirements and a post-validation argument updater that violates |
| // the schema. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, FileSystemApiGetDisplayPath) { |
| base::FilePath test_dir = test_data_dir_.AppendASCII("native_bindings"); |
| FileSystemChooseEntryFunction::RegisterTempExternalFileSystemForTest( |
| "test_root", test_dir); |
| base::FilePath test_file = test_dir.AppendASCII("text.txt"); |
| const FileSystemChooseEntryFunction::TestOptions test_options{ |
| .path_to_be_picked = &test_file}; |
| auto reset_options = |
| FileSystemChooseEntryFunction::SetOptionsForTesting(test_options); |
| ASSERT_TRUE(RunExtensionTest("native_bindings/instance_of", |
| {.launch_as_platform_app = true})) |
| << message_; |
| } |
| |
| // Tests the webRequest API, which requires IO thread requests and custom |
| // events. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, WebRequest) { |
| embedded_test_server()->ServeFilesFromDirectory(test_data_dir_); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| // Load an extension and wait for it to be ready. |
| ResultCatcher catcher; |
| const Extension* extension = |
| LoadExtension(test_data_dir_.AppendASCII("native_bindings/web_request")); |
| ASSERT_TRUE(extension); |
| ASSERT_TRUE(catcher.GetNextResult()) << catcher.message(); |
| |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL( |
| web_contents, embedded_test_server()->GetURL( |
| "example.com", "/native_bindings/simple.html"))); |
| |
| GURL expected_url = embedded_test_server()->GetURL( |
| "example.com", "/native_bindings/simple2.html"); |
| EXPECT_EQ(expected_url, web_contents->GetLastCommittedURL()); |
| } |
| |
| // Tests the context menu API, which includes calling sendRequest with an |
| // different signature than specified and using functions as properties on an |
| // object. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, ContextMenusTest) { |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest( |
| R"({ |
| "name": "Context menus", |
| "manifest_version": 2, |
| "version": "0.1", |
| "permissions": ["contextMenus"], |
| "background": { |
| "scripts": ["background.js"] |
| } |
| })"); |
| test_dir.WriteFile( |
| FILE_PATH_LITERAL("background.js"), |
| R"(chrome.contextMenus.create( |
| { |
| title: 'Context Menu Item', |
| onclick: () => { chrome.test.sendMessage('clicked'); }, |
| }, () => { chrome.test.sendMessage('registered'); });)"); |
| |
| const Extension* extension = nullptr; |
| { |
| ExtensionTestMessageListener listener("registered"); |
| extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| content::WebContents* web_contents = GetActiveWebContents(); |
| std::unique_ptr<TestRenderViewContextMenu> menu( |
| TestRenderViewContextMenu::Create(web_contents, |
| GURL("https://www.example.com"))); |
| |
| ExtensionTestMessageListener listener("clicked"); |
| int command_id = ContextMenuMatcher::ConvertToExtensionsCustomCommandId(0); |
| EXPECT_TRUE(menu->IsCommandIdEnabled(command_id)); |
| menu->ExecuteCommand(command_id, 0); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| // Tests that unchecked errors don't impede future calls. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, ErrorsInCallbackTest) { |
| embedded_test_server()->ServeFilesFromDirectory(test_data_dir_); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest( |
| R"({ |
| "name": "Errors In Callback", |
| "manifest_version": 2, |
| "version": "0.1", |
| "permissions": ["contextMenus"], |
| "background": { |
| "scripts": ["background.js"] |
| } |
| })"); |
| test_dir.WriteFile( |
| FILE_PATH_LITERAL("background.js"), |
| R"(chrome.tabs.query({}, function(tabs) { |
| chrome.tabs.executeScript(tabs[0].id, {code: 'x'}, function() { |
| // There's an error here (we don't have permission to access the |
| // host), but we don't check it so that it gets surfaced as an |
| // unchecked runtime.lastError. |
| // We should still be able to invoke other APIs and get correct |
| // callbacks. |
| chrome.tabs.query({}, function(tabs) { |
| chrome.tabs.query({}, function(tabs) { |
| chrome.test.sendMessage('callback'); |
| }); |
| }); |
| }); |
| });)"); |
| |
| ASSERT_TRUE( |
| NavigateToURL(GetActiveWebContents(), |
| embedded_test_server()->GetURL( |
| "example.com", "/native_bindings/simple.html"))); |
| |
| ExtensionTestMessageListener listener("callback"); |
| ASSERT_TRUE(LoadExtension(test_dir.UnpackedPath())); |
| EXPECT_TRUE(listener.WaitUntilSatisfied()); |
| } |
| |
| // Tests that bindings are available in WebUI pages. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, WebUIBindings) { |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, GURL("chrome://extensions"))); |
| |
| EXPECT_TRUE(ApiExists(web_contents, "chrome.developerPrivate")); |
| EXPECT_TRUE(ApiExists(web_contents, |
| "chrome.developerPrivate.getProfileConfiguration")); |
| EXPECT_TRUE(ApiExists(web_contents, "chrome.management")); |
| EXPECT_TRUE(ApiExists(web_contents, "chrome.management.setEnabled")); |
| EXPECT_FALSE(ApiExists(web_contents, "chrome.networkingPrivate")); |
| EXPECT_FALSE(ApiExists(web_contents, "chrome.sockets")); |
| EXPECT_FALSE(ApiExists(web_contents, "chrome.browserAction")); |
| } |
| |
| // Tests creating an API from a context that hasn't been initialized yet |
| // by doing so in a parent frame. Regression test for https://crbug.com/819968. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, APICreationFromNewContext) { |
| embedded_test_server()->ServeFilesFromDirectory(test_data_dir_); |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| ASSERT_TRUE(RunExtensionTest("native_bindings/context_initialization")) |
| << message_; |
| } |
| |
| // End-to-end test for promise support on bindings for MV3 extensions, using a |
| // few tabs APIs. Also ensures callbacks still work for the API as expected. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, PromiseBasedAPI) { |
| base::HistogramTester histogram_tester; |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest( |
| R"({ |
| "name": "Promises", |
| "manifest_version": 3, |
| "version": "0.1", |
| "background": { |
| "service_worker": "background.js" |
| }, |
| "permissions": ["tabs", "storage", "contentSettings", "privacy"] |
| })"); |
| constexpr char kBackgroundJs[] = |
| R"(let tabIdExample; |
| let tabIdGoogle; |
| |
| chrome.test.getConfig((config) => { |
| let exampleUrl = `https://example.com:${config.testServer.port}/`; |
| let googleUrl = `https://google.com:${config.testServer.port}/` |
| |
| chrome.test.runTests([ |
| function createNewTabPromise() { |
| let promise = chrome.tabs.create({url: exampleUrl}); |
| chrome.test.assertNoLastError(); |
| chrome.test.assertTrue(promise instanceof Promise); |
| promise.then((tab) => { |
| let url = tab.pendingUrl; |
| chrome.test.assertEq(exampleUrl, url); |
| tabIdExample = tab.id; |
| chrome.test.assertNoLastError(); |
| chrome.test.succeed(); |
| }); |
| }, |
| function queryTabPromise() { |
| let promise = chrome.tabs.query({url: exampleUrl}); |
| chrome.test.assertNoLastError(); |
| chrome.test.assertTrue(promise instanceof Promise); |
| promise.then((tabs) => { |
| chrome.test.assertTrue(tabs instanceof Array); |
| chrome.test.assertEq(1, tabs.length); |
| chrome.test.assertEq(tabIdExample, tabs[0].id); |
| chrome.test.assertNoLastError(); |
| chrome.test.succeed(); |
| }); |
| }, |
| async function storageAreaCustomTypeWithPromises() { |
| await chrome.storage.local.set({foo: 'bar', alpha: 'beta'}); |
| { |
| const {foo} = await chrome.storage.local.get('foo'); |
| chrome.test.assertEq('bar', foo); |
| } |
| await chrome.storage.local.remove('foo'); |
| { |
| const {foo} = await chrome.storage.local.get('foo'); |
| chrome.test.assertEq(undefined, foo); |
| } |
| let allValues = await chrome.storage.local.get(null); |
| chrome.test.assertEq({alpha: 'beta'}, allValues); |
| await chrome.storage.local.clear(); |
| allValues = await chrome.storage.local.get(null); |
| chrome.test.assertEq({}, allValues); |
| chrome.test.succeed(); |
| }, |
| async function contentSettingsCustomTypesWithPromises() { |
| await chrome.contentSettings.cookies.set({ |
| primaryPattern: '<all_urls>', setting: 'block'}); |
| { |
| const {setting} = await chrome.contentSettings.cookies.get({ |
| primaryUrl: exampleUrl}); |
| chrome.test.assertEq('block', setting); |
| } |
| await chrome.contentSettings.cookies.clear({}); |
| { |
| const {setting} = await chrome.contentSettings.cookies.get({ |
| primaryUrl: exampleUrl}); |
| // 'allow' is the default value for the setting. |
| chrome.test.assertEq('allow', setting); |
| } |
| chrome.test.succeed(); |
| }, |
| async function chromeSettingCustomTypesWithPromises() { |
| // Short alias for ease of calling. |
| let doNotTrack = chrome.privacy.websites.doNotTrackEnabled; |
| await doNotTrack.set({value: true}); |
| { |
| const {value} = await doNotTrack.get({}); |
| chrome.test.assertEq(true, value); |
| } |
| await doNotTrack.clear({}); |
| { |
| const {value} = await doNotTrack.get({}); |
| // false is the default value for the setting. |
| chrome.test.assertEq(false, value); |
| } |
| chrome.test.succeed(); |
| }, |
| |
| |
| function createNewTabCallback() { |
| chrome.tabs.create({url: googleUrl}, (tab) => { |
| let url = tab.pendingUrl; |
| chrome.test.assertEq(googleUrl, url); |
| tabIdGoogle = tab.id; |
| chrome.test.assertNoLastError(); |
| chrome.test.succeed(); |
| }); |
| }, |
| function queryTabCallback() { |
| chrome.tabs.query({url: googleUrl}, (tabs) => { |
| chrome.test.assertTrue(tabs instanceof Array); |
| chrome.test.assertEq(1, tabs.length); |
| chrome.test.assertEq(tabIdGoogle, tabs[0].id); |
| chrome.test.assertNoLastError(); |
| chrome.test.succeed(); |
| }); |
| }, |
| function storageAreaCustomTypeWithCallbacks() { |
| // Lots of stuff would probably fail if the callback version of |
| // storage failed, so this is mostly just a rough sanity check. |
| chrome.storage.local.set({gamma: 'delta'}, () => { |
| chrome.storage.local.get('gamma', ({gamma}) => { |
| chrome.test.assertEq('delta', gamma); |
| chrome.storage.local.clear(() => { |
| chrome.storage.local.get(null, (allValues) => { |
| chrome.test.assertEq({}, allValues); |
| chrome.test.succeed(); |
| }); |
| }); |
| }); |
| }); |
| }, |
| ]); |
| });)"; |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| ResultCatcher catcher; |
| ASSERT_TRUE(LoadExtension(test_dir.UnpackedPath())); |
| ASSERT_TRUE(catcher.GetNextResult()) << catcher.message(); |
| |
| // The above test makes 2 calls to chrome.tabs.create, so check that those |
| // have been logged in the histograms we expect them to be. |
| EXPECT_EQ(2, histogram_tester.GetBucketCount( |
| "Extensions.Functions.ExtensionCalls", |
| functions::HistogramValue::TABS_CREATE)); |
| EXPECT_EQ(2, histogram_tester.GetBucketCount( |
| "Extensions.Functions.ExtensionServiceWorkerCalls", |
| functions::HistogramValue::TABS_CREATE)); |
| EXPECT_EQ(2, histogram_tester.GetBucketCount( |
| "Extensions.Functions.ExtensionMV3Calls", |
| functions::HistogramValue::TABS_CREATE)); |
| } |
| |
| // Tests that calling an API which supports promises using an MV2 extension does |
| // not get a promise based return and still needs to use callbacks when |
| // required. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsApiTest, MV2PromisesNotSupported) { |
| base::HistogramTester histogram_tester; |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest( |
| R"({ |
| "name": "Promises", |
| "manifest_version": 2, |
| "version": "0.1", |
| "background": { |
| "scripts": ["background.js"] |
| }, |
| "permissions": ["tabs", "storage", "contentSettings", "privacy"] |
| })"); |
| constexpr char kBackgroundJs[] = |
| R"(let tabIdGooge; |
| |
| chrome.test.getConfig((config) => { |
| let exampleUrl = `https://example.com:${config.testServer.port}/`; |
| let googleUrl = `https://google.com:${config.testServer.port}/` |
| |
| chrome.test.runTests([ |
| function createNewTabPromise() { |
| let result = chrome.tabs.create({url: exampleUrl}); |
| chrome.test.assertEq(undefined, result); |
| chrome.test.assertNoLastError(); |
| chrome.test.succeed(); |
| }, |
| function queryTabPromise() { |
| let expectedError = 'Error in invocation of tabs.query(object ' + |
| 'queryInfo, function callback): No matching signature.'; |
| chrome.test.assertThrows(chrome.tabs.query, |
| [{url: exampleUrl}], |
| expectedError); |
| chrome.test.succeed(); |
| }, |
| function storageAreaPromise() { |
| let expectedError = 'Error in invocation of storage.get(' + |
| 'optional [string|array|object] keys, function callback): ' + |
| 'No matching signature.'; |
| chrome.test.assertThrows(chrome.storage.local.get, |
| chrome.storage.local, |
| ['foo'], expectedError); |
| chrome.test.succeed(); |
| }, |
| function contentSettingPromise() { |
| let expectedError = 'Error in invocation of contentSettings' + |
| '.ContentSetting.get(object details, function callback): ' + |
| 'No matching signature.'; |
| chrome.test.assertThrows(chrome.contentSettings.cookies.get, |
| chrome.contentSettings.cookies, |
| [{primaryUrl: exampleUrl}], |
| expectedError); |
| chrome.test.succeed(); |
| }, |
| function chromeSettingPromise() { |
| let expectedError = 'Error in invocation of types' + |
| '.ChromeSetting.get(object details, function callback): ' + |
| 'No matching signature.'; |
| chrome.test.assertThrows( |
| chrome.privacy.websites.doNotTrackEnabled.get, |
| chrome.privacy.websites.doNotTrackEnabled, |
| [{}], |
| expectedError); |
| chrome.test.succeed(); |
| }, |
| function createNewTabCallback() { |
| chrome.tabs.create({url: googleUrl}, (tab) => { |
| let url = tab.pendingUrl; |
| chrome.test.assertEq(googleUrl, url); |
| tabIdGoogle = tab.id; |
| chrome.test.assertNoLastError(); |
| chrome.test.succeed(); |
| }); |
| }, |
| function queryTabCallback() { |
| chrome.tabs.query({url: googleUrl}, (tabs) => { |
| chrome.test.assertTrue(tabs instanceof Array); |
| chrome.test.assertEq(1, tabs.length); |
| chrome.test.assertEq(tabIdGoogle, tabs[0].id); |
| chrome.test.assertNoLastError(); |
| chrome.test.succeed(); |
| }); |
| } |
| ]); |
| });)"; |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs); |
| ResultCatcher catcher; |
| ASSERT_TRUE(LoadExtension(test_dir.UnpackedPath())); |
| ASSERT_TRUE(catcher.GetNextResult()) << catcher.message(); |
| |
| // The above test makes 2 calls to chrome.tabs.create, so check that those |
| // have been logged in the histograms we expect, but not to the histograms |
| // specifically tracking service worker and MV3 calls. |
| EXPECT_EQ(2, histogram_tester.GetBucketCount( |
| "Extensions.Functions.ExtensionCalls", |
| functions::HistogramValue::TABS_CREATE)); |
| EXPECT_EQ(0, histogram_tester.GetBucketCount( |
| "Extensions.Functions.ExtensionServiceWorkerCalls", |
| functions::HistogramValue::TABS_CREATE)); |
| EXPECT_EQ(0, histogram_tester.GetBucketCount( |
| "Extensions.Functions.ExtensionMV3Calls", |
| functions::HistogramValue::TABS_CREATE)); |
| } |
| |
| class NativeBindingsBrowserNamespaceTest : public NativeBindingsApiTest { |
| public: |
| NativeBindingsBrowserNamespaceTest() { |
| scoped_feature_list_.InitAndEnableFeature( |
| extensions_features::kExtensionBrowserNamespaceAlternative); |
| } |
| |
| NativeBindingsBrowserNamespaceTest( |
| const NativeBindingsBrowserNamespaceTest&) = delete; |
| const NativeBindingsBrowserNamespaceTest& operator=( |
| const NativeBindingsBrowserNamespaceTest&) = delete; |
| ~NativeBindingsBrowserNamespaceTest() override = default; |
| |
| protected: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // Tests that extension background script contexts have access to |
| // `chrome.<extension_api>` and `browser.<extension_api>` objects. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsBrowserNamespaceTest, |
| ChromeAndBrowserObjects_ExtensionBackground) { |
| ASSERT_TRUE(RunExtensionTest("browser_object/background_context")) |
| << message_; |
| } |
| |
| // Tests that extension foreground script contexts (e.g. content script, |
| // extension page) have access to `chrome.<extension_api>` and |
| // `browser.<extension_api>` objects. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsBrowserNamespaceTest, |
| ChromeAndBrowserObjects_ExtensionForeground) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| const GURL& test_website = |
| embedded_test_server()->GetURL("a.com", "/title1.html"); |
| |
| const Extension* extension = LoadExtension( |
| test_data_dir_.AppendASCII("browser_object/foreground_context")); |
| ASSERT_TRUE(extension); |
| |
| // Content script. |
| ResultCatcher catcher; |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, test_website)); |
| ASSERT_TRUE(catcher.GetNextResult()) << catcher.message(); |
| |
| // Extension page. |
| ResultCatcher extension_resource_catcher; |
| ASSERT_TRUE(NavigateToURL( |
| web_contents, |
| GURL(extension->GetResourceURL("extension_resource_page.html")))); |
| ASSERT_TRUE(catcher.GetNextResult()) << catcher.message(); |
| } |
| |
| // Tests that an externally connectable webpage has access to |
| // `chrome.<extension_api>` and `browser.<extension_api>` objects. Additionally |
| // it tests that `chrome.app` is bound, but `browser.app` is not. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsBrowserNamespaceTest, |
| ChromeAndBrowserObjects_ExternallyConnectableWebpage) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| const GURL& test_website = |
| embedded_test_server()->GetURL("a.com", "/title1.html"); |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest( |
| R"({ |
| "name": "Externally connectable test extension", |
| "version": "0.1", |
| "manifest_version": 3, |
| "externally_connectable": { |
| "matches": ["*://a.com/*"] |
| } |
| })"); |
| test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), ""); |
| ASSERT_TRUE(LoadExtension(test_dir.UnpackedPath())); |
| |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, test_website)); |
| |
| EXPECT_TRUE(ApiExists(web_contents, "chrome.runtime")); |
| EXPECT_TRUE(ApiExists(web_contents, "browser.runtime")); |
| EXPECT_TRUE(ApiExists(web_contents, "chrome.app")); |
| EXPECT_FALSE(ApiExists(web_contents, "browser.app")); |
| } |
| |
| // Tests that the `browser` namespace is not available in WebUI. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsBrowserNamespaceTest, WebUIBindings) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, GURL("chrome://extensions"))); |
| |
| EXPECT_TRUE(ObjectIsDefined(web_contents, "chrome")); |
| EXPECT_FALSE(ObjectIsDefined(web_contents, "browser")); |
| } |
| |
| // Tests that an arbitrary web page with no extension API access has access to |
| // the `chrome` namespace, but not the browser namespace. |
| IN_PROC_BROWSER_TEST_F(NativeBindingsBrowserNamespaceTest, |
| TestNonExtensionChromeAndBrowserObjects) { |
| ASSERT_TRUE(StartEmbeddedTestServer()); |
| const GURL& test_website = |
| embedded_test_server()->GetURL("a.com", "/title1.html"); |
| auto* web_contents = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(web_contents, test_website)); |
| |
| EXPECT_TRUE(ObjectIsDefined(web_contents, "chrome")); |
| EXPECT_FALSE(ObjectIsDefined(web_contents, "browser")); |
| } |
| |
| // TODO(crbug.com/401226626): Test that the browser object also has dev mode |
| // restricted APIs set on correctly as well. |
| |
| class DeveloperModeNativeBindingsApiTest |
| : public NativeBindingsApiTest, |
| public testing::WithParamInterface<bool> { |
| public: |
| DeveloperModeNativeBindingsApiTest() { |
| if (GetParam()) { |
| // Ensure chrome.debugger is controlled by Developer Mode. |
| scoped_feature_list_.InitWithFeatures( |
| /*enabled_features=*/ |
| {extensions_features::kUserScriptUserExtensionToggle, |
| extensions_features::kDebuggerAPIRestrictedToDevMode}, |
| /*disabled_features=*/{}); |
| |
| } else { |
| // Ensure chrome.userScripts is controlled by Developer Mode. |
| scoped_feature_list_.InitWithFeatures( |
| /*enabled_features=*/{}, /*disabled_features=*/{ |
| extensions_features::kUserScriptUserExtensionToggle, |
| extensions_features::kDebuggerAPIRestrictedToDevMode}); |
| } |
| } |
| |
| private: |
| base::test::ScopedFeatureList scoped_feature_list_; |
| }; |
| |
| // TODO(crbug.com/390138269): Revert the user scripts specific testing once the |
| // extensions::kUserScriptUserExtensionToggle feature is launched. |
| IN_PROC_BROWSER_TEST_P( |
| DeveloperModeNativeBindingsApiTest, |
| DeveloperModeOnlyWithAPIPermissionUserIsNotInDeveloperMode) { |
| // Developer mode-only APIs should not be available if the user is not in |
| // developer mode. |
| SetCustomArg("not_in_developer_mode"); |
| util::SetDeveloperModeForProfile(profile(), false); |
| if (GetParam()) { |
| ASSERT_TRUE(RunExtensionTest( |
| "native_bindings/developer_mode_only_with_api_permission")) |
| << message_; |
| } else { |
| ASSERT_TRUE(RunExtensionTest( |
| "native_bindings/developer_mode_only_with_user_scripts_api_permission")) |
| << message_; |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P( |
| DeveloperModeNativeBindingsApiTest, |
| DeveloperModeOnlyWithAPIPermissionUserIsInDeveloperMode) { |
| // Developer mode-only APIs should be available if the user is in developer |
| // mode. |
| SetCustomArg("in_developer_mode"); |
| util::SetDeveloperModeForProfile(profile(), true); |
| if (GetParam()) { |
| ASSERT_TRUE(RunExtensionTest( |
| "native_bindings/developer_mode_only_with_api_permission")) |
| << message_; |
| } else { |
| ASSERT_TRUE(RunExtensionTest( |
| "native_bindings/developer_mode_only_with_user_scripts_api_permission")) |
| << message_; |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P( |
| DeveloperModeNativeBindingsApiTest, |
| DeveloperModeOnlyWithoutAPIPermissionUserIsNotInDeveloperMode) { |
| util::SetDeveloperModeForProfile(profile(), false); |
| if (GetParam()) { |
| ASSERT_TRUE(RunExtensionTest( |
| "native_bindings/developer_mode_only_without_api_permission")) |
| << message_; |
| } else { |
| ASSERT_TRUE(RunExtensionTest( |
| "native_bindings/" |
| "developer_mode_only_without_user_scripts_api_permission")) |
| << message_; |
| } |
| } |
| |
| IN_PROC_BROWSER_TEST_P( |
| DeveloperModeNativeBindingsApiTest, |
| DeveloperModeOnlyWithoutAPIPermissionUserIsInDeveloperMode) { |
| util::SetDeveloperModeForProfile(profile(), true); |
| if (GetParam()) { |
| ASSERT_TRUE(RunExtensionTest( |
| "native_bindings/developer_mode_only_without_api_permission")) |
| << message_; |
| } else { |
| ASSERT_TRUE(RunExtensionTest( |
| "native_bindings/" |
| "developer_mode_only_without_user_scripts_api_permission")) |
| << message_; |
| } |
| } |
| |
| // Tests that changing the developer mode setting affects existing renderers |
| // for page-based contexts (i.e., the main renderer thread). |
| IN_PROC_BROWSER_TEST_P(DeveloperModeNativeBindingsApiTest, |
| SwitchingDeveloperModeAffectsExistingRenderers_Pages) { |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "Test", |
| "manifest_version": 3, |
| "version": "0.1", |
| "permissions": ["%s"] |
| })"; |
| static constexpr char kPageHtml[] = |
| R"(<!doctype html> |
| <html> |
| <script src="page.js"></script> |
| </html>)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest( |
| base::StringPrintf(kManifest, GetParam() ? "debugger" : "userScripts")); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtml); |
| test_dir.WriteFile( |
| FILE_PATH_LITERAL("page.js"), |
| GetParam() ? kCheckApiAvailability : kCheckApiAvailabilityUserScripts); |
| test_dir.WriteFile(FILE_PATH_LITERAL("script.js"), "// blank"); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| const GURL extension_url = extension->GetResourceURL("page.html"); |
| |
| // Navigate to the extension page. |
| auto* existing_tab = GetActiveWebContents(); |
| ASSERT_TRUE(NavigateToURL(existing_tab, extension_url)); |
| ASSERT_EQ(extension_url, existing_tab->GetLastCommittedURL()); |
| |
| ScriptResultQueue result_queue; |
| |
| // By default, the API is unavailable. |
| ASSERT_TRUE(content::ExecJs(existing_tab, "verifyApiIsNotAvailable();")); |
| EXPECT_EQ("success", result_queue.GetNextResult()); |
| |
| // Next, set the user in developer mode. Now the API should be available. |
| util::SetDeveloperModeForProfile(profile(), true); |
| ASSERT_TRUE(content::ExecJs(existing_tab, "verifyApiIsAvailable();")); |
| EXPECT_EQ("success", result_queue.GetNextResult()); |
| |
| // Toggle back to not in developer mode. The API should be unavailable again. |
| util::SetDeveloperModeForProfile(profile(), false); |
| ASSERT_TRUE(content::ExecJs(existing_tab, "verifyApiIsNotAvailable();")); |
| EXPECT_EQ("success", result_queue.GetNextResult()); |
| } |
| |
| // Tests that incognito windows use the developer mode setting from the |
| // original, on-the-record profile (since incognito windows can't separately |
| // set developer mode). |
| IN_PROC_BROWSER_TEST_P(DeveloperModeNativeBindingsApiTest, |
| IncognitoRenderersUseOriginalProfilesDevModeSetting) { |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "Test", |
| "manifest_version": 3, |
| "version": "0.1", |
| "incognito": "split", |
| "permissions": ["%s"] |
| })"; |
| static constexpr char kPageHtml[] = |
| R"(<!doctype html> |
| <html> |
| <script src="page.js"></script> |
| </html>)"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest( |
| base::StringPrintf(kManifest, GetParam() ? "debugger" : "userScripts")); |
| test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtml); |
| test_dir.WriteFile( |
| FILE_PATH_LITERAL("page.js"), |
| GetParam() ? kCheckApiAvailability : kCheckApiAvailabilityUserScripts); |
| test_dir.WriteFile(FILE_PATH_LITERAL("script.js"), "// blank"); |
| |
| const Extension* extension = |
| LoadExtension(test_dir.UnpackedPath(), {.allow_in_incognito = true}); |
| ASSERT_TRUE(extension); |
| |
| const GURL extension_url = extension->GetResourceURL("page.html"); |
| |
| Browser* incognito_browser = OpenURLOffTheRecord(profile(), extension_url); |
| content::WebContents* incognito_tab = |
| incognito_browser->tab_strip_model()->GetActiveWebContents(); |
| content::WaitForLoadStop(incognito_tab); |
| |
| ScriptResultQueue result_queue; |
| |
| // By default, the API is unavailable. |
| ASSERT_TRUE(content::ExecJs(incognito_tab, "verifyApiIsNotAvailable();")); |
| EXPECT_EQ("success", result_queue.GetNextResult()); |
| |
| // Next, set the user in developer mode. Now the API should be available. |
| util::SetDeveloperModeForProfile(profile(), true); |
| ASSERT_TRUE(content::ExecJs(incognito_tab, "verifyApiIsAvailable();")); |
| EXPECT_EQ("success", result_queue.GetNextResult()); |
| |
| // Toggle back to not in developer mode. The API should be unavailable again. |
| util::SetDeveloperModeForProfile(profile(), false); |
| ASSERT_TRUE(content::ExecJs(incognito_tab, "verifyApiIsNotAvailable();")); |
| EXPECT_EQ("success", result_queue.GetNextResult()); |
| } |
| |
| // Tests that changing the developer mode setting affects existing renderers |
| // for service worker contexts (which run off the main thread in the renderer). |
| // TODO(crbug.com/40946312): Test flaky on multiple platforms |
| IN_PROC_BROWSER_TEST_P( |
| DeveloperModeNativeBindingsApiTest, |
| DISABLED_SwitchingDeveloperModeAffectsExistingRenderers_ServiceWorkers) { |
| static constexpr char kManifest[] = |
| R"({ |
| "name": "Test", |
| "manifest_version": 3, |
| "version": "0.1", |
| "permissions": ["%s"], |
| "background": {"service_worker": "background.js"} |
| })"; |
| |
| TestExtensionDir test_dir; |
| test_dir.WriteManifest( |
| base::StringPrintf(kManifest, GetParam() ? "debugger" : "userScripts")); |
| test_dir.WriteFile( |
| FILE_PATH_LITERAL("background.js"), |
| GetParam() ? kCheckApiAvailability : kCheckApiAvailabilityUserScripts); |
| test_dir.WriteFile(FILE_PATH_LITERAL("script.js"), "// blank"); |
| |
| const Extension* extension = LoadExtension(test_dir.UnpackedPath()); |
| ASSERT_TRUE(extension); |
| |
| auto call_in_service_worker = [this, extension](const std::string& script) { |
| return BackgroundScriptExecutor::ExecuteScript( |
| profile(), extension->id(), script, |
| BackgroundScriptExecutor::ResultCapture::kSendScriptResult); |
| }; |
| |
| auto renderer_round_trip = [this, extension]() { |
| EXPECT_EQ("success", |
| BackgroundScriptExecutor::ExecuteScript( |
| profile(), extension->id(), |
| "chrome.test.sendScriptResult('success');", |
| BackgroundScriptExecutor::ResultCapture::kSendScriptResult)); |
| }; |
| |
| // By default, the API is unavailable. |
| EXPECT_EQ("success", call_in_service_worker("verifyApiIsNotAvailable();")); |
| |
| // Next, set the user in developer mode. Now the API should be available. |
| util::SetDeveloperModeForProfile(profile(), true); |
| // We need to give the renderer time to do a few thread hops since there are |
| // multiple IPC channels at play (unlike the test above). Do a round-trip to |
| // the renderer to allow it to process. |
| renderer_round_trip(); |
| EXPECT_EQ("success", call_in_service_worker("verifyApiIsAvailable();")); |
| |
| // Toggle back to not in developer mode. The API should be unavailable again. |
| util::SetDeveloperModeForProfile(profile(), false); |
| renderer_round_trip(); |
| EXPECT_EQ("success", call_in_service_worker("verifyApiIsNotAvailable();")); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| DeveloperModeNativeBindingsApiTest, |
| // extensions_features::kDebuggerAPIRestrictedToDevMode |
| testing::Bool()); |
| |
| } // namespace extensions |