blob: 300d5977c2dbc0cae5611923bbf470d7f7606a13 [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 <memory>
#include <string>
#include "base/files/file_path.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/crx_installer.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/webui_url_constants.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/delayed_install_manager.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_util.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/script_executor.h"
#include "extensions/browser/script_result_queue.h"
#include "extensions/browser/service_worker/service_worker_task_queue.h"
#include "extensions/browser/service_worker/service_worker_test_utils.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_handlers/background_info.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"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace extensions {
using service_worker_test_utils::TestServiceWorkerTaskQueueObserver;
namespace {
constexpr char kNTPTestExtensionId[] = "ldnnhddmnhbkjipkidpdiheffobcpfmf";
enum class BackgroundType {
kPersistentPage,
kLazyPage,
};
// Convenience method for checking true/false counts of boolean histograms.
void CheckBooleanHistogramCounts(const char* histogram_name,
int true_count,
int false_count,
base::HistogramTester& histogram_tester) {
histogram_tester.ExpectBucketCount(histogram_name,
/*sample=*/true,
/*expected_count=*/true_count);
histogram_tester.ExpectBucketCount(histogram_name,
/*sample=*/false,
/*expected_count=*/false_count);
}
GURL new_tab_url() {
return GURL("chrome://newtab");
}
// Allows test to wait for the attempt to register and unregister a worker to
// complete. It does not guarantee (un)registration attempt success.
class ExtensionRegistrationAndUnregistrationWaiter
: public ServiceWorkerTaskQueue::TestObserver {
public:
explicit ExtensionRegistrationAndUnregistrationWaiter(
const ExtensionId extension_id)
: expected_extension_id_(extension_id) {
ServiceWorkerTaskQueue::SetObserverForTest(this);
}
~ExtensionRegistrationAndUnregistrationWaiter() override {
ServiceWorkerTaskQueue::SetObserverForTest(nullptr);
}
ExtensionRegistrationAndUnregistrationWaiter(
const ExtensionRegistrationAndUnregistrationWaiter&) = delete;
ExtensionRegistrationAndUnregistrationWaiter& operator=(
const ExtensionRegistrationAndUnregistrationWaiter&) = delete;
void WaitForWorkerRegistrationAttemptCompleted() {
SCOPED_TRACE("Waiting for worker registration attempt to complete");
registration_attempt_runloop.Run();
}
void WaitForWorkerRegistrationAndUnRegistrationAttemptCompleted() {
WaitForWorkerRegistrationAttemptCompleted();
WaitForWorkerUnregistrationAttemptCompleted();
}
private:
void WaitForWorkerUnregistrationAttemptCompleted() {
SCOPED_TRACE("Waiting for worker unregistration attempt to complete");
unregistration_attempt_runloop.Run();
}
void OnWorkerRegistered(const ExtensionId& extension_id) override {
if (extension_id == expected_extension_id_) {
registration_attempt_runloop.Quit();
}
}
void WorkerUnregistered(const ExtensionId& extension_id) override {
if (extension_id == expected_extension_id_) {
unregistration_attempt_runloop.Quit();
}
}
const ExtensionId expected_extension_id_;
base::RunLoop registration_attempt_runloop;
base::RunLoop unregistration_attempt_runloop;
};
} // namespace
// 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();
}
// Returns true if the extension with the specified `extension_id` has an
// active worker registered in the ProcessManager.
bool HasActiveServiceWorker(const ExtensionId& extension_id) {
ProcessManager* process_manager = ProcessManager::Get(profile());
std::vector<WorkerId> worker_ids =
process_manager->GetServiceWorkersForExtension(extension_id);
if (worker_ids.size() > 1) {
// We should never have more than one worker registered in the process
// manager for a given extension.
ADD_FAILURE() << "Muliple active worker IDs found for extension.";
return false;
}
return worker_ids.size() == 1;
}
// Returns the value of `self.currentVersion` in the background context of the
// extension with the given `extension_id`.
int GetVersionFlagFromBackgroundContext(const ExtensionId& extension_id) {
static constexpr char kScript[] =
R"(chrome.test.sendScriptResult(
self.currentVersion ? self.currentVersion : -1);)";
return BackgroundScriptExecutor::ExecuteScript(
profile(), extension_id, kScript,
BackgroundScriptExecutor::ResultCapture::kSendScriptResult)
.GetInt();
}
};
// 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 when an extension has invalid syntax in it's background worker
// script the registration is not considered a failure due to it being user
// error.
IN_PROC_BROWSER_TEST_F(ServiceWorkerRegistrationApiTest,
InvalidWorkerSyntaxFailsWorkerRegistration) {
const ExtensionId test_extension_id("iegclhlplifhodhkoafiokenjoapiobj");
static constexpr const char kKey[] =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjzv7dI7Ygyh67VHE1DdidudpYf8P"
"Ffv8iucWvzO+3xpF/Dm5xNo7aQhPNiEaNfHwJQ7lsp4gc+C+4bbaVewBFspTruoSJhZc5uEf"
"qxwovJwN+v1/SUFXTXQmQBv6gs0qZB4gBbl4caNQBlqrFwAMNisnu1V6UROna8rOJQ90D7Nv"
"7TCwoVPKBfVshpFjdDOTeBg4iLctO3S/06QYqaTDrwVceSyHkVkvzBY6tc6mnYX0RZu78J9i"
"L8bdqwfllOhs69cqoHHgrLdI6JdOyiuh6pBP6vxMlzSKWJ3YTNjaQTPwfOYaLMuzdl0v+Ydz"
"afIzV9zwe4Xiskk+5JNGt8b2rQIDAQAB";
static constexpr char kManifest[] =
R"({
"name": "TestExtension",
"manifest_version": 3,
"version": "0.1",
"key": "%s",
"background": {"service_worker": "background.js"}
})";
TestExtensionDir extension_dir;
extension_dir.WriteManifest(base::StringPrintf(kManifest, kKey));
extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
"invalid_js_syntax;");
base::HistogramTester histogram_tester;
ExtensionRegistrationAndUnregistrationWaiter registration_waiter(
test_extension_id);
ChromeTestExtensionLoader(profile()).LoadUnpackedExtensionAsync(
extension_dir.UnpackedPath(), base::DoNothing());
{
SCOPED_TRACE("waiting for extension registration to finish");
registration_waiter.WaitForWorkerRegistrationAttemptCompleted();
}
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerRegistrationState",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.Registration_FailStatus",
/*expected_count=*/0);
histogram_tester.ExpectBucketCount(
"Extensions.ServiceWorkerBackground.Registration_FailStatus",
/*sample=*/blink::ServiceWorkerStatusCode::kErrorScriptEvaluateFailed,
/*expected_count=*/0);
}
// 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);
base::HistogramTester histogram_tester;
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));
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.RegistrationTime",
/*expected_count=*/1);
// Confirm the time is a sane value (less than the maximum value).
histogram_tester.ExpectBucketCount(
"Extensions.ServiceWorkerBackground.RegistrationTime",
/*sample=*/base::Seconds(10).InMilliseconds(),
/*expected_count=*/0);
}
// 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();
EXPECT_EQ(base::Value(1), GetVersionFlagFromBackgroundContext(id));
// 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), GetVersionFlagFromBackgroundContext(id));
}
// Test what happens when installing/loading an extension, but then before the
// install (and worker registration) completes disable the extension (which
// tries to unregister the worker). Observes a failed registration and
// unregistration.
IN_PROC_BROWSER_TEST_F(ServiceWorkerRegistrationApiTest,
AttemptInstallAndImmediateDisable) {
const ExtensionId test_extension_id("iegclhlplifhodhkoafiokenjoapiobj");
static constexpr const char kKey[] =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjzv7dI7Ygyh67VHE1DdidudpYf8P"
"Ffv8iucWvzO+3xpF/Dm5xNo7aQhPNiEaNfHwJQ7lsp4gc+C+4bbaVewBFspTruoSJhZc5uEf"
"qxwovJwN+v1/SUFXTXQmQBv6gs0qZB4gBbl4caNQBlqrFwAMNisnu1V6UROna8rOJQ90D7Nv"
"7TCwoVPKBfVshpFjdDOTeBg4iLctO3S/06QYqaTDrwVceSyHkVkvzBY6tc6mnYX0RZu78J9i"
"L8bdqwfllOhs69cqoHHgrLdI6JdOyiuh6pBP6vxMlzSKWJ3YTNjaQTPwfOYaLMuzdl0v+Ydz"
"afIzV9zwe4Xiskk+5JNGt8b2rQIDAQAB";
static constexpr char kManifest[] =
R"({
"name": "TestExtension",
"manifest_version": 3,
"version": "0.1",
"key": "%s",
"background": {"service_worker": "background.js"}
})";
TestExtensionDir extension_dir;
extension_dir.WriteManifest(base::StringPrintf(kManifest, kKey));
extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), "");
// Load the extension, but then disable it as soon as possible.
ServiceWorkerTaskQueue* task_queue = ServiceWorkerTaskQueue::Get(profile());
ASSERT_TRUE(task_queue);
ExtensionRegistrationAndUnregistrationWaiter extension_registration_waiter(
test_extension_id);
base::HistogramTester histogram_tester;
std::optional<base::UnguessableToken> activation_token;
ChromeTestExtensionLoader(profile()).LoadUnpackedExtensionAsync(
extension_dir.UnpackedPath(),
base::BindLambdaForTesting([&](const extensions::Extension* extension) {
ASSERT_TRUE(extension);
// Confirm that an activation token was generated so we can later check
// for it to be removed.
activation_token =
task_queue->GetCurrentActivationToken(extension->id());
ASSERT_TRUE(activation_token.has_value());
// Disable the extension as soon as it's been added as enabled,
// which causes the worker unregistration request to be sent.
DisableExtension(extension->id());
}));
extension_registration_waiter
.WaitForWorkerRegistrationAndUnRegistrationAttemptCompleted();
// Registration considered success because we didn't allow the registration to
// complete before we disabled the extension. We can't check the failure
// status code because it's not consistent. I've seen
// blink::ServiceWorkerStatusCode::kErrorDisallowed, kErrorNetwork, and
// kErrorNotFound in manual testing.
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerRegistrationState",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
// Unregistration "succeeds" because the registration above did not complete.
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationFailureStatus",
/*expected_count=*/0);
// Confirm the activation token doesn't remain after disabling the
// failed-to-register extension.
activation_token = task_queue->GetCurrentActivationToken(test_extension_id);
EXPECT_FALSE(activation_token.has_value());
}
// Tests updating an extension and installing it immediately while it has an
// active new tab page override and a new tab is open.
// Regression test for https://crbug.com/1498035.
IN_PROC_BROWSER_TEST_F(ServiceWorkerRegistrationApiTest,
ImmediateUpdateWithNewTabPageOverrideActive) {
// An extension manifest with a service worker and a new tab page override.
// The new tab page override is important because:
// * It commits to the extension origin and can be claimed by the service
// worker as a client.
// * Unlike other chrome-extension:-scheme pages, we don't close the new
// tab page when the extension is unloaded, which means the client is
// still around when the worker is being re-registered.
static constexpr char kManifestWithNtpV1[] =
R"({
"name": "Extension",
"manifest_version": 3,
"version": "0.1",
"background": {"service_worker": "background.js"},
"action": {},
"chrome_url_overrides": {
"newtab": "page.html"
}
})";
static constexpr char kManifestWithNtpV2[] =
R"({
"name": "Extension",
"manifest_version": 3,
"version": "0.2",
"action": {},
"background": {"service_worker": "background.js"},
"chrome_url_overrides": {
"newtab": "page.html"
}
})";
// A background script that sends a message once the service worker is
// activated.
constexpr char kBackgroundV1[] =
R"(self.currentVersion = 1;
// Wait for the service worker to be active and claim any clients.
(async () => {
if (self.serviceWorker.state != 'activated') {
await new Promise(resolve => {
self.addEventListener('activate', resolve);
});
}
await clients.claim();
chrome.test.sendMessage('v1 ready');
})();)";
constexpr char kBackgroundV2[] = R"(self.currentVersion = 2;)";
static constexpr char kPageHtml[] = "<html>This is a page</html>";
// Write and package the two versions of the extension.
TestExtensionDir extension_dir;
extension_dir.WriteManifest(kManifestWithNtpV1);
extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundV1);
extension_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtml);
base::FilePath crx_v1 = extension_dir.Pack("v1.crx");
extension_dir.WriteManifest(kManifestWithNtpV2);
extension_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundV2);
base::FilePath crx_v2 = extension_dir.Pack("v2.crx");
// Load the first version of the extension.
const Extension* extension = nullptr;
{
ExtensionTestMessageListener listener("v1 ready");
extension = InstallExtension(crx_v1, 1);
ASSERT_TRUE(listener.WaitUntilSatisfied());
}
ASSERT_TRUE(extension);
EXPECT_EQ(mojom::ManifestLocation::kInternal, extension->location());
const ExtensionId id = extension->id();
EXPECT_TRUE(HasActiveServiceWorker(id));
// Open a new tab. The extension overrides the NTP, so this is the extension's
// page.
ASSERT_TRUE(NavigateToURLInNewTab(GURL("chrome://newtab/")));
EXPECT_EQ("This is a page", content::EvalJs(GetActiveWebContents(),
"document.body.innerText;"));
// Verify the service worker is at v1.
EXPECT_EQ(base::Value(1), GetVersionFlagFromBackgroundContext(id));
{
// Install v2. This will result in the extension updating. We set
// `install_immediately` to true so that the system won't wait for the
// extension to be idle to unload the old version and start the new one
// (since there's an active NTP that the extension overrides, it would
// never be idle and it's important for the test case to update the
// extension while there's an active client of the service worker).
// This also mimics update behavior if a user clicks "Update" in the
// chrome://extensions page.
scoped_refptr<CrxInstaller> crx_installer =
CrxInstaller::Create(profile(), /*client=*/nullptr);
crx_installer->set_error_on_unsupported_requirements(true);
crx_installer->set_off_store_install_allow_reason(
CrxInstaller::OffStoreInstallAllowedFromSettingsPage);
crx_installer->set_install_immediately(true);
base::test::TestFuture<std::optional<CrxInstallError>>
installer_done_future;
crx_installer->AddInstallerCallback(
installer_done_future
.GetCallback<const std::optional<CrxInstallError>&>());
TestServiceWorkerTaskQueueObserver worker_waiter;
crx_installer->InstallCrx(crx_v2);
// Wait for the install to finish and for the (new) service worker context
// to be initialized.
std::optional<CrxInstallError> install_error = installer_done_future.Get();
ASSERT_FALSE(install_error.has_value()) << install_error->message();
worker_waiter.WaitForWorkerContextInitialized(id);
}
// Grab the new version of the extension (the old one was replaced and is
// unsafe to use).
extension =
ExtensionRegistry::Get(profile())->enabled_extensions().GetByID(id);
ASSERT_TRUE(extension);
EXPECT_EQ(mojom::ManifestLocation::kInternal, extension->location());
EXPECT_EQ("0.2", extension->version().GetString());
EXPECT_EQ(id, extension->id());
EXPECT_TRUE(HasActiveServiceWorker(id));
// The service worker context should be that of the new version.
EXPECT_EQ(base::Value(2), GetVersionFlagFromBackgroundContext(id));
}
// 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/40268625): 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(NavigateToURLInNewTab(url));
};
// 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.
// TODO(crbug.com/355339195): Re-enable this test
#if BUILDFLAG(IS_LINUX) && defined(ADDRESS_SANITIZER)
#define MAYBE_ModifyingLocalFilesForUnpackedExtensions \
DISABLED_ModifyingLocalFilesForUnpackedExtensions
#else
#define MAYBE_ModifyingLocalFilesForUnpackedExtensions \
ModifyingLocalFilesForUnpackedExtensions
#endif
IN_PROC_BROWSER_TEST_F(ServiceWorkerRegistrationApiTest,
MAYBE_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(NavigateToURLInNewTab(page_url));
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());
}
class ServiceWorkerExtensionUpdateOnBrowserRestartRegistrationApiTest
: public ServiceWorkerRegistrationApiTest {
protected:
void SetUp() override {
// Usually browser test loads about:blank in the default tab for the window,
// but we want to ensure chrome://newtab is loaded instead. This ensures
// that chrome://newtab is loaded as soon as possible which is important to
// confirm the update works as expected.
set_open_about_blank_on_browser_launch(false);
// Create the observer now because the browser will be started after we call
// `ServiceWorkerRegistrationApiTest::SetUp()`.
browser_start_new_tab_observer_ =
std::make_unique<ui_test_utils::UrlLoadObserver>(new_tab_url());
ServiceWorkerRegistrationApiTest::SetUp();
}
void SetUpOnMainThread() override {
ServiceWorkerRegistrationApiTest::SetUpOnMainThread();
{
SCOPED_TRACE(
"waiting for the initial new tab to open after browser start");
browser_start_new_tab_observer_->Wait();
}
}
void CreatedBrowserMainParts(content::BrowserMainParts* main_parts) override {
// These are meant to be used in `NTPUpdateAcrossBrowserRestart` (after
// PRE_NTPUpdateAcrossBrowserRestart finishes). But we need them to be
// created before the browser starts in the test so we define them here.
v2_install_listener_ =
std::make_unique<ExtensionTestMessageListener>("v2 installed");
v2_update_histogram_tester_ = std::make_unique<base::HistogramTester>();
ServiceWorkerRegistrationApiTest::CreatedBrowserMainParts(main_parts);
}
void TearDownOnMainThread() override {
ServiceWorkerRegistrationApiTest::TearDownOnMainThread();
// Prevent dangling pointer on test teardown.
browser_start_new_tab_observer_.reset();
}
// Ensure any new tab that is opened defaults goes to chrome://newtab.
void SetUpCommandLine(base::CommandLine* command_line) override {
ServiceWorkerRegistrationApiTest::SetUpCommandLine(command_line);
command_line->AppendSwitchASCII(switches::kHomePage,
chrome::kChromeUINewTabURL);
}
// Get the NTP javascript's version.
content::EvalJsResult GetVersionOfNTPScript() {
return content::EvalJs(GetActiveWebContents(), "self.currentVersion;");
}
// Request the version of the background context script from the perspective
// of the NTP js.
content::EvalJsResult GetBackgroundContextVersionFromNTPPage() {
return content::EvalJs(GetActiveWebContents(),
"getCurrentVersionOfBackgroundContext();");
}
// Observes that chrome://newtab loads on test start.
std::unique_ptr<ui_test_utils::UrlLoadObserver>
browser_start_new_tab_observer_;
std::unique_ptr<ExtensionTestMessageListener> v2_install_listener_;
std::unique_ptr<base::HistogramTester> v2_update_histogram_tester_;
};
// Tests that updating an extension that overrides the NTP updates successfully
// across browser restart when the updated is delayed until shutdown. This PRE_
// test installs v1, updates to v2, but v2 does not install since v1 is not
// idle. At the end of the test the browser shuts down with the v2 update as a
// delayed install.
IN_PROC_BROWSER_TEST_F(
ServiceWorkerExtensionUpdateOnBrowserRestartRegistrationApiTest,
PRE_NTPUpdateAcrossBrowserRestart) {
// Browser should have started and opened a new tab to chrome://newtab.
// Install v1 of the extension and verify its background context is running
// the v1 version.
base::FilePath crx_v1_path = PackExtension(test_data_dir_.AppendASCII(
"service_worker/registration/ntp_extension/extension_version_1"));
const Extension* extension_v1 = nullptr;
{
ExtensionTestMessageListener v1_install_listener_("v1 installed");
extension_v1 = InstallExtension(crx_v1_path, /*expected_change=*/1);
SCOPED_TRACE("waiting for version 1 of the extension to install");
ASSERT_TRUE(v1_install_listener_.WaitUntilSatisfied());
ASSERT_EQ("1", extension_v1->version().GetString());
}
ASSERT_TRUE(extension_v1);
ASSERT_EQ(kNTPTestExtensionId, extension_v1->id());
EXPECT_TRUE(HasActiveServiceWorker(extension_v1->id()));
ASSERT_EQ(base::Value(1),
GetVersionFlagFromBackgroundContext(extension_v1->id()));
// Navigate current tab to new tab to engage v1 of the NTP extension to stay
// non-idle.
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), new_tab_url()));
// Verify v1 of extension is responding to messages in the tab.
std::u16string first_new_tab_title;
ui_test_utils::GetCurrentTabTitle(browser(), &first_new_tab_title);
ASSERT_EQ(u"Custom NTP test v1", first_new_tab_title);
ASSERT_EQ(base::Value(1), GetVersionOfNTPScript());
{
SCOPED_TRACE("waiting for v1 of background context to respond to NTP");
ASSERT_EQ(base::Value(1), GetBackgroundContextVersionFromNTPPage());
}
// v1 of extension is still installed because extension is not idle.
ASSERT_TRUE(HasActiveServiceWorker(extension_v1->id()));
ASSERT_EQ(base::Value(1),
GetVersionFlagFromBackgroundContext(extension_v1->id()));
// Attempt install of v2 of the extension.
base::FilePath crx_v2_path = PackExtension(test_data_dir_.AppendASCII(
"service_worker/registration/ntp_extension/extension_version_2"));
const Extension* extension_update = nullptr;
{
SCOPED_TRACE("waiting for update of v2 of extension");
extension_update =
UpdateExtensionWaitForIdle(kNTPTestExtensionId, crx_v2_path,
/*expected_change=*/0);
}
ASSERT_EQ(1u,
DelayedInstallManager::Get(profile())->delayed_installs().size());
// v2 won't install though since v1 isn't idle (NTP page is still open) yet so
// we're given the original `extension_v1` object.
ASSERT_TRUE(extension_update);
ASSERT_EQ(extension_update, extension_v1);
// Confirm v1 background context is still installed because extension is not
// idle.
ASSERT_TRUE(HasActiveServiceWorker(extension_v1->id()));
ASSERT_EQ(base::Value(1),
GetVersionFlagFromBackgroundContext(extension_v1->id()));
// Confirm the existing NTP we opened is still responding as v1 of the
// extension.
std::u16string second_new_tab_title;
ui_test_utils::GetCurrentTabTitle(browser(), &second_new_tab_title);
ASSERT_EQ(u"Custom NTP test v1", second_new_tab_title);
ASSERT_EQ(base::Value(1), GetVersionOfNTPScript());
{
SCOPED_TRACE("waiting for v1 of background context to respond to NTP");
ASSERT_EQ(base::Value(1), GetBackgroundContextVersionFromNTPPage());
}
// Navigate again to new tab so we can confirm v1 is still running and v2
// hasn't taken over future new tabs.
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), new_tab_url()));
std::u16string third_new_tab_title;
ui_test_utils::GetCurrentTabTitle(browser(), &third_new_tab_title);
ASSERT_EQ(u"Custom NTP test v1", third_new_tab_title);
ASSERT_EQ(base::Value(1), GetVersionOfNTPScript());
{
SCOPED_TRACE("waiting for v1 of background context to respond to NTP");
ASSERT_EQ(base::Value(1), GetBackgroundContextVersionFromNTPPage());
}
// Shut down the browser (test ending causes browser to shut down).
}
// TODO(https://crbug.com/330066242): Flakes on chromeOS.
// ui_test_utils::NavigateToURL() causes: [focus_controller.cc(277)] Check
// failed: rules_->CanFocusWindow(window, nullptr).
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_NTPUpdateAcrossBrowserRestart \
DISABLED_NTPUpdateAcrossBrowserRestart
#else
#define MAYBE_NTPUpdateAcrossBrowserRestart NTPUpdateAcrossBrowserRestart
#endif
// Continues the PRE_ test by testing that upon browser start v2 of the
// extension is installed.
IN_PROC_BROWSER_TEST_F(
ServiceWorkerExtensionUpdateOnBrowserRestartRegistrationApiTest,
MAYBE_NTPUpdateAcrossBrowserRestart) {
// Browser should have started and opened a new tab to chrome://newtab.
{
SCOPED_TRACE(
"waiting for install of extension to v2 after browser restart");
// Verify new extension background context is now active installed.
EXPECT_TRUE(v2_install_listener_->WaitUntilSatisfied());
}
// Confirm that v2 of extension is now installed.
const Extension* extension_v2 =
ExtensionRegistry::Get(profile())->GetExtensionById(
kNTPTestExtensionId, ExtensionRegistry::ENABLED);
ASSERT_TRUE(extension_v2);
ASSERT_EQ("2", extension_v2->version().GetString());
EXPECT_EQ(base::Value(2),
GetVersionFlagFromBackgroundContext(extension_v2->id()));
// Verify that v2 of extension is responding to NTP requests.
std::u16string browser_start_new_tab_title;
ui_test_utils::GetCurrentTabTitle(browser(), &browser_start_new_tab_title);
ASSERT_EQ(u"Custom NTP test v2", browser_start_new_tab_title);
EXPECT_EQ(base::Value(2), GetVersionOfNTPScript());
{
SCOPED_TRACE("waiting for v2 of background context to respond to NTP");
ASSERT_EQ(base::Value(2), GetBackgroundContextVersionFromNTPPage());
}
// Navigate to new tab page so we can confirm v2 is still running and v1
// hasn't taken over future new tabs loads.
ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), new_tab_url()));
std::u16string new_tab_title;
ui_test_utils::GetCurrentTabTitle(browser(), &new_tab_title);
ASSERT_EQ(u"Custom NTP test v2", new_tab_title);
ASSERT_EQ(base::Value(2), GetVersionOfNTPScript());
{
SCOPED_TRACE("waiting for v2 of background context to respond to NTP");
ASSERT_EQ(base::Value(2), GetBackgroundContextVersionFromNTPPage());
}
// TODO(crbug.com/353738494): Let's make it so that vN of extension is not
// redundantly installed on start when delayed install of vN+1 is ready.
// v1 worker is unregistered twice because on browser start v1 of extension is
// installed first, then delayed installs are installed. This causes us to
// deactivate v1 of the extension since it has a worker task queue (which
// unregisters v1 of the worker), then we unregister again because v1 of the
// extension is loaded.
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState",
/*true_count=*/2, /*false_count=*/0, *v2_update_histogram_tester_);
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState_"
"DeactivateExtension",
/*true_count=*/1, /*false_count=*/0, *v2_update_histogram_tester_);
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState_"
"AddExtension",
/*true_count=*/1, /*false_count=*/0, *v2_update_histogram_tester_);
// Then v2 extension worker is registered.
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerRegistrationState",
/*true_count=*/1, /*false_count=*/0, *v2_update_histogram_tester_);
v2_update_histogram_tester_->ExpectTotalCount(
"Extensions.ServiceWorkerBackground.Registration_FailStatus",
/*expected_count=*/0);
}
// Registration and unregistration metrics tests.
// TODO(crbug.com/346732739): Add tests for extension updates from:
// * non-sw background to sw background
// * sw registered manually via web API to sw background
// * sw background context to sw background context
class ServiceWorkerManifestVersionBrowserTest
: public ExtensionBrowserTest,
public testing::WithParamInterface<int> {
public:
void InstallMv2OrMv3Extension() {
const char* test_extension_subpath;
if (GetParam() == 2) {
test_extension_subpath = "service_worker/registration/mv2_service_worker";
} else if (GetParam() == 3) {
test_extension_subpath = "service_worker/registration/mv3_service_worker";
} else {
FAIL() << "Invalid test parameter: \"" << GetParam()
<< "\" manifest version must be 2 or 3.";
}
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII(test_extension_subpath),
{.wait_for_registration_stored = true});
ASSERT_TRUE(extension);
extension_ = extension;
}
void ReleaseExtension() { extension_ = nullptr; }
const Extension* extension() const { return extension_.get(); }
protected:
void TearDownOnMainThread() override {
extension_ = nullptr;
ExtensionBrowserTest::TearDownOnMainThread();
}
private:
raw_ptr<const Extension> extension_;
};
using ServiceWorkerRegistrationInstallMetricBrowserTest =
ServiceWorkerManifestVersionBrowserTest;
// Tests that installing an extension emits metrics for registering the service
// worker.
IN_PROC_BROWSER_TEST_P(ServiceWorkerRegistrationInstallMetricBrowserTest,
ExtensionInstall) {
base::HistogramTester histogram_tester;
ASSERT_NO_FATAL_FAILURE(InstallMv2OrMv3Extension());
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerRegistrationState",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState",
/*expected_count=*/0);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.Registration_FailStatus",
/*expected_count=*/0);
}
// Tracks when a worker is registered in the //content layer.
class ServiceWorkerTaskQueueRegistrationObserver
: public ServiceWorkerTaskQueue::TestObserver {
public:
explicit ServiceWorkerTaskQueueRegistrationObserver(
const ExtensionId& extension_id)
: extension_id_(extension_id) {}
void WaitForWorkerUnregistered() { unregister_loop.Run(); }
void WaitForWorkerRegistered() { register_loop.Run(); }
private:
void WorkerUnregistered(const ExtensionId& extension_id) override {
if (extension_id == extension_id_) {
unregister_loop.Quit();
}
}
void OnWorkerRegistered(const ExtensionId& extension_id) override {
if (extension_id == extension_id_) {
register_loop.Quit();
}
}
ExtensionId extension_id_;
base::RunLoop unregister_loop;
base::RunLoop register_loop;
};
// Tests that uninstalling an extension emits metrics for unregistering the
// service worker.
IN_PROC_BROWSER_TEST_P(ServiceWorkerRegistrationInstallMetricBrowserTest,
ExtensionUninstall) {
ASSERT_NO_FATAL_FAILURE(InstallMv2OrMv3Extension());
base::HistogramTester histogram_tester;
// Uninstall extension and wait for the unregistration metrics to have been
// emitted.
ServiceWorkerTaskQueue* task_queue = ServiceWorkerTaskQueue::Get(profile());
ASSERT_TRUE(task_queue);
ServiceWorkerTaskQueueRegistrationObserver register_observer(
extension()->id());
task_queue->SetObserverForTest(&register_observer);
const ExtensionId extension_id = extension()->id();
// Uninstalling frees `extension_` so we must free it here to prevent dangling
// ptr between the uninstall and until the test is torn down.
ReleaseExtension();
ExtensionRegistrar::Get(profile())->UninstallExtension(
extension_id, UNINSTALL_REASON_FOR_TESTING, nullptr);
{
SCOPED_TRACE(
"waiting for worker to be unregistered after uninstalling extension");
register_observer.WaitForWorkerUnregistered();
}
// Expected unregistration metrics for disable.
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState_"
"DeactivateExtension",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationFailureStatus",
/*expected_count=*/0);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationFailureStatus_"
"DeactivateExtension",
/*expected_count=*/0);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationFailureStatus_"
"AddExtension",
/*expected_count=*/0);
// We didn't update the extension.
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState_"
"AddExtension",
/*expected_count=*/0);
}
using ServiceWorkerRegistrationRestartMetricBrowserTest =
ServiceWorkerManifestVersionBrowserTest;
// Tests that restarting an extension emits metrics for unregistering and
// registering the service worker.
//
// TODO(crbug.com/349683323): Fix flakiness
IN_PROC_BROWSER_TEST_P(ServiceWorkerRegistrationRestartMetricBrowserTest,
DISABLED_ExtensionRestart) {
ASSERT_NO_FATAL_FAILURE(InstallMv2OrMv3Extension());
base::HistogramTester histogram_tester;
// Disable and then re-enable the extension.
ServiceWorkerTaskQueue* task_queue = ServiceWorkerTaskQueue::Get(profile());
ASSERT_TRUE(task_queue);
ServiceWorkerTaskQueueRegistrationObserver register_observer(
extension()->id());
task_queue->SetObserverForTest(&register_observer);
auto* extension_registrar = ExtensionRegistrar::Get(profile());
// Disable extension and wait for worker to be unregistered.
extension_registrar->DisableExtension(extension()->id(),
{disable_reason::DISABLE_USER_ACTION});
{
SCOPED_TRACE(
"waiting for worker to be unregistered after disabling extension");
register_observer.WaitForWorkerUnregistered();
}
// Enable extension and wait for registration metric should have been emitted.
extension_registrar->EnableExtension(extension()->id());
{
SCOPED_TRACE(
"waiting for worker to be registered after enabling extension");
register_observer.WaitForWorkerRegistered();
}
// Expected unregistration and registration metrics for disable and then
// enable for restart.
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState_"
"DeactivateExtension",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationFailureStatus",
/*expected_count=*/0);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationFailureStatus_"
"DeactivateExtension",
/*expected_count=*/0);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationFailureStatus_"
"AddExtension",
/*expected_count=*/0);
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerRegistrationState",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.Registration_FailStatus",
/*expected_count=*/0);
// We didn't update the extension.
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState_"
"AddExtension",
/*expected_count=*/0);
}
class MV2BackgroundsToMV3WorkerRegistrationMetricBrowserTest
: public ServiceWorkerRegistrationApiTest,
public testing::WithParamInterface<BackgroundType> {
protected:
const char* GetMV2ManifestBackgroundSectionForBackgroundType() {
switch (GetParam()) {
case BackgroundType::kLazyPage:
return R"(
"background": {
"scripts": ["background.js"],
"persistent": false
}
)";
case BackgroundType::kPersistentPage:
return R"(
"background": {
"scripts": ["background.js"],
"persistent": true
}
)";
}
}
};
// Tests that MV2 extensions of all background types, when updated, emit the
// metrics for previous worker unregistration and new worker registration.
IN_PROC_BROWSER_TEST_P(MV2BackgroundsToMV3WorkerRegistrationMetricBrowserTest,
ExtensionUpdate) {
static constexpr char kManifestMv2Template[] =
R"({
"name": "MV2 extension with non-SW background",
"version": "1",
"manifest_version": 2,
%s
})";
static constexpr char kManifestMv3[] =
R"({
"name": "MV3 extension with service worker",
"version": "2",
"manifest_version": 3,
"background": {
"service_worker": "background.js"
}
})";
constexpr char kBackgroundTemplate[] =
R"(
self.currentVersion = %d;
chrome.runtime.onInstalled.addListener((details) => {
chrome.test.sendMessage('v' + self.currentVersion + ' ready');
});
)";
const char* ManifestMv2BackgroundSection =
GetMV2ManifestBackgroundSectionForBackgroundType();
// Write and package the first version of the extension.
TestExtensionDir extension_dir;
extension_dir.WriteManifest(
base::StringPrintf(kManifestMv2Template, ManifestMv2BackgroundSection));
extension_dir.WriteFile(
FILE_PATH_LITERAL("background.js"),
base::StringPrintf(kBackgroundTemplate, /*self.currentVersion=*/1));
base::FilePath crx_v1_path = extension_dir.Pack("v1.crx");
// Install the MV2 extension.
const Extension* extension_v1 = nullptr;
{
ExtensionTestMessageListener listener("v1 ready");
extension_v1 = InstallExtension(crx_v1_path, /*expected_change=*/1);
SCOPED_TRACE("waiting for extension to be installed");
ASSERT_TRUE(listener.WaitUntilSatisfied());
}
ASSERT_TRUE(extension_v1);
EXPECT_EQ("1", extension_v1->version().GetString());
const ExtensionId extension_v1_id = extension_v1->id();
// Verify the first version of the extension is at v1.
EXPECT_EQ(base::Value(1),
GetVersionFlagFromBackgroundContext(extension_v1_id));
// Write and package the second version of the extension.
extension_dir.WriteManifest(kManifestMv3);
extension_dir.WriteFile(
FILE_PATH_LITERAL("background.js"),
base::StringPrintf(kBackgroundTemplate, /*self.currentVersion=*/2));
base::FilePath crx_v2_path = extension_dir.Pack("v2.crx");
// Update to the second (MV3) version of the extension with a worker.
const Extension* extension_v2 = nullptr;
base::HistogramTester histogram_tester; // Monitors metrics during update.
{
ExtensionTestMessageListener listener("v2 ready");
// `extension_v1` will be unsafe to use after update.
extension_v2 =
UpdateExtension(extension_v1_id, crx_v2_path, /*expected_change=*/0);
SCOPED_TRACE("waiting for extension to be updated");
ASSERT_TRUE(listener.WaitUntilSatisfied());
}
ASSERT_TRUE(extension_v2);
EXPECT_EQ("2", extension_v2->version().GetString());
EXPECT_EQ(extension_v1_id, extension_v2->id());
ASSERT_TRUE(HasActiveServiceWorker(extension_v2->id()));
// The service worker context should be that of the new version.
EXPECT_EQ(base::Value(2),
GetVersionFlagFromBackgroundContext(extension_v2->id()));
// First the old worker registration is unregistered. It is unregistered
// twice: once when removing the extension (ServiceWorkerTaskQueue) and then
// (redundantly) again before adding the new version of the extension. The
// redundant removal is meant to handle the case where a
// non-ServiceWorkerTaskQueue-tracked worker is registered for the extension
// (example: an MV2 extension that registered a worker via the web API).
// When updating from an MV2 worker we try to unregister the previous worker
// version first.
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState_"
"DeactivateExtension",
/*expected_count=*/0);
// We unsuccessfully try to unregister it again to handle workers that are
// registered via the web API. This is an expected failure.
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState_"
"AddExtension",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationFailureStatus4",
/*expected_count=*/0);
// Then the new worker registration is registered.
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerRegistrationState",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.Registration_FailStatus",
/*expected_count=*/0);
}
class WorkerBackgroundToWorkerBackgroundRegistrationMetricTest
: public ServiceWorkerRegistrationApiTest,
public testing::WithParamInterface<std::pair<int, int>> {};
// Tests that extensions of either manifest type can update to a worker from a
// previous worker version and emit metrics for unregistering the previous
// worker and registering the new worker version.
IN_PROC_BROWSER_TEST_P(WorkerBackgroundToWorkerBackgroundRegistrationMetricTest,
ExtensionUpdate) {
const char kManifestV1Template[] =
R"({
"name": "Version 1 extension with service worker",
"version": "1",
"manifest_version": %d,
"background": {
"service_worker": "background.js"
}
})";
static constexpr char kManifestV2Template[] =
R"({
"name": "Version 2 extension with service worker",
"version": "2",
"manifest_version": %d,
"background": {
"service_worker": "background.js"
}
})";
constexpr char kBackgroundTemplate[] =
R"(
self.currentVersion = %d;
chrome.runtime.onInstalled.addListener((details) => {
chrome.test.sendMessage('v' + self.currentVersion + ' ready');
});
)";
// Write and package the first version of the extension.
TestExtensionDir extension_dir;
extension_dir.WriteManifest(base::StringPrintf(
kManifestV1Template, /*manifest_version*/ GetParam().first));
extension_dir.WriteFile(
FILE_PATH_LITERAL("background.js"),
base::StringPrintf(kBackgroundTemplate, /*self.currentVersion=*/1));
base::FilePath crx_v1_path = extension_dir.Pack("v1.crx");
// Install the first version of the extension.
const Extension* extension_v1 = nullptr;
{
ExtensionTestMessageListener listener("v1 ready");
extension_v1 = InstallExtension(crx_v1_path, /*expected_change=*/1);
SCOPED_TRACE("waiting for version 1 of the extension to install");
ASSERT_TRUE(listener.WaitUntilSatisfied());
}
ASSERT_TRUE(extension_v1);
EXPECT_EQ("1", extension_v1->version().GetString());
const ExtensionId extension_v1_id = extension_v1->id();
ASSERT_TRUE(HasActiveServiceWorker(extension_v1_id));
// Verify the service worker is at v1.
EXPECT_EQ(base::Value(1),
GetVersionFlagFromBackgroundContext(extension_v1_id));
// Write and package the first version of the extension.
extension_dir.WriteManifest(base::StringPrintf(
kManifestV2Template, /*manifest_version*/ GetParam().second));
extension_dir.WriteFile(
FILE_PATH_LITERAL("background.js"),
base::StringPrintf(kBackgroundTemplate, /*self.currentVersion=*/2));
base::FilePath crx_v2_path = extension_dir.Pack("v2.crx");
// Update to the second version of the extension.
const Extension* extension_v2 = nullptr;
base::HistogramTester histogram_tester; // Monitors metrics during update.
{
ExtensionTestMessageListener listener("v2 ready");
// `extension_v1` will be unsafe to use after update.
extension_v2 =
UpdateExtension(extension_v1_id, crx_v2_path, /*expected_change=*/0);
SCOPED_TRACE("waiting for updated version 2 of the extension to install");
ASSERT_TRUE(listener.WaitUntilSatisfied());
}
ASSERT_TRUE(extension_v2);
EXPECT_EQ("2", extension_v2->version().GetString());
EXPECT_EQ(extension_v1_id, extension_v2->id());
EXPECT_TRUE(HasActiveServiceWorker(extension_v2->id()));
// The service worker context should be that of the new version.
EXPECT_EQ(base::Value(2),
GetVersionFlagFromBackgroundContext(extension_v2->id()));
// First the old worker registration is unregistered. It is unregistered
// twice: once when removing the extension (ServiceWorkerTaskQueue) and then
// redundantly again (but curiously it succeeds) before adding the new version
// of the extension.
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState",
/*true_count=*/2, /*false_count=*/0, histogram_tester);
// And it's unregistered due to the MV2 service worker being deactivated.
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState_"
"DeactivateExtension",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
// We redundantly attempt to unregister it again to handle workers that are
// registered via the web API.
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationState_"
"AddExtension",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationFailureStatus",
/*expected_count=*/0);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationFailureStatus_"
"DeactivateExtension",
/*expected_count=*/0);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.WorkerUnregistrationFailureStatus_"
"AddExtension",
/*expected_count=*/0);
CheckBooleanHistogramCounts(
"Extensions.ServiceWorkerBackground.WorkerRegistrationState",
/*true_count=*/1, /*false_count=*/0, histogram_tester);
histogram_tester.ExpectTotalCount(
"Extensions.ServiceWorkerBackground.Registration_FailStatus",
/*expected_count=*/0);
}
INSTANTIATE_TEST_SUITE_P(MV2,
ServiceWorkerRegistrationInstallMetricBrowserTest,
testing::Values(2));
INSTANTIATE_TEST_SUITE_P(MV3,
ServiceWorkerRegistrationInstallMetricBrowserTest,
testing::Values(3));
INSTANTIATE_TEST_SUITE_P(MV2,
ServiceWorkerRegistrationRestartMetricBrowserTest,
testing::Values(2));
INSTANTIATE_TEST_SUITE_P(MV3,
ServiceWorkerRegistrationRestartMetricBrowserTest,
testing::Values(3));
INSTANTIATE_TEST_SUITE_P(MV2EventPageToMV3Worker,
MV2BackgroundsToMV3WorkerRegistrationMetricBrowserTest,
testing::Values(BackgroundType::kLazyPage));
INSTANTIATE_TEST_SUITE_P(MV2PersistentPageToMV3Worker,
MV2BackgroundsToMV3WorkerRegistrationMetricBrowserTest,
testing::Values(BackgroundType::kPersistentPage));
INSTANTIATE_TEST_SUITE_P(
Mv2ToMv2,
WorkerBackgroundToWorkerBackgroundRegistrationMetricTest,
testing::Values(std::pair<int, int>(2, 2)));
INSTANTIATE_TEST_SUITE_P(
Mv2ToMv3,
WorkerBackgroundToWorkerBackgroundRegistrationMetricTest,
testing::Values(std::pair<int, int>(2, 3)));
INSTANTIATE_TEST_SUITE_P(
Mv3ToMv3,
WorkerBackgroundToWorkerBackgroundRegistrationMetricTest,
testing::Values(std::pair<int, int>(3, 3)));
} // namespace extensions