| // Copyright 2021 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/notifications/mac/notification_dispatcher_mojo.h" |
| |
| #include <memory> |
| #include <optional> |
| |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/path_service.h" |
| #include "base/process/process_handle.h" |
| #include "base/test/gmock_callback_support.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/mock_callback.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/notifications/mac/mac_notification_provider_factory.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "chrome/common/notifications/notification_operation.h" |
| #include "chrome/services/mac_notifications/public/mojom/mac_notifications.mojom.h" |
| #include "chrome/test/base/testing_browser_process.h" |
| #include "chrome/test/base/testing_profile_manager.h" |
| #include "content/public/test/browser_task_environment.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/receiver.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace { |
| |
| const char kNotificationId[] = "notification-id"; |
| const char kProfileId[] = "profile-id"; |
| |
| class MockNotificationService |
| : public mac_notifications::mojom::MacNotificationService { |
| public: |
| MOCK_METHOD(void, |
| DisplayNotification, |
| (mac_notifications::mojom::NotificationPtr), |
| (override)); |
| MOCK_METHOD(void, |
| GetDisplayedNotifications, |
| (mac_notifications::mojom::ProfileIdentifierPtr, |
| const std::optional<GURL>& origin, |
| GetDisplayedNotificationsCallback), |
| (override)); |
| MOCK_METHOD(void, |
| CloseNotification, |
| (mac_notifications::mojom::NotificationIdentifierPtr), |
| (override)); |
| MOCK_METHOD(void, |
| CloseNotificationsForProfile, |
| (mac_notifications::mojom::ProfileIdentifierPtr), |
| (override)); |
| MOCK_METHOD(void, CloseAllNotifications, (), (override)); |
| MOCK_METHOD(void, |
| OkayToTerminateService, |
| (OkayToTerminateServiceCallback), |
| (override)); |
| }; |
| |
| class MockNotificationProvider |
| : public mac_notifications::mojom::MacNotificationProvider { |
| public: |
| MOCK_METHOD( |
| void, |
| BindNotificationService, |
| (mojo::PendingReceiver<mac_notifications::mojom::MacNotificationService>, |
| mojo::PendingRemote< |
| mac_notifications::mojom::MacNotificationActionHandler>), |
| (override)); |
| }; |
| |
| class FakeMacNotificationProviderFactory |
| : public MacNotificationProviderFactory, |
| public mac_notifications::mojom::MacNotificationProvider { |
| public: |
| explicit FakeMacNotificationProviderFactory( |
| base::RepeatingClosure on_disconnect) |
| : MacNotificationProviderFactory( |
| mac_notifications::NotificationStyle::kAlert), |
| on_disconnect_(std::move(on_disconnect)) {} |
| ~FakeMacNotificationProviderFactory() override = default; |
| |
| // MacNotificationProviderFactory: |
| mojo::Remote<mac_notifications::mojom::MacNotificationProvider> |
| LaunchProvider() override { |
| mojo::Remote<mac_notifications::mojom::MacNotificationProvider> remote; |
| provider_receiver_.Bind(remote.BindNewPipeAndPassReceiver()); |
| provider_receiver_.set_disconnect_handler( |
| base::BindOnce(&FakeMacNotificationProviderFactory::Disconnect, |
| base::Unretained(this))); |
| return remote; |
| } |
| |
| // mac_notifications::mojom::MacNotificationProvider: |
| void BindNotificationService( |
| mojo::PendingReceiver<mac_notifications::mojom::MacNotificationService> |
| service, |
| mojo::PendingRemote< |
| mac_notifications::mojom::MacNotificationActionHandler> handler) |
| override { |
| service_receiver_.Bind(std::move(service)); |
| handler_remote_.Bind(std::move(handler)); |
| } |
| |
| MockNotificationService& service() { return mock_service_; } |
| |
| mac_notifications::mojom::MacNotificationActionHandler* handler() { |
| return handler_remote_.get(); |
| } |
| |
| void Disconnect() { |
| handler_remote_.reset(); |
| service_receiver_.reset(); |
| provider_receiver_.reset(); |
| on_disconnect_.Run(); |
| } |
| |
| bool is_service_connected() { return service_receiver_.is_bound(); } |
| |
| private: |
| base::RepeatingClosure on_disconnect_; |
| mojo::Receiver<mac_notifications::mojom::MacNotificationProvider> |
| provider_receiver_{this}; |
| MockNotificationService mock_service_; |
| mojo::Receiver<mac_notifications::mojom::MacNotificationService> |
| service_receiver_{&mock_service_}; |
| mojo::Remote<mac_notifications::mojom::MacNotificationActionHandler> |
| handler_remote_; |
| }; |
| |
| message_center::Notification CreateNotification() { |
| return message_center::Notification( |
| message_center::NOTIFICATION_TYPE_SIMPLE, kNotificationId, u"title", |
| u"message", /*icon=*/ui::ImageModel(), |
| /*display_source=*/std::u16string(), /*origin_url=*/GURL(), |
| message_center::NotifierId(), message_center::RichNotificationData(), |
| base::MakeRefCounted<message_center::NotificationDelegate>()); |
| } |
| |
| mac_notifications::mojom::NotificationMetadataPtr CreateNotificationMetadata() { |
| auto profile_identifier = mac_notifications::mojom::ProfileIdentifier::New( |
| kProfileId, /*incognito=*/true); |
| auto notification_identifier = |
| mac_notifications::mojom::NotificationIdentifier::New( |
| kNotificationId, std::move(profile_identifier)); |
| base::FilePath user_data_dir; |
| EXPECT_TRUE(base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir)); |
| return mac_notifications::mojom::NotificationMetadata::New( |
| std::move(notification_identifier), /*notification_type=*/0, |
| /*origin_url=*/GURL("https://example.com"), user_data_dir.value()); |
| } |
| |
| mac_notifications::mojom::NotificationActionInfoPtr |
| CreateNotificationActionInfo() { |
| auto meta = CreateNotificationMetadata(); |
| return mac_notifications::mojom::NotificationActionInfo::New( |
| std::move(meta), NotificationOperation::kClick, |
| /*button_index=*/-1, /*reply=*/std::nullopt); |
| } |
| |
| } // namespace |
| |
| class NotificationDispatcherMojoTest : public testing::Test { |
| public: |
| NotificationDispatcherMojoTest() = default; |
| |
| ~NotificationDispatcherMojoTest() override { |
| base::RunLoop run_loop; |
| ExpectDisconnect(run_loop.QuitClosure()); |
| provider_factory_->Disconnect(); |
| run_loop.Run(); |
| } |
| |
| // testing::Test: |
| void SetUp() override { |
| ASSERT_TRUE(testing_profile_manager_.SetUp()); |
| profile_ = testing_profile_manager_.CreateTestingProfile("profile"); |
| |
| auto provider_factory = |
| std::make_unique<FakeMacNotificationProviderFactory>( |
| on_disconnect_.Get()); |
| provider_factory_ = provider_factory.get(); |
| |
| // NotificationDispatcherMojo will query if it can terminate the service |
| // at startup. Once that finishes it should disconnect due to inactivity. |
| base::RunLoop run_loop; |
| EmulateOkayToTerminate(/*can_terminate=*/true); |
| ExpectDisconnect(run_loop.QuitClosure()); |
| |
| notification_dispatcher_ = std::make_unique<NotificationDispatcherMojo>( |
| std::move(provider_factory)); |
| run_loop.Run(); |
| } |
| |
| MockNotificationService& service() { return provider_factory_->service(); } |
| |
| mac_notifications::mojom::MacNotificationActionHandler* handler() { |
| return provider_factory_->handler(); |
| } |
| |
| protected: |
| void ExpectDisconnect(base::OnceClosure callback) { |
| EXPECT_CALL(on_disconnect_, Run()) |
| .WillOnce(base::test::RunOnceClosure(std::move(callback))) |
| .WillRepeatedly(testing::DoDefault()); |
| } |
| |
| void ExpectKeepConnected() { |
| EXPECT_CALL(on_disconnect_, Run()).Times(0); |
| // Run remaining tasks to see if we get disconnected. |
| task_environment_.RunUntilIdle(); |
| } |
| |
| void EmulateOkayToTerminate(bool can_terminate, |
| base::OnceClosure callback = base::DoNothing()) { |
| EXPECT_CALL(service(), OkayToTerminateService) |
| .WillRepeatedly(testing::DoAll( |
| base::test::RunOnceClosure(std::move(callback)), |
| [can_terminate]( |
| MockNotificationService::OkayToTerminateServiceCallback |
| callback) { std::move(callback).Run(can_terminate); })); |
| } |
| |
| void DisplayNotificationSync() { |
| base::RunLoop run_loop; |
| EXPECT_CALL(service(), DisplayNotification) |
| .WillOnce(base::test::RunOnceClosure(run_loop.QuitClosure())); |
| notification_dispatcher_->DisplayNotification( |
| NotificationHandler::Type::WEB_PERSISTENT, profile_, |
| CreateNotification()); |
| run_loop.Run(); |
| } |
| |
| content::BrowserTaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| TestingProfileManager testing_profile_manager_{ |
| TestingBrowserProcess::GetGlobal()}; |
| raw_ptr<Profile> profile_; |
| base::MockRepeatingClosure on_disconnect_; |
| std::unique_ptr<NotificationDispatcherMojo> notification_dispatcher_; |
| raw_ptr<FakeMacNotificationProviderFactory> provider_factory_ = nullptr; |
| }; |
| |
| TEST_F(NotificationDispatcherMojoTest, CloseNotificationAndDisconnect) { |
| DisplayNotificationSync(); |
| |
| base::RunLoop run_loop; |
| // Expect that we disconnect after closing the last notification. |
| ExpectDisconnect(run_loop.QuitClosure()); |
| EXPECT_CALL(service(), CloseNotification) |
| .WillOnce( |
| [](mac_notifications::mojom::NotificationIdentifierPtr identifier) { |
| EXPECT_EQ(kNotificationId, identifier->id); |
| EXPECT_EQ(kProfileId, identifier->profile->id); |
| EXPECT_TRUE(identifier->profile->incognito); |
| }); |
| EmulateOkayToTerminate(/*can_terminate=*/true); |
| notification_dispatcher_->CloseNotificationWithId( |
| {kNotificationId, kProfileId, /*incognito=*/true}); |
| run_loop.Run(); |
| } |
| |
| TEST_F(NotificationDispatcherMojoTest, CloseNotificationAndKeepConnected) { |
| DisplayNotificationSync(); |
| |
| base::RunLoop run_loop; |
| // Expect that we continue running if there are remaining notifications. |
| EXPECT_CALL(service(), CloseNotification); |
| EmulateOkayToTerminate(/*can_terminate=*/false, run_loop.QuitClosure()); |
| notification_dispatcher_->CloseNotificationWithId( |
| {kNotificationId, kProfileId, /*incognito=*/true}); |
| run_loop.Run(); |
| ExpectKeepConnected(); |
| } |
| |
| TEST_F(NotificationDispatcherMojoTest, |
| CloseThenDispatchNotificationAndKeepConnected) { |
| base::RunLoop run_loop; |
| DisplayNotificationSync(); |
| |
| // Expect that we continue running when showing a new notification just after |
| // closing the last one. |
| EXPECT_CALL(service(), CloseNotification); |
| EmulateOkayToTerminate(/*can_terminate=*/true); |
| notification_dispatcher_->CloseNotificationWithId( |
| {kNotificationId, kProfileId, /*incognito=*/true}); |
| |
| EXPECT_CALL(service(), DisplayNotification) |
| .WillOnce(base::test::RunOnceClosure(run_loop.QuitClosure())); |
| notification_dispatcher_->DisplayNotification( |
| NotificationHandler::Type::WEB_PERSISTENT, profile_, |
| CreateNotification()); |
| |
| run_loop.Run(); |
| ExpectKeepConnected(); |
| |
| base::RunLoop run_loop2; |
| // Expect that we disconnect after closing all notifications. |
| ExpectDisconnect(run_loop2.QuitClosure()); |
| EXPECT_CALL(service(), CloseAllNotifications); |
| notification_dispatcher_->CloseAllNotifications(); |
| run_loop2.Run(); |
| } |
| |
| TEST_F(NotificationDispatcherMojoTest, CloseProfileNotificationsAndDisconnect) { |
| DisplayNotificationSync(); |
| |
| base::RunLoop run_loop; |
| // Expect that we disconnect after closing the last notification. |
| ExpectDisconnect(run_loop.QuitClosure()); |
| EXPECT_CALL(service(), CloseNotificationsForProfile) |
| .WillOnce([](mac_notifications::mojom::ProfileIdentifierPtr profile) { |
| EXPECT_EQ(kProfileId, profile->id); |
| EXPECT_TRUE(profile->incognito); |
| }); |
| EmulateOkayToTerminate(/*can_terminate=*/true); |
| notification_dispatcher_->CloseNotificationsWithProfileId(kProfileId, |
| /*incognito=*/true); |
| run_loop.Run(); |
| } |
| |
| TEST_F(NotificationDispatcherMojoTest, CloseAndDisconnectTiming) { |
| base::HistogramTester histograms; |
| // Show a new notification. |
| EXPECT_CALL(service(), DisplayNotification); |
| notification_dispatcher_->DisplayNotification( |
| NotificationHandler::Type::WEB_PERSISTENT, profile_, |
| CreateNotification()); |
| |
| // Wait for 30 seconds and close the notification. |
| auto delay = base::Seconds(30); |
| task_environment_.FastForwardBy(delay); |
| |
| // Expect that we disconnect after closing the last notification. |
| base::RunLoop run_loop; |
| ExpectDisconnect(run_loop.QuitClosure()); |
| EmulateOkayToTerminate(/*can_terminate=*/true); |
| EXPECT_CALL(service(), CloseNotification); |
| notification_dispatcher_->CloseNotificationWithId( |
| {kNotificationId, kProfileId, /*incognito=*/true}); |
| |
| // Verify that we log the runtime length and no unexpected kill. |
| run_loop.Run(); |
| histograms.ExpectUniqueTimeSample("Notifications.macOS.ServiceProcessRuntime", |
| delay, /*expected_count=*/1); |
| histograms.ExpectTotalCount("Notifications.macOS.ServiceProcessKilled", |
| /*count=*/0); |
| } |
| |
| TEST_F(NotificationDispatcherMojoTest, KillServiceTiming) { |
| base::HistogramTester histograms; |
| // Show a new notification. |
| EXPECT_CALL(service(), DisplayNotification); |
| notification_dispatcher_->DisplayNotification( |
| NotificationHandler::Type::WEB_PERSISTENT, profile_, |
| CreateNotification()); |
| |
| // Wait for 30 seconds and terminate the service. |
| auto delay = base::Seconds(30); |
| task_environment_.FastForwardBy(delay); |
| // Simulate a dying service process. |
| provider_factory_->Disconnect(); |
| |
| // Run remaining tasks as we can't observe the disconnect callback. |
| task_environment_.RunUntilIdle(); |
| // Verify that we log the runtime length and an unexpected kill. |
| histograms.ExpectUniqueTimeSample("Notifications.macOS.ServiceProcessRuntime", |
| delay, /*expected_count=*/1); |
| histograms.ExpectUniqueTimeSample("Notifications.macOS.ServiceProcessKilled", |
| delay, /*expected_count=*/1); |
| } |
| |
| TEST_F(NotificationDispatcherMojoTest, DidActivateNotification) { |
| base::HistogramTester histograms; |
| // Show a new notification. |
| EmulateOkayToTerminate(/*can_terminate=*/true); |
| EXPECT_CALL(service(), DisplayNotification); |
| notification_dispatcher_->DisplayNotification( |
| NotificationHandler::Type::WEB_PERSISTENT, profile_, |
| CreateNotification()); |
| |
| // Wait until the action handler has been bound. |
| task_environment_.RunUntilIdle(); |
| handler()->OnNotificationAction(CreateNotificationActionInfo()); |
| |
| // Handling responses is async, make sure we wait for all tasks to complete. |
| task_environment_.RunUntilIdle(); |
| } |
| |
| TEST_F(NotificationDispatcherMojoTest, TestUnexpectedDisconnectReconnects) { |
| // Display a notification and verify that the service is running. |
| DisplayNotificationSync(); |
| EXPECT_TRUE(provider_factory_->is_service_connected()); |
| |
| // Disconnect after 30 seconds while there is still a notification on screen. |
| task_environment_.FastForwardBy(base::Seconds(30)); |
| provider_factory_->Disconnect(); |
| EXPECT_FALSE(provider_factory_->is_service_connected()); |
| |
| // Expect the service to be restarted after a short timeout. |
| EmulateOkayToTerminate(/*can_terminate=*/false); |
| task_environment_.FastForwardBy(base::Milliseconds(500)); |
| EXPECT_TRUE(provider_factory_->is_service_connected()); |
| } |
| |
| TEST_F(NotificationDispatcherMojoTest, TestReconnectBackoff) { |
| // Display a notification and verify that the service is running. |
| DisplayNotificationSync(); |
| // Disconnect after 30 seconds while there is still a notification on screen. |
| task_environment_.FastForwardBy(base::Seconds(30)); |
| provider_factory_->Disconnect(); |
| |
| // Verify the service hasn't restarted if not enough time has passed. |
| task_environment_.FastForwardBy(base::Milliseconds(499)); |
| EXPECT_FALSE(provider_factory_->is_service_connected()); |
| // Expect the service to be restarted after a short timeout. |
| EmulateOkayToTerminate(/*can_terminate=*/false); |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| EXPECT_TRUE(provider_factory_->is_service_connected()); |
| |
| // Disconnect again immediately which should double the restart timeout. |
| provider_factory_->Disconnect(); |
| |
| // Verify the service hasn't restarted if not enough time has passed. |
| task_environment_.FastForwardBy(base::Milliseconds(999)); |
| EXPECT_FALSE(provider_factory_->is_service_connected()); |
| // Expect the service to be restarted after a short timeout. |
| EmulateOkayToTerminate(/*can_terminate=*/false); |
| task_environment_.FastForwardBy(base::Milliseconds(1)); |
| EXPECT_TRUE(provider_factory_->is_service_connected()); |
| } |