| // Copyright 2019 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "content/browser/devtools/devtools_background_services_context_impl.h" | |
| #include <memory> | |
| #include <string> | |
| #include <vector> | |
| #include "base/containers/flat_map.h" | |
| #include "base/memory/scoped_refptr.h" | |
| #include "base/run_loop.h" | |
| #include "base/time/time.h" | |
| #include "content/browser/devtools/devtools_background_services.pb.h" | |
| #include "content/browser/service_worker/embedded_worker_test_helper.h" | |
| #include "content/browser/service_worker/service_worker_context_core.h" | |
| #include "content/public/browser/content_browser_client.h" | |
| #include "content/public/common/content_client.h" | |
| #include "content/public/test/browser_task_environment.h" | |
| #include "content/public/test/test_browser_context.h" | |
| #include "content/public/test/test_utils.h" | |
| #include "testing/gmock/include/gmock/gmock.h" | |
| #include "testing/gtest/include/gtest/gtest.h" | |
| #include "third_party/blink/public/common/storage_key/storage_key.h" | |
| #include "third_party/blink/public/mojom/service_worker/service_worker_registration_options.mojom.h" | |
| #include "url/origin.h" | |
| namespace content { | |
| namespace { | |
| using testing::_; | |
| const std::string kEventName = "Test Event"; | |
| const std::string kInstanceId = "my-instance"; | |
| class TestBrowserClient : public ContentBrowserClient { | |
| public: | |
| TestBrowserClient() {} | |
| ~TestBrowserClient() override {} | |
| void UpdateDevToolsBackgroundServiceExpiration( | |
| BrowserContext* browser_context, | |
| int service, | |
| base::Time expiration_time) override { | |
| exp_dict_[service] = expiration_time; | |
| } | |
| base::flat_map<int, base::Time> GetDevToolsBackgroundServiceExpirations( | |
| BrowserContext* browser_context) override { | |
| return exp_dict_; | |
| } | |
| private: | |
| base::flat_map<int, base::Time> exp_dict_; | |
| }; | |
| void DidRegisterServiceWorker(int64_t* out_service_worker_registration_id, | |
| base::OnceClosure quit_closure, | |
| blink::ServiceWorkerStatusCode status, | |
| const std::string& status_message, | |
| int64_t service_worker_registration_id) { | |
| DCHECK(out_service_worker_registration_id); | |
| EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, status) << status_message; | |
| *out_service_worker_registration_id = service_worker_registration_id; | |
| std::move(quit_closure).Run(); | |
| } | |
| void DidFindServiceWorkerRegistration( | |
| scoped_refptr<ServiceWorkerRegistration>* out_service_worker_registration, | |
| base::OnceClosure quit_closure, | |
| blink::ServiceWorkerStatusCode status, | |
| scoped_refptr<ServiceWorkerRegistration> service_worker_registration) { | |
| DCHECK(out_service_worker_registration); | |
| EXPECT_EQ(blink::ServiceWorkerStatusCode::kOk, status) | |
| << blink::ServiceWorkerStatusToString(status); | |
| *out_service_worker_registration = service_worker_registration; | |
| std::move(quit_closure).Run(); | |
| } | |
| void DidGetLoggedBackgroundServiceEvents( | |
| base::OnceClosure quit_closure, | |
| std::vector<devtools::proto::BackgroundServiceEvent>* out_feature_states, | |
| std::vector<devtools::proto::BackgroundServiceEvent> feature_states) { | |
| *out_feature_states = std::move(feature_states); | |
| std::move(quit_closure).Run(); | |
| } | |
| } // namespace | |
| class DevToolsBackgroundServicesContextTest | |
| : public ::testing::Test, | |
| DevToolsBackgroundServicesContextImpl::EventObserver { | |
| public: | |
| DevToolsBackgroundServicesContextTest() | |
| : task_environment_(BrowserTaskEnvironment::IO_MAINLOOP), | |
| embedded_worker_test_helper_(base::FilePath() /* in memory */) {} | |
| DevToolsBackgroundServicesContextTest( | |
| const DevToolsBackgroundServicesContextTest&) = delete; | |
| DevToolsBackgroundServicesContextTest& operator=( | |
| const DevToolsBackgroundServicesContextTest&) = delete; | |
| ~DevToolsBackgroundServicesContextTest() override = default; | |
| void SetUp() override { | |
| // Register Service Worker. | |
| service_worker_registration_id_ = RegisterServiceWorker(); | |
| ASSERT_NE(service_worker_registration_id_, | |
| blink::mojom::kInvalidServiceWorkerRegistrationId); | |
| browser_client_ = std::make_unique<TestBrowserClient>(); | |
| scoped_content_browser_client_setting_ = | |
| std::make_unique<ScopedContentBrowserClientSetting>( | |
| browser_client_.get()); | |
| SimulateBrowserRestart(); | |
| } | |
| void TearDown() override { context_->RemoveObserver(this); } | |
| mojo::Remote<storage::mojom::ServiceWorkerStorageControl>& storage_control() { | |
| return embedded_worker_test_helper_.context() | |
| ->registry() | |
| .GetRemoteStorageControl(); | |
| } | |
| protected: | |
| MOCK_METHOD1(OnEventReceived, | |
| void(const devtools::proto::BackgroundServiceEvent& event)); | |
| MOCK_METHOD2(OnRecordingStateChanged, | |
| void(bool shoul_record, | |
| devtools::proto::BackgroundService service)); | |
| void SimulateBrowserRestart() { | |
| if (context_) | |
| context_->RemoveObserver(this); | |
| // Create |context_|. | |
| context_ = std::make_unique<DevToolsBackgroundServicesContextImpl>( | |
| &browser_context_, embedded_worker_test_helper_.context_wrapper()); | |
| context_->AddObserver(this); | |
| ASSERT_TRUE(context_); | |
| } | |
| void SimulateOneWeekPassing() { | |
| base::Time one_week_ago = base::Time::Now() - base::Days(7); | |
| context_->expiration_times_ | |
| [devtools::proto::BackgroundService::BACKGROUND_FETCH] = one_week_ago; | |
| } | |
| bool IsRecording() { | |
| return context_->IsRecording( | |
| devtools::proto::BackgroundService::BACKGROUND_FETCH); | |
| } | |
| base::Time GetExpirationTime() { | |
| return context_->expiration_times_ | |
| [devtools::proto::BackgroundService::BACKGROUND_FETCH]; | |
| } | |
| std::vector<devtools::proto::BackgroundServiceEvent> | |
| GetLoggedBackgroundServiceEvents() { | |
| std::vector<devtools::proto::BackgroundServiceEvent> feature_states; | |
| base::RunLoop run_loop; | |
| context_->GetLoggedBackgroundServiceEvents( | |
| devtools::proto::BackgroundService::BACKGROUND_FETCH, | |
| base::BindOnce(&DidGetLoggedBackgroundServiceEvents, | |
| run_loop.QuitClosure(), &feature_states)); | |
| run_loop.Run(); | |
| return feature_states; | |
| } | |
| void LogTestBackgroundServiceEvent(const std::string& log_message) { | |
| context_->LogBackgroundServiceEvent( | |
| service_worker_registration_id_, | |
| blink::StorageKey::CreateFirstParty(origin_), | |
| DevToolsBackgroundService::kBackgroundFetch, kEventName, kInstanceId, | |
| {{"key", log_message}}); | |
| } | |
| void StartRecording() { | |
| EXPECT_CALL( | |
| *this, OnRecordingStateChanged( | |
| true, devtools::proto::BackgroundService::BACKGROUND_FETCH)); | |
| context_->StartRecording( | |
| devtools::proto::BackgroundService::BACKGROUND_FETCH); | |
| // Wait for the messages to propagate to the browser client. | |
| task_environment_.RunUntilIdle(); | |
| } | |
| void StopRecording() { | |
| EXPECT_CALL( | |
| *this, | |
| OnRecordingStateChanged( | |
| false, devtools::proto::BackgroundService::BACKGROUND_FETCH)); | |
| context_->StopRecording( | |
| devtools::proto::BackgroundService::BACKGROUND_FETCH); | |
| // Wait for the messages to propagate to the browser client. | |
| task_environment_.RunUntilIdle(); | |
| } | |
| void ClearLoggedBackgroundServiceEvents() { | |
| context_->ClearLoggedBackgroundServiceEvents( | |
| devtools::proto::BackgroundService::BACKGROUND_FETCH); | |
| } | |
| BrowserTaskEnvironment task_environment_; // Must be first member. | |
| url::Origin origin_ = url::Origin::Create(GURL("https://example.com")); | |
| int64_t service_worker_registration_id_ = | |
| blink::mojom::kInvalidServiceWorkerRegistrationId; | |
| private: | |
| int64_t RegisterServiceWorker() { | |
| GURL script_url(origin_.GetURL().spec() + "sw.js"); | |
| const blink::StorageKey key = blink::StorageKey::CreateFirstParty(origin_); | |
| int64_t service_worker_registration_id = | |
| blink::mojom::kInvalidServiceWorkerRegistrationId; | |
| { | |
| blink::mojom::ServiceWorkerRegistrationOptions options; | |
| options.scope = origin_.GetURL(); | |
| base::RunLoop run_loop; | |
| auto fetch_client_settings_object = | |
| blink::mojom::FetchClientSettingsObject::New(); | |
| fetch_client_settings_object->policy_container_policies = | |
| blink::mojom::PolicyContainerPolicies::New(); | |
| embedded_worker_test_helper_.context()->RegisterServiceWorker( | |
| script_url, key, options, std::move(fetch_client_settings_object), | |
| base::BindOnce(&DidRegisterServiceWorker, | |
| &service_worker_registration_id, | |
| run_loop.QuitClosure()), | |
| /*requesting_frame_id=*/GlobalRenderFrameHostId(), | |
| PolicyContainerPolicies()); | |
| run_loop.Run(); | |
| } | |
| if (service_worker_registration_id == | |
| blink::mojom::kInvalidServiceWorkerRegistrationId) { | |
| ADD_FAILURE() << "Could not obtain a valid Service Worker registration"; | |
| return blink::mojom::kInvalidServiceWorkerRegistrationId; | |
| } | |
| { | |
| base::RunLoop run_loop; | |
| embedded_worker_test_helper_.context()->registry().FindRegistrationForId( | |
| service_worker_registration_id, key, | |
| base::BindOnce(&DidFindServiceWorkerRegistration, | |
| &service_worker_registration_, | |
| run_loop.QuitClosure())); | |
| run_loop.Run(); | |
| } | |
| // Wait for the worker to be activated. | |
| base::RunLoop().RunUntilIdle(); | |
| if (!service_worker_registration_) { | |
| ADD_FAILURE() << "Could not find the new Service Worker registration."; | |
| return blink::mojom::kInvalidServiceWorkerRegistrationId; | |
| } | |
| return service_worker_registration_id; | |
| } | |
| EmbeddedWorkerTestHelper embedded_worker_test_helper_; | |
| TestBrowserContext browser_context_; | |
| std::unique_ptr<DevToolsBackgroundServicesContextImpl> context_; | |
| scoped_refptr<ServiceWorkerRegistration> service_worker_registration_; | |
| std::unique_ptr<ContentBrowserClient> browser_client_; | |
| std::unique_ptr<ScopedContentBrowserClientSetting> | |
| scoped_content_browser_client_setting_; | |
| }; | |
| // Flaky on Fuchsia. | |
| // TODO(crbug.com/40936408): Reenable test on Fuchsia. | |
| #if BUILDFLAG(IS_FUCHSIA) | |
| #define MAYBE_NothingStoredWithRecordingModeOff \ | |
| DISABLED_NothingStoredWithRecordingModeOff | |
| #else | |
| #define MAYBE_NothingStoredWithRecordingModeOff \ | |
| NothingStoredWithRecordingModeOff | |
| #endif | |
| TEST_F(DevToolsBackgroundServicesContextTest, | |
| MAYBE_NothingStoredWithRecordingModeOff) { | |
| // Initially there are no entries. | |
| EXPECT_TRUE(GetLoggedBackgroundServiceEvents().empty()); | |
| // "Log" some events and wait for them to finish. | |
| LogTestBackgroundServiceEvent("f1"); | |
| LogTestBackgroundServiceEvent("f2"); | |
| // There should still be nothing since recording mode is off. | |
| EXPECT_TRUE(GetLoggedBackgroundServiceEvents().empty()); | |
| } | |
| TEST_F(DevToolsBackgroundServicesContextTest, GetLoggedEvents) { | |
| StartRecording(); | |
| // "Log" some events and wait for them to finish. | |
| LogTestBackgroundServiceEvent("f1"); | |
| LogTestBackgroundServiceEvent("f2"); | |
| storage_control().FlushForTesting(); | |
| // Check the values. | |
| auto feature_events = GetLoggedBackgroundServiceEvents(); | |
| ASSERT_EQ(feature_events.size(), 2u); | |
| for (const auto& feature_event : feature_events) { | |
| EXPECT_EQ(feature_event.background_service(), | |
| devtools::proto::BackgroundService::BACKGROUND_FETCH); | |
| EXPECT_EQ(feature_event.origin(), origin_.GetURL().spec()); | |
| EXPECT_EQ(feature_event.service_worker_registration_id(), | |
| service_worker_registration_id_); | |
| EXPECT_EQ(feature_event.event_name(), kEventName); | |
| EXPECT_EQ(feature_event.instance_id(), kInstanceId); | |
| ASSERT_EQ(feature_event.event_metadata().size(), 1u); | |
| } | |
| EXPECT_EQ(feature_events[0].event_metadata().at("key"), "f1"); | |
| EXPECT_EQ(feature_events[1].event_metadata().at("key"), "f2"); | |
| EXPECT_LE(feature_events[0].timestamp(), feature_events[1].timestamp()); | |
| } | |
| TEST_F(DevToolsBackgroundServicesContextTest, StopRecording) { | |
| StartRecording(); | |
| // Initially there are no entries. | |
| EXPECT_TRUE(GetLoggedBackgroundServiceEvents().empty()); | |
| // "Log" some events and wait for them to finish. | |
| LogTestBackgroundServiceEvent("f1"); | |
| StopRecording(); | |
| LogTestBackgroundServiceEvent("f2"); | |
| // Check the values. | |
| ASSERT_EQ(GetLoggedBackgroundServiceEvents().size(), 1u); | |
| } | |
| TEST_F(DevToolsBackgroundServicesContextTest, DelegateExpirationTimes) { | |
| // Initially expiration time is null. | |
| EXPECT_TRUE(GetExpirationTime().is_null()); | |
| EXPECT_FALSE(IsRecording()); | |
| // Toggle Recording mode, and now this should be non-null. | |
| StartRecording(); | |
| EXPECT_FALSE(GetExpirationTime().is_null()); | |
| EXPECT_TRUE(IsRecording()); | |
| // The value should still be there on browser restarts. | |
| SimulateBrowserRestart(); | |
| EXPECT_FALSE(GetExpirationTime().is_null()); | |
| EXPECT_TRUE(IsRecording()); | |
| // Stopping Recording mode should clear the value. | |
| StopRecording(); | |
| EXPECT_TRUE(GetExpirationTime().is_null()); | |
| EXPECT_FALSE(IsRecording()); | |
| SimulateBrowserRestart(); | |
| EXPECT_TRUE(GetExpirationTime().is_null()); | |
| EXPECT_FALSE(IsRecording()); | |
| } | |
| TEST_F(DevToolsBackgroundServicesContextTest, RecordingExpiration) { | |
| // Initially expiration time is null. | |
| EXPECT_FALSE(IsRecording()); | |
| // Toggle Recording mode, and now this should be non-null. | |
| StartRecording(); | |
| EXPECT_TRUE(IsRecording()); | |
| SimulateOneWeekPassing(); | |
| EXPECT_FALSE(GetExpirationTime().is_null()); | |
| // Recording should be true, with an expired value. | |
| EXPECT_TRUE(IsRecording()); | |
| // Logging should not happen. | |
| EXPECT_CALL(*this, OnEventReceived(_)).Times(0); | |
| // Observers should be informed when recording stops. | |
| EXPECT_CALL(*this, | |
| OnRecordingStateChanged( | |
| false, devtools::proto::BackgroundService::BACKGROUND_FETCH)); | |
| LogTestBackgroundServiceEvent("f1"); | |
| task_environment_.RunUntilIdle(); | |
| // The expiration time entry should be cleared. | |
| EXPECT_TRUE(GetExpirationTime().is_null()); | |
| EXPECT_FALSE(IsRecording()); | |
| } | |
| TEST_F(DevToolsBackgroundServicesContextTest, ClearLoggedEvents) { | |
| StartRecording(); | |
| // "Log" some events and wait for them to finish. | |
| LogTestBackgroundServiceEvent("f1"); | |
| LogTestBackgroundServiceEvent("f2"); | |
| storage_control().FlushForTesting(); | |
| // Check the values. | |
| auto feature_events = GetLoggedBackgroundServiceEvents(); | |
| ASSERT_EQ(feature_events.size(), 2u); | |
| ClearLoggedBackgroundServiceEvents(); | |
| storage_control().FlushForTesting(); | |
| // Should be empty now. | |
| feature_events = GetLoggedBackgroundServiceEvents(); | |
| EXPECT_TRUE(feature_events.empty()); | |
| } | |
| TEST_F(DevToolsBackgroundServicesContextTest, EventObserverCalled) { | |
| { | |
| EXPECT_CALL(*this, OnEventReceived(_)).Times(0); | |
| LogTestBackgroundServiceEvent("f1"); | |
| task_environment_.RunUntilIdle(); | |
| } | |
| StartRecording(); | |
| { | |
| EXPECT_CALL(*this, OnEventReceived(_)); | |
| LogTestBackgroundServiceEvent("f2"); | |
| task_environment_.RunUntilIdle(); | |
| } | |
| } | |
| } // namespace content |