blob: d67deac343da7992f2e273a0300ae1524cd06f9d [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/test/test_future.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/background_script_executor.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/script_result_queue.h"
#include "extensions/browser/service_worker_task_queue.h"
#include "extensions/common/extension.h"
#include "extensions/common/mojom/manifest.mojom.h"
#include "extensions/test/extension_background_page_waiter.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
namespace extensions {
// Tests related to the registration state of extension background service
// workers.
class ServiceWorkerRegistrationApiTest : public ExtensionApiTest {
public:
ServiceWorkerRegistrationApiTest() = default;
~ServiceWorkerRegistrationApiTest() override = default;
// Retrieves the registration state of the service worker for the given
// `extension` from the //content layer.
content::ServiceWorkerCapability GetServiceWorkerRegistrationState(
const Extension& extension) {
const GURL& root_scope = extension.url();
const blink::StorageKey storage_key =
blink::StorageKey::CreateFirstParty(extension.origin());
base::test::TestFuture<content::ServiceWorkerCapability> future;
content::ServiceWorkerContext* service_worker_context =
util::GetStoragePartitionForExtensionId(extension.id(), profile())
->GetServiceWorkerContext();
service_worker_context->CheckHasServiceWorker(root_scope, storage_key,
future.GetCallback());
return future.Get();
}
};
// TODO(devlin): There's overlap with service_worker_apitest.cc in this file,
// and other tests in that file that should go here so that it's less
// monolithic.
// Tests that a service worker registration is properly stored after extension
// installation, both at the content layer and in the cached state in the
// extensions layer.
IN_PROC_BROWSER_TEST_F(ServiceWorkerRegistrationApiTest,
ServiceWorkerIsProperlyRegisteredAfterInstallation) {
static constexpr char kManifest[] =
R"({
"name": "Extension",
"manifest_version": 3,
"version": "0.1",
"background": {"service_worker": "background.js"}
})";
static constexpr char kBackground[] = "// Blank";
TestExtensionDir extension_dir;
extension_dir.WriteManifest(kManifest);
extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackground);
const Extension* extension = LoadExtension(
extension_dir.UnpackedPath(), {.wait_for_registration_stored = true});
ASSERT_TRUE(extension);
ServiceWorkerTaskQueue* task_queue = ServiceWorkerTaskQueue::Get(profile());
ASSERT_TRUE(task_queue);
base::Version stored_version =
task_queue->RetrieveRegisteredServiceWorkerVersion(extension->id());
ASSERT_TRUE(stored_version.IsValid());
EXPECT_EQ("0.1", stored_version.GetString());
EXPECT_EQ(content::ServiceWorkerCapability::SERVICE_WORKER_NO_FETCH_HANDLER,
GetServiceWorkerRegistrationState(*extension));
}
// Tests that updating an unpacked extension properly updates the extension's
// service worker.
IN_PROC_BROWSER_TEST_F(ServiceWorkerRegistrationApiTest,
UpdatingUnpackedExtensionUpdatesServiceWorker) {
static constexpr char kManifest[] =
R"({
"name": "Extension",
"manifest_version": 3,
"version": "0.1",
"background": {"service_worker": "background.js"}
})";
static constexpr char kBackgroundV1[] = "self.currentVersion = 1;";
static constexpr char kBackgroundV2[] =
R"(self.currentVersion = 2;
chrome.test.sendMessage('ready');)";
TestExtensionDir extension_dir;
extension_dir.WriteManifest(kManifest);
extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundV1);
const Extension* extension = LoadExtension(
extension_dir.UnpackedPath(), {.wait_for_registration_stored = true});
ASSERT_TRUE(extension);
EXPECT_EQ(mojom::ManifestLocation::kUnpacked, extension->location());
const ExtensionId id = extension->id();
auto get_version_flag = [this, id]() {
static constexpr char kScript[] =
R"(chrome.test.sendScriptResult(
self.currentVersion ? self.currentVersion : -1);)";
return BackgroundScriptExecutor::ExecuteScript(
profile(), id, kScript,
BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
};
EXPECT_EQ(base::Value(1), get_version_flag());
// Unlike `LoadExtension()`, `ReloadExtension()` doesn't automatically wait
// for the service worker to be ready, so we need to wait for a message to
// come in signaling it's complete.
ExtensionTestMessageListener listener("ready");
// Update the background script file and reload the extension. This results in
// the extension effectively being updated.
extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundV2);
ReloadExtension(id);
ASSERT_TRUE(listener.WaitUntilSatisfied());
// Note: `extension` is unsafe to use here since the extension has been
// reloaded.
EXPECT_EQ(base::Value(2), get_version_flag());
}
// Tests that updating an unpacked extension properly updates the extension's
// service worker.
IN_PROC_BROWSER_TEST_F(ServiceWorkerRegistrationApiTest,
UpdatingPackedExtensionUpdatesServiceWorker) {
static constexpr char kManifestV1[] =
R"({
"name": "Extension",
"manifest_version": 3,
"version": "0.1",
"background": {"service_worker": "background.js"}
})";
static constexpr char kManifestV2[] =
R"({
"name": "Extension",
"manifest_version": 3,
"version": "0.2",
"background": {"service_worker": "background.js"}
})";
// The `InstallExtension()` and `UpdateExtension()` methods don't wait for
// the service worker to be ready, so each background script needs a message
// to indicate it's done.
static constexpr char kBackgroundV1[] =
R"(self.currentVersion = 1;
chrome.test.sendMessage('ready');)";
static constexpr char kBackgroundV2[] =
R"(self.currentVersion = 2;
chrome.test.sendMessage('ready');)";
TestExtensionDir extension_dir;
extension_dir.WriteManifest(kManifestV1);
extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundV1);
const Extension* extension = nullptr;
{
ExtensionTestMessageListener listener("ready");
extension = InstallExtension(extension_dir.Pack(), 1);
ASSERT_TRUE(listener.WaitUntilSatisfied());
ASSERT_TRUE(extension);
EXPECT_EQ(mojom::ManifestLocation::kInternal, extension->location());
}
const ExtensionId id = extension->id();
auto get_version_flag = [this, id]() {
static constexpr char kScript[] =
R"(chrome.test.sendScriptResult(
self.currentVersion ? self.currentVersion : -1);)";
return BackgroundScriptExecutor::ExecuteScript(
profile(), id, kScript,
BackgroundScriptExecutor::ResultCapture::kSendScriptResult);
};
EXPECT_EQ(base::Value(1), get_version_flag());
// Update the background script file, re-pack the extension, and update the
// installation. The service worker should remain registered and be properly
// updated.
extension_dir.WriteManifest(kManifestV2);
extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundV2);
{
ExtensionTestMessageListener listener("ready");
extension = UpdateExtension(id, extension_dir.Pack(), 0);
ASSERT_TRUE(listener.WaitUntilSatisfied());
EXPECT_EQ(mojom::ManifestLocation::kInternal, extension->location());
EXPECT_EQ("0.2", extension->version().GetString());
EXPECT_EQ(id, extension->id());
}
EXPECT_EQ(base::Value(2), get_version_flag());
}
// Tests that the service worker is properly unregistered when the extension is
// disabled or uninstalled.
// TODO(crbug.com/1446468): Flaky on multiple platforms.
IN_PROC_BROWSER_TEST_F(
ServiceWorkerRegistrationApiTest,
DISABLED_DisablingOrUninstallingAnExtensionUnregistersTheServiceWorker) {
static constexpr char kManifest[] =
R"({
"name": "Extension",
"manifest_version": 3,
"version": "0.1",
"background": {"service_worker": "background.js"}
})";
static constexpr char kBackground[] = "chrome.test.sendMessage('ready');";
TestExtensionDir extension_dir;
extension_dir.WriteManifest(kManifest);
extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackground);
// `LoadExtension()` waits for the service worker to be ready; no need to
// listen to the "ready" message.
const Extension* extension = LoadExtension(
extension_dir.UnpackedPath(), {.wait_for_registration_stored = true});
ASSERT_TRUE(extension);
// Disable the extension. The service worker should be unregistered.
DisableExtension(extension->id());
EXPECT_EQ(content::ServiceWorkerCapability::NO_SERVICE_WORKER,
GetServiceWorkerRegistrationState(*extension));
// Re-enable the extension. The service worker should be re-registered.
ExtensionTestMessageListener listener("ready");
EnableExtension(extension->id());
ASSERT_TRUE(listener.WaitUntilSatisfied());
EXPECT_EQ(content::ServiceWorkerCapability::SERVICE_WORKER_NO_FETCH_HANDLER,
GetServiceWorkerRegistrationState(*extension));
// Next, uninstall the extension. The worker should be unregistered again.
// We need to grab a reference to the extension here so that the object
// doesn't get deleted.
scoped_refptr<const Extension> extension_ref = extension;
UninstallExtension(extension->id());
EXPECT_EQ(content::ServiceWorkerCapability::NO_SERVICE_WORKER,
GetServiceWorkerRegistrationState(*extension_ref));
}
// Verifies that a service worker registration associated with an extension's
// manifest cannot be removed via the `chrome.browsingData` API.
// Regression test for https://crbug.com/1392498.
IN_PROC_BROWSER_TEST_F(ServiceWorkerRegistrationApiTest,
RegistrationCannotBeRemovedByBrowsingDataAPI) {
// Load two extensions: one with a service worker-based background context and
// a second with access to the browsingData API.
static constexpr char kServiceWorkerManifest[] =
R"({
"name": "Service Worker Extension",
"manifest_version": 3,
"version": "0.1",
"background": {"service_worker": "background.js"}
})";
static constexpr char kServiceWorkerBackground[] =
R"(chrome.tabs.onCreated.addListener(tab => {
chrome.test.sendMessage('received event');
});)";
TestExtensionDir service_worker_extension_dir;
service_worker_extension_dir.WriteManifest(kServiceWorkerManifest);
service_worker_extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
kServiceWorkerBackground);
static constexpr char kBrowsingDataManifest[] =
R"({
"name": "Browsing Data Remover",
"manifest_version": 3,
"version": "0.1",
"permissions": ["browsingData"]
})";
static constexpr char kClearDataJs[] =
R"(chrome.test.runTests([
async function clearServiceWorkers() {
// From the extension's perspective, this call should succeed (it
// will remove any service workers for extensions that aren't the
// root-scoped background service worker).
await chrome.browsingData.removeServiceWorkers(
{originTypes: {extension: true}});
chrome.test.succeed();
},
]);)";
TestExtensionDir browsing_data_extension_dir;
browsing_data_extension_dir.WriteManifest(kBrowsingDataManifest);
browsing_data_extension_dir.WriteFile(
FILE_PATH_LITERAL("clear_data.html"),
R"(<html><script src="clear_data.js"></script></html>)");
browsing_data_extension_dir.WriteFile(FILE_PATH_LITERAL("clear_data.js"),
kClearDataJs);
const Extension* service_worker_extension =
LoadExtension(service_worker_extension_dir.UnpackedPath(),
{.wait_for_registration_stored = true});
ASSERT_TRUE(service_worker_extension);
const Extension* browsing_data_extension =
LoadExtension(browsing_data_extension_dir.UnpackedPath());
ASSERT_TRUE(browsing_data_extension);
auto open_new_tab = [this](const GURL& url) {
ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
};
// Verify the initial state. The service worker-based extension should have a
// worker registered...
EXPECT_EQ(content::ServiceWorkerCapability::SERVICE_WORKER_NO_FETCH_HANDLER,
GetServiceWorkerRegistrationState(*service_worker_extension));
const GURL about_blank("about:blank");
// ... And the worker should be able to receive incoming events.
{
ExtensionTestMessageListener listener("received event");
open_new_tab(about_blank);
ASSERT_TRUE(listener.WaitUntilSatisfied());
}
// Open a page to the browsing data extension, which will trigger a call to
// the browsingData API to remove registered service workers for extensions.
{
ResultCatcher result_catcher;
open_new_tab(browsing_data_extension->GetResourceURL("clear_data.html"));
EXPECT_TRUE(result_catcher.GetNextResult());
}
// The removal above should *not* have resulted in the background service
// worker for the extension being removed (which would put the extension into
// a broken state). The only way to remove a service worker from an extension
// manifest is to uninstall the extension.
// The worker should still be registered, and should still receive new events.
EXPECT_EQ(content::ServiceWorkerCapability::SERVICE_WORKER_NO_FETCH_HANDLER,
GetServiceWorkerRegistrationState(*service_worker_extension));
{
ExtensionTestMessageListener listener("received event");
open_new_tab(about_blank);
ASSERT_TRUE(listener.WaitUntilSatisfied());
}
}
// Tests that modifying local files for an unpacked extension does not result
// in the service worker being seen as "updated" (which would result in a
// "waiting" service worker, violating expectations in the extensions system).
// https://crbug.com/1271154.
IN_PROC_BROWSER_TEST_F(ServiceWorkerRegistrationApiTest,
ModifyingLocalFilesForUnpackedExtensions) {
ASSERT_TRUE(StartEmbeddedTestServer());
const double kUpdateDelayInMilliseconds =
content::ServiceWorkerContext::GetUpdateDelay().InMillisecondsF();
// Assert that whatever our update delay is, it's less than 5 seconds. If it
// were more, the test would risk timing out. If we ever need to exceed this
// in practice, we could introduce a test setter for a different amount of
// time.
ASSERT_GE(5000, kUpdateDelayInMilliseconds);
static constexpr char kManifest[] =
R"({
"name": "Test",
"manifest_version": 3,
"version": "0.1",
"background": {"service_worker": "background.js"},
"permissions": ["storage"]
})";
static constexpr char kBackgroundTemplate[] =
R"(chrome.storage.local.onChanged.addListener((changes) => {
// Send a notification of the storage changing back to C++ after
// a delay long enough for the update check on the worker to trigger.
// This notification includes the "version" of the background script
// and the value of the storage bit.
setTimeout(() => {
chrome.test.sendScriptResult(
`storage changed version %d: count ${changes.count.newValue}`);
}, %f + 100);
});)";
// The following is a page that, when visited, sets a new (incrementing)
// value in the extension's storage. This should trigger the listener in the
// background service worker.
static constexpr char kPageHtml[] =
R"(<html><script src="page.js"></script></html>)";
static constexpr char kPageJs[] =
R"((async () => {
let {count} = await chrome.storage.local.get({count: 0});
++count;
await chrome.storage.local.set({count});
})();)";
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
test_dir.WriteFile(
FILE_PATH_LITERAL("background.js"),
base::StringPrintf(kBackgroundTemplate, 1, kUpdateDelayInMilliseconds));
test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtml);
test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kPageJs);
// Load the test extension. It's important it be unpacked, since packed
// extensions would normally be subject to content verification.
const Extension* extension = LoadExtension(
test_dir.UnpackedPath(), {.wait_for_registration_stored = true});
ASSERT_TRUE(extension);
EXPECT_EQ(extension->path(), test_dir.UnpackedPath());
EXPECT_EQ(mojom::ManifestLocation::kUnpacked, extension->location());
const GURL page_url = extension->GetResourceURL("page.html");
auto open_tab_and_get_result = [this, page_url]() {
ScriptResultQueue result_queue;
// Open the page in a new tab. We use a new tab here since any tabs open to
// an extension page will be closed later in the test when the extension
// reloads, and we need to make sure there's at least one tab left in the
// browser.
EXPECT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
browser(), page_url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP));
return result_queue.GetNextResult();
};
// Visit the page. The service worker listener should fire the first time.
EXPECT_EQ("storage changed version 1: count 1", open_tab_and_get_result());
// Stop the service worker.
browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(),
extension->id());
// Verify any pending tasks from stopping fully finish.
base::RunLoop().RunUntilIdle();
// Rewrite the extension service worker and update the "version" flag in the
// background service worker.
test_dir.WriteFile(
FILE_PATH_LITERAL("background.js"),
base::StringPrintf(kBackgroundTemplate, 2, kUpdateDelayInMilliseconds));
// Visit the page again. This should reawaken the extension service worker.
EXPECT_EQ("storage changed version 1: count 2", open_tab_and_get_result());
// Run any pending tasks. This ensures that the update check, if one were
// going to happen, does.
content::RunAllTasksUntilIdle();
// Visit a third time. As above, the old version of the worker should be
// running.
EXPECT_EQ("storage changed version 1: count 3", open_tab_and_get_result());
// Reload the extension from disk.
ExtensionId extension_id = extension->id();
ReloadExtension(extension->id());
extension = extension_registry()->enabled_extensions().GetByID(extension_id);
ASSERT_TRUE(extension);
ExtensionBackgroundPageWaiter(profile(), *extension)
.WaitForBackgroundInitialized();
// Visit the page a fourth time. Now, the new service worker file should
// be used, since the extension was reloaded from disk.
EXPECT_EQ("storage changed version 2: count 4", open_tab_and_get_result());
}
} // namespace extensions