blob: 1c31809f0d6d559569ac3788be6010b09ac9dfcc [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 <utility>
#include "base/test/scoped_feature_list.h"
#include "base/test/with_feature_override.h"
#include "chrome/browser/extensions/api/web_navigation/web_navigation_api_helpers.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/mock_navigation_handle.h"
#include "content/public/test/service_worker_test_helpers.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/service_worker/service_worker_test_utils.h"
#include "extensions/common/extension_features.h"
#include "extensions/test/extension_test_message_listener.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/service_worker/embedded_worker_status.h"
#include "third_party/blink/public/mojom/service_worker/service_worker_database.mojom-forward.h"
namespace extensions {
namespace {
constexpr char kTestExtensionId[] = "iegclhlplifhodhkoafiokenjoapiobj";
using DispatchWebNavigationEventCallback = base::OnceCallback<void()>;
using service_worker_test_utils::TestServiceWorkerTaskQueueObserver;
using service_worker_test_utils::TestServiceWorkerContextObserver;
// Monitors the worker's running status and allows a callback to be run when the
// running status matches a specific `blink::EmbeddedWorkerStatus` running
// status.
class TestExtensionServiceWorkerRunningStatusObserver
: public content::ServiceWorkerTestHelper {
public:
explicit TestExtensionServiceWorkerRunningStatusObserver(
content::ServiceWorkerContext* sw_context,
int64_t worker_version_id = blink::mojom::kInvalidServiceWorkerVersionId)
: ServiceWorkerTestHelper(sw_context, worker_version_id),
test_worker_version_id_(worker_version_id) {}
TestExtensionServiceWorkerRunningStatusObserver(
const TestExtensionServiceWorkerRunningStatusObserver&) = delete;
TestExtensionServiceWorkerRunningStatusObserver& operator=(
const TestExtensionServiceWorkerRunningStatusObserver&) = delete;
// Set the worker status to watch for before running
// `test_event_dispatch_callback_`.
void SetDispatchCallbackOnStatus(
blink::EmbeddedWorkerStatus dispatch_status) {
dispatch_callback_on_status_ = dispatch_status;
}
// Set the callback to run when `dispatch_callback_on_status_` matches
// worker's current running status.
void SetDispatchTestEventCallback(base::OnceCallback<void()> callback) {
test_event_dispatch_callback_ = std::move(callback);
}
protected:
void OnDidRunningStatusChange(blink::EmbeddedWorkerStatus running_status,
int64_t version_id) override {
worker_running_status_ = running_status;
// We assume the next worker that arrives here is the one we're testing.
// This would be an incorrect assumption if we ever allowed multiple workers
// for an extension.
test_worker_version_id_ = version_id;
CheckWorkerStatusAndMaybeDispatchTestEvent(version_id);
}
// If running status matches desired running status then run the test event
// callback.
void CheckWorkerStatusAndMaybeDispatchTestEvent(
int64_t target_worker_version_id) {
if (!test_event_dispatch_callback_.is_null() &&
worker_running_status_ == dispatch_callback_on_status_) {
std::move(test_event_dispatch_callback_).Run();
}
}
private:
int64_t test_worker_version_id_ =
blink::mojom::kInvalidServiceWorkerVersionId;
blink::EmbeddedWorkerStatus worker_running_status_;
blink::EmbeddedWorkerStatus dispatch_callback_on_status_;
base::OnceCallback<void()> test_event_dispatch_callback_;
};
class ServiceWorkerEventDispatchingBrowserTest : public ExtensionBrowserTest {
public:
ServiceWorkerEventDispatchingBrowserTest() = default;
ServiceWorkerEventDispatchingBrowserTest(
const ServiceWorkerEventDispatchingBrowserTest&) = delete;
ServiceWorkerEventDispatchingBrowserTest& operator=(
const ServiceWorkerEventDispatchingBrowserTest&) = delete;
void SetUpOnMainThread() override {
ExtensionBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
sw_context_ = GetServiceWorkerContext();
}
void TearDownOnMainThread() override {
ExtensionBrowserTest::TearDownOnMainThread();
sw_context_ = nullptr;
}
DispatchWebNavigationEventCallback CreateDispatchWebNavEventCallback(
int num_events_to_dispatch = 1) {
return base::BindOnce(
&ServiceWorkerEventDispatchingBrowserTest::DispatchWebNavigationEvent,
base::Unretained(this), num_events_to_dispatch);
}
// Broadcasts a webNavigation.onBeforeNavigate events.
void DispatchWebNavigationEvent(int num_events_to_dispatch = 1) {
EventRouter* router = EventRouter::EventRouter::Get(profile());
testing::NiceMock<content::MockNavigationHandle> handle(
GetActiveWebContents());
for (int i = 0; i < num_events_to_dispatch; i++) {
auto event =
web_navigation_api_helpers::CreateOnBeforeNavigateEvent(&handle);
router->BroadcastEvent(std::move(event));
}
}
protected:
raw_ptr<content::ServiceWorkerContext> sw_context_ = nullptr;
base::test::ScopedFeatureList scoped_feature_list_;
};
// Tests that dispatching an event to a worker with status
// `blink::EmbeddedWorkerStatus::kRunning` succeeds.
IN_PROC_BROWSER_TEST_F(ServiceWorkerEventDispatchingBrowserTest,
DispatchToRunningWorker) {
TestServiceWorkerContextObserver sw_started_observer(profile(),
kTestExtensionId);
ExtensionTestMessageListener extension_oninstall_listener_fired(
"installed listener fired");
const Extension* extension = LoadExtension(
test_data_dir_.AppendASCII("events/reliability/service_worker"),
{.wait_for_registration_stored = true});
ASSERT_TRUE(extension);
ASSERT_EQ(kTestExtensionId, extension->id());
// This ensures that we wait until the the browser receives the ack from the
// renderer. This prevents unexpected histogram emits later.
ASSERT_TRUE(extension_oninstall_listener_fired.WaitUntilSatisfied());
const int64_t test_worker_version_id =
sw_started_observer.WaitForWorkerStarted();
ASSERT_TRUE(content::CheckServiceWorkerIsRunning(sw_context_,
test_worker_version_id));
// Stop the worker, and wait for it to stop. We must stop it first before we
// can observe the kRunning status.
browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(),
extension->id());
sw_started_observer.WaitForWorkerStopped();
ASSERT_TRUE(content::CheckServiceWorkerIsStopped(sw_context_,
test_worker_version_id));
// Add observer that will watch for changes to the running status of the
// worker.
TestExtensionServiceWorkerRunningStatusObserver test_event_observer(
GetServiceWorkerContext());
// Setup to run the test event when kRunning status is encountered.
test_event_observer.SetDispatchTestEventCallback(
CreateDispatchWebNavEventCallback());
test_event_observer.SetDispatchCallbackOnStatus(
blink::EmbeddedWorkerStatus::kRunning);
// Setup listeners for confirming the event ran successfully.
base::HistogramTester histogram_tester;
ExtensionTestMessageListener extension_event_listener_fired("listener fired");
// Start the worker.
sw_context_->StartWorkerForScope(/*scope=*/extension->url(),
/*key=*/
blink::StorageKey::CreateFirstParty(
url::Origin::Create(extension->url())),
/*info_callback=*/base::DoNothing(),
/*failure_callback=*/base::DoNothing());
// During the above start request we catch the kRunning status with
// TestExtensionServiceWorkerRunningStatusObserver::OnDidRunningStatusChange()
// then synchronously dispatch the test event there.
// The histogram expect checks that we get an ack from the renderer to the
// browser for the event. The wait confirms that the extension worker listener
// finished. The wait is first (despite temporally possibly being after the
// ack) because it is currently the most convenient to wait on.
EXPECT_TRUE(extension_event_listener_fired.WaitUntilSatisfied());
// Call to webNavigation.onBeforeNavigate expected.
histogram_tester.ExpectTotalCount(
"Extensions.Events.DispatchToAckTime.ExtensionServiceWorker2",
/*expected_count=*/1);
}
// Tests that dispatching an event to a worker with status
// `blink::EmbeddedWorkerStatus::kStopped` succeeds. This logic is laid out
// differently than in the other test cases because we can't currently detect
// precisely when a worker enters the stopped status.
IN_PROC_BROWSER_TEST_F(ServiceWorkerEventDispatchingBrowserTest,
DispatchToStoppedWorker) {
TestServiceWorkerContextObserver sw_started_stopped_observer(
profile(), kTestExtensionId);
ExtensionTestMessageListener extension_oninstall_listener_fired(
"installed listener fired");
const Extension* extension = LoadExtension(
test_data_dir_.AppendASCII("events/reliability/service_worker"),
{.wait_for_registration_stored = true});
ASSERT_TRUE(extension);
ASSERT_EQ(kTestExtensionId, extension->id());
const int64_t test_worker_version_id =
sw_started_stopped_observer.WaitForWorkerStarted();
// This ensures that we wait until the the browser receives the ack from the
// renderer. This prevents unexpected histogram emits later.
ASSERT_TRUE(extension_oninstall_listener_fired.WaitUntilSatisfied());
ASSERT_TRUE(content::CheckServiceWorkerIsRunning(sw_context_,
test_worker_version_id));
// ServiceWorkerVersion is destroyed async when we stop the worker so we can't
// precisely check when the worker stopped. So instead, wait for when we
// notice a stopping worker, confirm the worker didn't restart, and check the
// worker's status to confirm kStopped occurred to be as certain that we can
// that the worker is stopped when we dispatch the event.
TestExtensionServiceWorkerRunningStatusObserver worker_restarted_observer(
GetServiceWorkerContext());
// Stop the worker, and wait for it to stop.
browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(),
extension->id());
sw_started_stopped_observer.WaitForWorkerStopped();
// TODO(crbug.com/40276609): Add a more guaranteed check that the worker was
// stopped when we dispatch the event. This check confirms the worker is
// currently stopped, but doesn't guarantee that when we dispatch the event
// below that it is still stopped.
ASSERT_TRUE(content::CheckServiceWorkerIsStopped(
sw_context_,
// Service workers keep the same version id across restarts.
test_worker_version_id));
// Setup listeners for confirming the event ran successfully.
base::HistogramTester histogram_tester;
ExtensionTestMessageListener extension_event_listener_fired("listener fired");
DispatchWebNavigationEvent();
// The histogram expect checks that we get an ack from the renderer to the
// browser for the event. The wait confirms that the extension worker
// listener finished. The wait is first (despite temporally possibly being
// after the ack) because it is currently the most convenient to wait on.
EXPECT_TRUE(extension_event_listener_fired.WaitUntilSatisfied());
// Call to webNavigation.onBeforeNavigate expected.
histogram_tester.ExpectTotalCount(
"Extensions.Events.DispatchToAckTime.ExtensionServiceWorker2",
/*expected_count=*/1);
}
// Tests that dispatching an event to a worker with status
// `blink::EmbeddedWorkerStatus::kStarting` succeeds. This test first
// installs the extension and waits for the worker to fully start. Then stops it
// and starts it again to catch the kStarting status. This is to avoid event
// acknowledgments on install we aren't trying to test for.
// TODO(jlulejian): If we suspect or see worker bugs that occur on extension
// install then create test cases where we dispatch events immediately on
// extension install.
IN_PROC_BROWSER_TEST_F(ServiceWorkerEventDispatchingBrowserTest,
DispatchToStartingWorker) {
TestServiceWorkerContextObserver sw_started_stopped_observer(
profile(), kTestExtensionId);
ExtensionTestMessageListener extension_oninstall_listener_fired(
"installed listener fired");
const Extension* extension = LoadExtension(
test_data_dir_.AppendASCII("events/reliability/service_worker"),
{.wait_for_registration_stored = true});
ASSERT_TRUE(extension);
ASSERT_EQ(kTestExtensionId, extension->id());
const int64_t test_worker_version_id =
sw_started_stopped_observer.WaitForWorkerStarted();
// This ensures that we wait until the the browser receives the ack from the
// renderer. This prevents unexpected histogram emits later.
ASSERT_TRUE(extension_oninstall_listener_fired.WaitUntilSatisfied());
ASSERT_TRUE(content::CheckServiceWorkerIsRunning(sw_context_,
test_worker_version_id));
// Stop the worker, and wait for it to stop. We must stop it first before we
// can start and observe the kStarting status.
browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(),
extension->id());
sw_started_stopped_observer.WaitForWorkerStopped();
// Add observer that will watch for changes to the running status of the
// worker.
TestExtensionServiceWorkerRunningStatusObserver test_event_observer(
GetServiceWorkerContext());
// Setup to run the test event when kStarting status is encountered.
test_event_observer.SetDispatchTestEventCallback(
CreateDispatchWebNavEventCallback());
test_event_observer.SetDispatchCallbackOnStatus(
blink::EmbeddedWorkerStatus::kStarting);
// Setup listeners for confirming the event ran successfully.
base::HistogramTester histogram_tester;
ExtensionTestMessageListener extension_event_listener_fired("listener fired");
// Start the worker and wait until the worker is kStarting.
sw_context_->StartWorkerForScope(/*scope=*/extension->url(),
/*key=*/
blink::StorageKey::CreateFirstParty(
url::Origin::Create(extension->url())),
/*info_callback=*/base::DoNothing(),
/*failure_callback=*/base::DoNothing());
// During the above start request we catch the transient kStarting status with
// TestExtensionServiceWorkerRunningStatusObserver::OnDidRunningStatusChange()
// then synchronously dispatch the test event there.
// The histogram expect checks that we get an ack from the renderer to the
// browser for the event. The wait confirms that the extension worker listener
// finished. The wait is first (despite temporally possibly being after the
// ack) because it is currently the most convenient to wait on.
EXPECT_TRUE(extension_event_listener_fired.WaitUntilSatisfied());
// Call to webNavigation.onBeforeNavigate expected.
histogram_tester.ExpectTotalCount(
"Extensions.Events.DispatchToAckTime.ExtensionServiceWorker2",
/*expected_count=*/1);
}
// Tests that dispatching an event to a
// worker with status `blink::EmbeddedWorkerStatus::kStopping` succeeds.
IN_PROC_BROWSER_TEST_F(ServiceWorkerEventDispatchingBrowserTest,
DispatchToStoppingWorker) {
TestServiceWorkerContextObserver sw_started_observer(profile(),
kTestExtensionId);
ExtensionTestMessageListener extension_oninstall_listener_fired(
"installed listener fired");
const Extension* extension = LoadExtension(
test_data_dir_.AppendASCII("events/reliability/service_worker"),
{.wait_for_registration_stored = true});
ASSERT_TRUE(extension);
ASSERT_EQ(kTestExtensionId, extension->id());
const int64_t test_worker_version_id =
sw_started_observer.WaitForWorkerStarted();
// This ensures that we wait until the the browser receives the ack from the
// renderer. This prevents unexpected histogram emits later.
ASSERT_TRUE(extension_oninstall_listener_fired.WaitUntilSatisfied());
ASSERT_TRUE(content::CheckServiceWorkerIsRunning(sw_context_,
test_worker_version_id));
// Add observer that will watch for changes to the running status of the
// worker.
TestExtensionServiceWorkerRunningStatusObserver test_event_observer(
GetServiceWorkerContext(), test_worker_version_id);
// Setup to run the test event when kStopping status is encountered.
test_event_observer.SetDispatchTestEventCallback(
CreateDispatchWebNavEventCallback());
test_event_observer.SetDispatchCallbackOnStatus(
blink::EmbeddedWorkerStatus::kStopping);
// Setup listeners for confirming the event ran successfully.
base::HistogramTester histogram_tester;
ExtensionTestMessageListener extension_event_listener_fired("listener fired");
// Stop the worker, but don't wait for it to stop. We want to catch the state
// change to kStopping status when we dispatch the event.
content::StopServiceWorkerForScope(sw_context_, extension->url(),
base::DoNothing());
// During the above stop request we catch the kStopped status with
// TestExtensionServiceWorkerRunningStatusObserver::OnDidRunningStatusChange()
// then synchronously dispatch the test event there.
// The histogram expect checks that we get an ack from the renderer to the
// browser for the event. The wait confirms that the extension worker listener
// finished. The wait is first (despite temporally possibly being after the
// ack) because it is currently the most convenient to wait on.
EXPECT_TRUE(extension_event_listener_fired.WaitUntilSatisfied());
// Call to webNavigation.onBeforeNavigate expected.
histogram_tester.ExpectTotalCount(
"Extensions.Events.DispatchToAckTime.ExtensionServiceWorker2",
/*expected_count=*/1);
}
// Tests that we will not attempt to redundantly start a worker if it is
// in the kStarting status (meaning: there are pending events/tasks to
// process).
IN_PROC_BROWSER_TEST_F(ServiceWorkerEventDispatchingBrowserTest,
StartingWorkerIsNotStartRequested) {
TestServiceWorkerContextObserver sw_started_stopped_observer(
profile(), kTestExtensionId);
ExtensionTestMessageListener extension_oninstall_listener_fired(
"installed listener fired");
const Extension* extension =
LoadExtension(test_data_dir_.AppendASCII(
"events/reliability/service_worker_redundant_start"),
{.wait_for_registration_stored = true});
ASSERT_TRUE(extension);
ASSERT_EQ(kTestExtensionId, extension->id());
const int64_t test_worker_version_id =
sw_started_stopped_observer.WaitForWorkerStarted();
// This ensures that we wait until the the browser receives the ack from the
// renderer. This prevents unexpected histogram emits later.
ASSERT_TRUE(extension_oninstall_listener_fired.WaitUntilSatisfied());
ASSERT_TRUE(content::CheckServiceWorkerIsRunning(sw_context_,
test_worker_version_id));
// Stop the worker, and wait for it to stop. We must stop it first before we
// can start and observe the kStarting status.
browsertest_util::StopServiceWorkerForExtensionGlobalScope(profile(),
extension->id());
sw_started_stopped_observer.WaitForWorkerStopped();
// Add observer that will watch for changes to the running status of the
// worker.
TestExtensionServiceWorkerRunningStatusObserver test_event_observer(
GetServiceWorkerContext());
// Setup to send test events when kStarting status is encountered.
// Sending multiple events is what could elicit a redundant start if the
// logic isn't working as expected.
test_event_observer.SetDispatchTestEventCallback(
CreateDispatchWebNavEventCallback(/*num_events_to_dispatch=*/2));
test_event_observer.SetDispatchCallbackOnStatus(
blink::EmbeddedWorkerStatus::kStarting);
// Setup listeners for confirming the event ran successfully.
base::HistogramTester histogram_tester;
ExtensionTestMessageListener extension_event_listener_fired_three_times(
"listener fired three times");
TestServiceWorkerTaskQueueObserver start_count_observer;
// This dispatch will start the worker with the existing event routing and
// task queueing logic.
DispatchWebNavigationEvent();
// During the above start that occurs as part of dispatching the event we
// catch the transient kStarting status with
// TestExtensionServiceWorkerRunningStatusObserver::OnDidRunningStatusChange()
// then synchronously dispatch two more test events there.
EXPECT_TRUE(extension_event_listener_fired_three_times.WaitUntilSatisfied());
// Three calls to webNavigation.onBeforeNavigate listener expected.
histogram_tester.ExpectTotalCount(
"Extensions.Events.DispatchToAckTime.ExtensionServiceWorker2",
/*expected_count=*/3);
// Confirm the expected number of start requests that are sent to the
// extension during the multi event dispatch. Should only need one start to
// process the multiple events.
EXPECT_EQ(
1, start_count_observer.GetRequestedWorkerStartedCount(extension->id()));
}
class ServiceWorkerEventDispatchingBrowserTestWithOptimizeServiceWorkerStart
: public ServiceWorkerEventDispatchingBrowserTest,
public base::test::WithFeatureOverride {
public:
ServiceWorkerEventDispatchingBrowserTestWithOptimizeServiceWorkerStart()
: WithFeatureOverride(
extensions_features::kOptimizeServiceWorkerStartRequests) {}
};
// Tests the behavior of service worker start requests when a worker is already
// running.
IN_PROC_BROWSER_TEST_P(
ServiceWorkerEventDispatchingBrowserTestWithOptimizeServiceWorkerStart,
StartedWorkerRedundantStarts) {
const bool wakeup_optimization_enabled = IsParamFeatureEnabled();
const int kExpectedWakeUps = wakeup_optimization_enabled ? 0 : 1;
TestServiceWorkerContextObserver sw_started_stopped_observer(
profile(), kTestExtensionId);
ExtensionTestMessageListener extension_oninstall_listener_fired(
"installed listener fired");
const Extension* extension = LoadExtension(
test_data_dir_.AppendASCII("events/reliability/service_worker"),
{.wait_for_registration_stored = true});
ASSERT_TRUE(extension);
ASSERT_EQ(kTestExtensionId, extension->id());
// This ensures that we wait until the the browser receives the ack from the
// renderer. This prevents unexpected histogram emits later.
ASSERT_TRUE(extension_oninstall_listener_fired.WaitUntilSatisfied());
const int64_t test_worker_version_id =
sw_started_stopped_observer.WaitForWorkerStarted();
ASSERT_TRUE(content::CheckServiceWorkerIsRunning(sw_context_,
test_worker_version_id));
// Setup listeners for confirming the event ran successfully.
ExtensionTestMessageListener extension_event_listener_fired("listener fired");
TestServiceWorkerTaskQueueObserver start_count_observer;
DispatchWebNavigationEvent();
EXPECT_TRUE(extension_event_listener_fired.WaitUntilSatisfied());
// Confirm the expected number of start requests that are sent to the
// extension worker during event dispatch.
EXPECT_EQ(
kExpectedWakeUps,
start_count_observer.GetRequestedWorkerStartedCount(extension->id()));
}
// Toggle `extensions_features::OptimizeServiceWorkerStartRequests`.
INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
ServiceWorkerEventDispatchingBrowserTestWithOptimizeServiceWorkerStart);
// TODO(crbug.com/40276609): Create test for event dispatching that uses the
// `EventRouter::DispatchEventToSender()` event flow.
// TODO(crbug.com/40072982): Test that kBadRequestId no longer kills the service
// worker renderer with a test that mimics receiving a stale ack to the browser.
} // namespace
} // namespace extensions