chromium/chrome/browser/notifications/mac/notification_dispatcher_mojo_unittest.cc

// 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());
}