chromium/chrome/services/mac_notifications/mac_notification_service_un_unittest.mm

// 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.

#import <UserNotifications/UserNotifications.h>

#include <string>
#include <utility>
#include <vector>

#include "base/apple/bundle_locations.h"
#include "base/run_loop.h"
#include "base/strings/sys_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "chrome/common/notifications/notification_constants.h"
#include "chrome/common/notifications/notification_operation.h"
#import "chrome/services/mac_notifications/mac_notification_service_un.h"
#import "chrome/services/mac_notifications/mac_notification_service_utils.h"
#import "chrome/services/mac_notifications/notification_test_utils_mac.h"
#include "chrome/services/mac_notifications/public/mojom/mac_notifications.mojom.h"
#include "chrome/services/mac_notifications/un_user_notifications_spi.h"
#include "chrome/services/mac_notifications/unnotification_metrics.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"
#include "testing/gtest_mac.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
#import "third_party/ocmock/ocmock_extensions.h"
#include "ui/gfx/image/image_skia.h"
#include "url/gurl.h"

namespace mac_notifications {

namespace {

struct NotificationActionParams {
  NSString* action_identifier;
  NotificationOperation operation;
  int button_index;
  std::optional<std::u16string> reply;
};

class MockNotificationActionHandler
    : public mojom::MacNotificationActionHandler {
 public:
  // mojom::MacNotificationActionHandler:
  MOCK_METHOD(void,
              OnNotificationAction,
              (mojom::NotificationActionInfoPtr),
              (override));
};

// Returns a block that invokes a closure. Used to invoke a closure when certain
// mock methods are called.
using InvocationBlock = void (^)(NSInvocation* invocation);
InvocationBlock invokeClosure(base::OnceClosure closure) {
  __block auto block_closure = std::move(closure);
  return ^(NSInvocation* invocation) {
    std::move(block_closure).Run();
  };
}

}  // namespace

class MacNotificationServiceUNTest : public testing::Test {
 public:
  MacNotificationServiceUNTest() {
    mock_notification_center_ =
        [OCMockObject mockForClass:[UNUserNotificationCenter class]];
    // Expect the MacNotificationServiceUN ctor to register a delegate with
    // the UNNotificationCenter and ask for notification permissions.
    ExpectAndUpdateUNUserNotificationCenterDelegate(/*expect_not_nil=*/true);
    id settings = OCMClassMock([UNNotificationSettings class]);
    OCMStub([mock_notification_center_
        getNotificationSettingsWithCompletionHandler:
            ([OCMArg invokeBlockWithArgs:settings, nil])]);

    // We also synchronize displayed notifications and categories.
    OCMExpect([mock_notification_center_
        getDeliveredNotificationsWithCompletionHandler:
            ([OCMArg invokeBlockWithArgs:@[], nil])]);
    OCMExpect([mock_notification_center_
        getNotificationCategoriesWithCompletionHandler:
            ([OCMArg invokeBlockWithArgs:@[], nil])]);

    service_ = std::make_unique<MacNotificationServiceUN>(
        handler_receiver_.BindNewPipeAndPassRemote(), base::DoNothing(),
        mock_notification_center_);
    service_->Bind(service_remote_.BindNewPipeAndPassReceiver());
    OCMStub([mock_notification_center_
        setNotificationCategories:[OCMArg checkWithBlock:^BOOL(
                                              NSSet<UNNotificationCategory*>*
                                                  categories) {
          category_count_ = [categories count];
          return YES;
        }]]);
    EXPECT_OCMOCK_VERIFY(mock_notification_center_);

    OCMExpect(
        [mock_notification_center_
            requestAuthorizationWithOptions:0
                          completionHandler:
                              ([OCMArg
                                  invokeBlockWithArgs:[NSNumber
                                                          numberWithBool:YES],
                                                      [NSNull null], nil])])
        .ignoringNonObjectArgs();
    base::test::TestFuture<mojom::RequestPermissionResult> permission_result;
    service_->RequestPermission(permission_result.GetCallback());
    EXPECT_TRUE(permission_result.Wait());
    EXPECT_OCMOCK_VERIFY(mock_notification_center_);
  }

  ~MacNotificationServiceUNTest() override {
    if (service_) {
      ResetService();
    }
  }

  void ResetService() {
    // Expect the MacNotificationServiceUN dtor to clear the delegate from the
    // UNNotificationCenter.
    ExpectAndUpdateUNUserNotificationCenterDelegate(/*expect_not_nil=*/false);
    service_.reset();
    EXPECT_OCMOCK_VERIFY(mock_notification_center_);
  }

 protected:
  void ExpectAndUpdateUNUserNotificationCenterDelegate(bool expect_not_nil) {
    OCMExpect([mock_notification_center_
        setDelegate:[OCMArg checkWithBlock:^BOOL(
                                id<UNUserNotificationCenterDelegate> delegate) {
          EXPECT_EQ(expect_not_nil, delegate != nil);
          notification_center_delegate_ = delegate;
          return YES;
        }]]);
  }

  FakeUNNotification* CreateNotification(const std::string& notification_id,
                                         const std::string& profile_id,
                                         bool incognito,
                                         bool display = true,
                                         const std::string& category_id = "",
                                         const GURL& origin = {}) {
    NSString* identifier = base::SysUTF8ToNSString(
        DeriveMacNotificationId(mojom::NotificationIdentifier::New(
            notification_id,
            mojom::ProfileIdentifier::New(profile_id, incognito))));

    UNMutableNotificationContent* content =
        [[UNMutableNotificationContent alloc] init];
    content.userInfo = @{
      kNotificationId : base::SysUTF8ToNSString(notification_id),
      kNotificationProfileId : base::SysUTF8ToNSString(profile_id),
      kNotificationIncognito : [NSNumber numberWithBool:incognito],
      kNotificationOrigin : base::SysUTF8ToNSString(origin.spec()),
    };
    if (!category_id.empty())
      content.categoryIdentifier = base::SysUTF8ToNSString(category_id);

    UNNotificationRequest* request =
        [UNNotificationRequest requestWithIdentifier:identifier
                                             content:content
                                             trigger:nil];

    FakeUNNotification* notification = [[FakeUNNotification alloc] init];
    notification.request = request;

    // Also call the |service_remote_| to setup the new notification. This will
    // make sure that any internal state is updated as well.
    if (display)
      DisplayNotificationSync(notification_id, profile_id, incognito);

    return notification;
  }

  std::vector<FakeUNNotification*> SetupNotifications() {
    std::vector<FakeUNNotification*> notifications = {
        CreateNotification("notificationId", "profileId", /*incognito=*/false,
                           /*display=*/true, /*category_id=*/"",
                           GURL("https://example.com")),
        CreateNotification("notificationId", "profileId2", /*incognito=*/true,
                           /*display=*/true, /*category_id=*/"",
                           GURL("https://example.com")),
        CreateNotification("notificationId2", "profileId", /*incognito=*/true,
                           /*display=*/true, /*category_id=*/"",
                           GURL("https://example.com")),
        CreateNotification("notificationId", "profileId", /*incognito=*/true,
                           /*display=*/true, /*category_id=*/"",
                           GURL("https://gmail.com")),
    };

    NSMutableArray* notifications_ns =
        [NSMutableArray arrayWithCapacity:notifications.size()];
    for (const auto& notification : notifications) {
      [notifications_ns addObject:notification];
    }

    OCMStub([mock_notification_center_
        getDeliveredNotificationsWithCompletionHandler:
            ([OCMArg invokeBlockOnQueue:dispatch_get_main_queue()
                               withArgs:notifications_ns, nil])]);

    return notifications;
  }

  static std::vector<mojom::NotificationIdentifierPtr>
  GetDisplayedNotificationsSync(mojom::MacNotificationService* service,
                                mojom::ProfileIdentifierPtr profile,
                                std::optional<GURL> origin = std::nullopt) {
    base::test::TestFuture<std::vector<mojom::NotificationIdentifierPtr>>
        displayed;
    service->GetDisplayedNotifications(std::move(profile), origin,
                                       displayed.GetCallback());
    return displayed.Take();
  }

  std::vector<mojom::NotificationIdentifierPtr> GetDisplayedNotificationsSync(
      mojom::ProfileIdentifierPtr profile,
      std::optional<GURL> origin = std::nullopt) {
    return GetDisplayedNotificationsSync(service_remote_.get(),
                                         std::move(profile), std::move(origin));
  }

  void DisplayNotificationSync(const std::string& notification_id,
                               const std::string& profile_id,
                               bool incognito,
                               bool success = true) {
    base::RunLoop run_loop;

    OCMExpect(
        [mock_notification_center_
            addNotificationRequest:[OCMArg any]
             withCompletionHandler:
                 ([OCMArg
                     invokeBlockWithArgs:(success
                                              ? [NSNull null]
                                              : [NSError errorWithDomain:@""
                                                                    code:0
                                                                userInfo:nil]),
                                         nil])])
        .andDo(invokeClosure(run_loop.QuitClosure()));

    // Create and display a new notification.
    auto notification =
        CreateMojoNotification(notification_id, profile_id, incognito);
    service_remote_->DisplayNotification(std::move(notification));

    run_loop.Run();
    EXPECT_OCMOCK_VERIFY(mock_notification_center_);
  }

  mac_notifications::mojom::NotificationPtr CreateMojoNotification(
      const std::string& notification_id,
      const std::string& profile_id,
      bool incognito,
      bool renotify = true) {
    auto notification_identifier = mojom::NotificationIdentifier::New(
        notification_id, mojom::ProfileIdentifier::New(profile_id, incognito));
    auto meta = mojom::NotificationMetadata::New(
        std::move(notification_identifier), /*type=*/0, /*origin_url=*/GURL(),
        /*user_data_dir=*/"");
    std::vector<mac_notifications::mojom::NotificationActionButtonPtr> buttons;

    return mac_notifications::mojom::Notification::New(
        std::move(meta), u"title", u"subtitle", u"body", renotify,
        /*show_settings_button=*/true, std::move(buttons),
        /*icon=*/gfx::ImageSkia());
  }

  id CreateMockNotificationSettings(UNAuthorizationStatus status) {
    id settings = OCMClassMock([UNNotificationSettings class]);
    OCMStub([settings authorizationStatus]).andReturn(status);
    return settings;
  }

  id CreateMockNotificationCenter(
      id settings,
      NSArray<UNNotification*>* notifications = nil,
      NSArray<UNNotificationCategory*>* categories = nil) {
    id mock_notification_center =
        [OCMockObject mockForClass:[UNUserNotificationCenter class]];

    OCMStub([mock_notification_center setDelegate:[OCMArg any]]);
    OCMStub([mock_notification_center
        getNotificationSettingsWithCompletionHandler:
            ([OCMArg invokeBlockWithArgs:settings, nil])]);
    OCMStub([mock_notification_center
        getDeliveredNotificationsWithCompletionHandler:
            ([OCMArg invokeBlockWithArgs:(notifications ? notifications : @[]),
                                         nil])]);
    OCMStub([mock_notification_center
        getNotificationCategoriesWithCompletionHandler:
            ([OCMArg
                invokeBlockWithArgs:(categories ? categories : @[]), nil])]);
    return mock_notification_center;
  }

  // Creates a new service and destroys it immediately. This is used to test any
  // metrics logged during construction of the service. Tests can optionally
  // pass in an |on_create| callback to do further checks before the service is
  // destroyed.
  void CreateAndDestroyService(
      mojom::RequestPermissionResult result,
      NSArray<UNNotification*>* notifications = nil,
      NSArray<UNNotificationCategory*>* categories = nil,
      base::OnceCallback<void(MacNotificationServiceUN*)> on_create =
          base::NullCallback()) {
    UNAuthorizationStatus status =
        result == mojom::RequestPermissionResult::kPermissionPreviouslyDenied
            ? UNAuthorizationStatusDenied
        : result == mojom::RequestPermissionResult::kPermissionPreviouslyGranted
            ? UNAuthorizationStatusAuthorized
            : UNAuthorizationStatusNotDetermined;
    id mock_notification_center = CreateMockNotificationCenter(
        CreateMockNotificationSettings(status), notifications, categories);

    MockNotificationActionHandler mock_handler;
    mojo::Receiver<mojom::MacNotificationActionHandler> handler_receiver{
        &mock_handler};
    mojo::Remote<mojom::MacNotificationService> service_remote;

    if (result != mojom::RequestPermissionResult::kPermissionPreviouslyDenied &&
        result !=
            mojom::RequestPermissionResult::kPermissionPreviouslyGranted) {
      bool granted =
          result == mojom::RequestPermissionResult::kPermissionGranted;
      id error = (result == mojom::RequestPermissionResult::kRequestFailed ||
                  result == mojom::RequestPermissionResult::kPermissionDenied)
                     ? [NSError errorWithDomain:@"" code:0 userInfo:nil]
                     : NSNull.null;
      OCMExpect(
          [mock_notification_center
              requestAuthorizationWithOptions:0
                            completionHandler:
                                ([OCMArg invokeBlockWithArgs:@(granted), error,
                                                             nil])])
          .ignoringNonObjectArgs();
    }

    auto service = std::make_unique<MacNotificationServiceUN>(
        handler_receiver.BindNewPipeAndPassRemote(), base::DoNothing(),
        mock_notification_center);
    service->Bind(service_remote.BindNewPipeAndPassReceiver());
    base::test::TestFuture<mojom::RequestPermissionResult> permission_result;
    service->RequestPermission(permission_result.GetCallback());
    EXPECT_EQ(result, permission_result.Get());
    if (on_create)
      std::move(on_create).Run(service.get());

    service.reset();
    EXPECT_OCMOCK_VERIFY(mock_notification_center);
  }

  base::test::TaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME,
      base::test::TaskEnvironment::MainThreadType::UI};
  MockNotificationActionHandler mock_handler_;
  mojo::Receiver<mojom::MacNotificationActionHandler> handler_receiver_{
      &mock_handler_};
  mojo::Remote<mojom::MacNotificationService> service_remote_;
  id mock_notification_center_ = nil;
  id<UNUserNotificationCenterDelegate> notification_center_delegate_ = nullptr;
  std::unique_ptr<MacNotificationServiceUN> service_;
  unsigned int category_count_ = 0u;
};

TEST_F(MacNotificationServiceUNTest, DisplayNotification) {
  base::RunLoop run_loop;

  // Verify notification content.
  OCMExpect(
      [mock_notification_center_
          addNotificationRequest:[OCMArg checkWithBlock:^BOOL(
                                             UNNotificationRequest* request) {
            EXPECT_NSEQ(@"i|profileId|notificationId", [request identifier]);
            NSDictionary* user_info = [[request content] userInfo];
            EXPECT_NSEQ(@"notificationId",
                        [user_info objectForKey:kNotificationId]);
            EXPECT_NSEQ(@"profileId",
                        [user_info objectForKey:kNotificationProfileId]);
            EXPECT_TRUE(
                [[user_info objectForKey:kNotificationIncognito] boolValue]);

            EXPECT_NSEQ(@"title", [[request content] title]);
            EXPECT_NSEQ(@"subtitle", [[request content] subtitle]);
            EXPECT_NSEQ(@"body", [[request content] body]);
            return YES;
          }]
           withCompletionHandler:[OCMArg any]])
      .andDo(invokeClosure(run_loop.QuitClosure()));

  // Create and display a new notification.
  auto notification = CreateMojoNotification("notificationId", "profileId",
                                             /*incognito=*/true);
  service_remote_->DisplayNotification(std::move(notification));

  run_loop.Run();
  EXPECT_OCMOCK_VERIFY(mock_notification_center_);

  // Expect a new notification category for this notification.
  EXPECT_EQ(1u, category_count_);
}

TEST_F(MacNotificationServiceUNTest, RedisplayNotification) {
  auto notification_no_renotify =
      CreateMojoNotification("notificationId", "profileId",
                             /*incognito=*/false, /*renotify=*/false);
  auto notification_with_renotify =
      CreateMojoNotification("notificationId", "profileId",
                             /*incognito=*/false, /*renotify=*/true);

  auto verify_notification_content = [](UNNotificationContent* content) {
    NSDictionary* user_info = content.userInfo;
    EXPECT_NSEQ(@"notificationId", [user_info objectForKey:kNotificationId]);
    EXPECT_NSEQ(@"profileId", [user_info objectForKey:kNotificationProfileId]);
    EXPECT_FALSE([[user_info objectForKey:kNotificationIncognito] boolValue]);

    EXPECT_NSEQ(@"title", content.title);
    EXPECT_NSEQ(@"subtitle", content.subtitle);
    EXPECT_NSEQ(@"body", content.body);
  };

  {
    // Display a new notification.
    base::RunLoop run_loop;

    OCMExpect(
        [mock_notification_center_
            addNotificationRequest:[OCMArg checkWithBlock:^BOOL(
                                               UNNotificationRequest* request) {
              EXPECT_NSEQ(@"r|profileId|notificationId", request.identifier);
              verify_notification_content(request.content);
              return YES;
            }]
             withCompletionHandler:[OCMArg any]])
        .andDo(invokeClosure(run_loop.QuitClosure()));

    service_remote_->DisplayNotification(notification_no_renotify.Clone());

    run_loop.Run();
    EXPECT_OCMOCK_VERIFY(mock_notification_center_);
  }

  {
    // Now display the same notification again, with redisplay set to false.
    // This should query the currently displayed notifications, and only cause
    // the contents to be replaced if the notification is still delivered.
    base::RunLoop run_loop;

    OCMExpect([mock_notification_center_
        getDeliveredNotificationsWithCompletionHandler:
            ([OCMArg invokeBlockWithArgs:@[
              CreateNotification("notificationId", "profileId",
                                 /*incognito=*/false, /*display=*/false)
            ],
                                         nil])]);
    OCMExpect(
        [mock_notification_center_
            replaceContentForRequestWithIdentifier:@"r|profileId|notificationId"
                                replacementContent:
                                    [OCMArg
                                        checkWithBlock:^BOOL(
                                            UNNotificationContent* content) {
                                          verify_notification_content(content);
                                          return YES;
                                        }]
                                 completionHandler:[OCMArg any]])
        .andDo(invokeClosure(run_loop.QuitClosure()));
    service_remote_->DisplayNotification(notification_no_renotify.Clone());

    run_loop.Run();
    EXPECT_OCMOCK_VERIFY(mock_notification_center_);
  }

  {
    // Now display the notification with renotify set to true.
    base::RunLoop run_loop;

    OCMExpect(
        [mock_notification_center_
            addNotificationRequest:[OCMArg checkWithBlock:^BOOL(
                                               UNNotificationRequest* request) {
              EXPECT_NSEQ(@"r|profileId|notificationId", request.identifier);
              verify_notification_content(request.content);
              return YES;
            }]
             withCompletionHandler:[OCMArg any]])
        .andDo(invokeClosure(run_loop.QuitClosure()));
    service_remote_->DisplayNotification(notification_with_renotify.Clone());

    run_loop.Run();
    EXPECT_OCMOCK_VERIFY(mock_notification_center_);
  }

  {
    // Finally, display the notification with renotify set to false, but with
    // the notification having been closed by the OS in the meantime,
    base::RunLoop run_loop;

    OCMExpect([mock_notification_center_
        getDeliveredNotificationsWithCompletionHandler:
            ([OCMArg invokeBlockWithArgs:@[], nil])]);
    OCMExpect(
        [mock_notification_center_
            addNotificationRequest:[OCMArg checkWithBlock:^BOOL(
                                               UNNotificationRequest* request) {
              EXPECT_NSEQ(@"r|profileId|notificationId", request.identifier);
              verify_notification_content(request.content);
              return YES;
            }]
             withCompletionHandler:[OCMArg any]])
        .andDo(invokeClosure(run_loop.QuitClosure()));
    service_remote_->DisplayNotification(notification_no_renotify.Clone());

    run_loop.Run();
    EXPECT_OCMOCK_VERIFY(mock_notification_center_);
  }
}

TEST_F(MacNotificationServiceUNTest, Rebind) {
  base::RunLoop run_loop;

  // Reconnnect to the same MacNotificationServiceUNTest instance.
  service_remote_.reset();
  service_->Bind(service_remote_.BindNewPipeAndPassReceiver());

  // Verify notification is created..
  OCMExpect([mock_notification_center_ addNotificationRequest:[OCMArg any]
                                        withCompletionHandler:[OCMArg any]])
      .andDo(invokeClosure(run_loop.QuitClosure()));

  // Create and display a new notification.
  auto notification = CreateMojoNotification("notificationId", "profileId",
                                             /*incognito=*/true);
  service_remote_->DisplayNotification(std::move(notification));

  run_loop.Run();
  EXPECT_OCMOCK_VERIFY(mock_notification_center_);
}

TEST_F(MacNotificationServiceUNTest, GetDisplayedNotificationsForProfile) {
  auto notifications = SetupNotifications();
  auto profile = mojom::ProfileIdentifier::New("profileId", /*incognito=*/true);
  auto displayed = GetDisplayedNotificationsSync(std::move(profile));
  ASSERT_EQ(2u, displayed.size());

  std::set<std::string> notification_ids;
  for (const auto& notification : displayed) {
    ASSERT_TRUE(notification->profile);
    EXPECT_EQ("profileId", notification->profile->id);
    EXPECT_TRUE(notification->profile->incognito);
    notification_ids.insert(notification->id);
  }

  ASSERT_EQ(2u, notification_ids.size());
  EXPECT_EQ(1u, notification_ids.count("notificationId"));
  EXPECT_EQ(1u, notification_ids.count("notificationId2"));
}

TEST_F(MacNotificationServiceUNTest,
       GetDisplayedNotificationsForProfileAndOrigin) {
  auto notifications = SetupNotifications();
  auto profile = mojom::ProfileIdentifier::New("profileId", /*incognito=*/true);
  auto displayed = GetDisplayedNotificationsSync(std::move(profile),
                                                 GURL("https://example.com"));
  ASSERT_EQ(1u, displayed.size());
  const auto& notification = *displayed.begin();
  ASSERT_TRUE(notification->profile);
  EXPECT_EQ("profileId", notification->profile->id);
  EXPECT_TRUE(notification->profile->incognito);
  EXPECT_EQ("notificationId2", notification->id);
}

TEST_F(MacNotificationServiceUNTest, GetAllDisplayedNotifications) {
  auto notifications = SetupNotifications();
  auto displayed = GetDisplayedNotificationsSync(/*profile=*/nullptr);
  EXPECT_EQ(notifications.size(), displayed.size());
}

TEST_F(MacNotificationServiceUNTest, CloseNotification) {
  DisplayNotificationSync("notificationId", "profileId", /*incognito=*/true);
  EXPECT_EQ(1u, category_count_);

  base::RunLoop run_loop;

  NSString* identifier = @"i|profileId|notificationId";
  OCMExpect([mock_notification_center_
                removeDeliveredNotificationsWithIdentifiers:@[ identifier ]])
      .andDo(invokeClosure(run_loop.QuitClosure()));

  auto profile_identifier =
      mojom::ProfileIdentifier::New("profileId", /*incognito=*/true);
  auto notification_identifier = mojom::NotificationIdentifier::New(
      "notificationId", std::move(profile_identifier));
  service_remote_->CloseNotification(std::move(notification_identifier));

  run_loop.Run();
  EXPECT_OCMOCK_VERIFY(mock_notification_center_);

  // Expect closing the notification to remove the category as well.
  EXPECT_EQ(0u, category_count_);
}

TEST_F(MacNotificationServiceUNTest, SynchronizesNotifications) {
  // Setup 4 notifications that are being returned by system APIs too.
  auto notifications = SetupNotifications();
  ASSERT_EQ(4u, GetDisplayedNotificationsSync(/*profile=*/nullptr).size());

  // Display a notification which won't be reflected in the system APIs.
  DisplayNotificationSync("notificationId3", "profileId", /*incognito=*/true);
  ASSERT_EQ(4u, GetDisplayedNotificationsSync(/*profile=*/nullptr).size());

  // Wait until the notification synchronization timer kicks in and expect it
  // to detect the missing notification.
  base::RunLoop run_loop;
  base::Time start_time = base::Time::Now();
  EXPECT_CALL(mock_handler_, OnNotificationAction)
      .WillOnce([&](mojom::NotificationActionInfoPtr action_info) {
        EXPECT_EQ(NotificationOperation::kClose, action_info->operation);
        EXPECT_EQ(kNotificationInvalidButtonIndex, action_info->button_index);
        EXPECT_EQ("notificationId3", action_info->meta->id->id);
        EXPECT_EQ("profileId", action_info->meta->id->profile->id);
        EXPECT_EQ(MacNotificationServiceUN::kSynchronizationInterval,
                  base::Time::Now() - start_time);
        run_loop.Quit();
      });
  run_loop.Run();
  testing::Mock::VerifyAndClearExpectations(&mock_handler_);
}

TEST_F(MacNotificationServiceUNTest, CloseProfileNotifications) {
  auto notifications = SetupNotifications();
  // Even though we created 3 notifications, all of them share the same
  // category as they have the same actions.
  EXPECT_EQ(1u, category_count_);

  base::RunLoop run_loop;

  NSArray* identifiers = @[
    @"i|profileId|notificationId2",
    @"i|profileId|notificationId",
  ];
  OCMExpect([mock_notification_center_
                removeDeliveredNotificationsWithIdentifiers:identifiers])
      .andDo(invokeClosure(run_loop.QuitClosure()));

  auto profile_identifier =
      mojom::ProfileIdentifier::New("profileId", /*incognito=*/true);
  service_->CloseNotificationsForProfile(std::move(profile_identifier));

  // Reset `service_` to verify that this still works if `service_` gets
  // destroyed while waiting for UNUserNotificationCenter to reply back with
  // the currently displaying notifications.
  ExpectAndUpdateUNUserNotificationCenterDelegate(/*expect_not_nil=*/false);
  service_.reset();

  run_loop.Run();
  EXPECT_OCMOCK_VERIFY(mock_notification_center_);
  // Verify that we closed two notifications but one still holds a reverence
  // to the common category.
  EXPECT_EQ(1u, category_count_);
}

TEST_F(MacNotificationServiceUNTest, CloseAllNotifications) {
  DisplayNotificationSync("notificationId", "profileId", /*incognito=*/true);
  EXPECT_EQ(1u, category_count_);
  base::RunLoop run_loop;
  OCMExpect([mock_notification_center_ removeAllDeliveredNotifications])
      .andDo(invokeClosure(run_loop.QuitClosure()));
  service_remote_->CloseAllNotifications();
  run_loop.Run();
  EXPECT_OCMOCK_VERIFY(mock_notification_center_);
  EXPECT_EQ(0u, category_count_);
}

TEST_F(MacNotificationServiceUNTest, LogsMetricsForAlerts) {
  base::HistogramTester histogram_tester;
  id mainBundleMock =
      [OCMockObject partialMockForObject:base::apple::MainBundle()];

  // Mock the alert style to "alert" and verify we log the correct metrics.
  OCMStub([mainBundleMock infoDictionary]).andReturn(@{
    @"NSUserNotificationAlertStyle" : @"alert"
  });

  // Test does not include kRequestFailed, as currently there is no code path
  // that would result in that error.
  for (auto result :
       {mojom::RequestPermissionResult::kPermissionDenied,
        mojom::RequestPermissionResult::kPermissionGranted,
        mojom::RequestPermissionResult::kPermissionPreviouslyDenied,
        mojom::RequestPermissionResult::kPermissionPreviouslyGranted}) {
    CreateAndDestroyService(result);
    histogram_tester.ExpectBucketCount(
        "Notifications.Permissions.UNNotification.Alert.PermissionRequest",
        /*sample=*/result, /*expected_count=*/1);
  }

  [mainBundleMock stopMocking];
}

TEST_F(MacNotificationServiceUNTest, LogsMetricsForBanners) {
  base::HistogramTester histogram_tester;
  id mainBundleMock =
      [OCMockObject partialMockForObject:base::apple::MainBundle()];

  // Mock the alert style to "banner" and verify we log the correct metrics.
  OCMStub([mainBundleMock infoDictionary]).andReturn(@{
    @"NSUserNotificationAlertStyle" : @"banner"
  });

  // Test does not include kRequestFailed, as currently there is no code path
  // that would result in that error.
  for (auto result :
       {mojom::RequestPermissionResult::kPermissionDenied,
        mojom::RequestPermissionResult::kPermissionGranted,
        mojom::RequestPermissionResult::kPermissionPreviouslyDenied,
        mojom::RequestPermissionResult::kPermissionPreviouslyGranted}) {
    CreateAndDestroyService(result);
    histogram_tester.ExpectBucketCount(
        "Notifications.Permissions.UNNotification.Banner.PermissionRequest",
        /*sample=*/result, /*expected_count=*/1);
  }

  [mainBundleMock stopMocking];
}

TEST_F(MacNotificationServiceUNTest, InitializeDeliveredNotifications) {
  // Create an existing notification with a category that exist before
  // creating a new service.
  UNNotificationCategory* category_ns =
      NotificationCategoryManager::CreateCategory(
          {{{u"Action", /*reply=*/std::nullopt}}, /*settings_button=*/true});
  std::string category_id = base::SysNSStringToUTF8(category_ns.identifier);
  FakeUNNotification* notification =
      CreateNotification("notificationId", "profileId",
                         /*incognito=*/false, /*display=*/false, category_id);
  auto notification_ns = static_cast<UNNotification*>(notification);

  // Expect the service to initialize internal state based on the existing
  // notifications and categories.
  CreateAndDestroyService(
      mojom::RequestPermissionResult::kPermissionGranted, @[ notification_ns ],
      @[ category_ns ], base::BindOnce([](MacNotificationServiceUN* service) {
        auto notifications =
            GetDisplayedNotificationsSync(service, /*profile=*/nullptr);
        ASSERT_EQ(1u, notifications.size());
        EXPECT_EQ("notificationId", notifications[0]->id);
      }));
}

TEST_F(MacNotificationServiceUNTest, OnNotificationAction) {
  // We can't use TEST_P and INSTANTIATE_TEST_SUITE_P as we can't access
  // UNNotificationDefaultActionIdentifier etc. outside an @available block.
  NotificationActionParams kNotificationActionParams[] = {
      {UNNotificationDismissActionIdentifier, NotificationOperation::kClose,
       kNotificationInvalidButtonIndex, /*reply=*/std::nullopt},
      {UNNotificationDefaultActionIdentifier, NotificationOperation::kClick,
       kNotificationInvalidButtonIndex, /*reply=*/std::nullopt},
      {kNotificationButtonOne, NotificationOperation::kClick,
       /*button_index=*/0, /*reply=*/std::nullopt},
      {kNotificationButtonTwo, NotificationOperation::kClick,
       /*button_index=*/1, /*reply=*/std::nullopt},
      {kNotificationSettingsButtonTag, NotificationOperation::kSettings,
       kNotificationInvalidButtonIndex, /*reply=*/std::nullopt},
      {kNotificationButtonOne, NotificationOperation::kClick,
       /*button_index=*/0, u"reply"},
  };

  int i = 0;
  for (const auto& params : kNotificationActionParams) {
    std::string notification_id = base::StringPrintf("notificationId%d", i++);
    FakeUNNotification* notification =
        CreateNotification(notification_id, "profileId",
                           /*incognito=*/false);

    base::RunLoop run_loop;
    EXPECT_CALL(mock_handler_, OnNotificationAction)
        .WillOnce([&](mojom::NotificationActionInfoPtr action_info) {
          EXPECT_EQ(notification_id, action_info->meta->id->id);
          EXPECT_EQ("profileId", action_info->meta->id->profile->id);
          EXPECT_FALSE(action_info->meta->id->profile->incognito);
          EXPECT_EQ(params.operation, action_info->operation);
          EXPECT_EQ(params.button_index, action_info->button_index);
          EXPECT_EQ(params.reply, action_info->reply);
          run_loop.Quit();
        });

    // Simulate a notification action and wait until we acknowledge it.
    base::RunLoop inner_run_loop;
    base::RepeatingClosure inner_quit_closure = inner_run_loop.QuitClosure();

    id response = [OCMockObject
        mockForClass:params.reply ? [UNTextInputNotificationResponse class]
                                  : [UNNotificationResponse class]];
    OCMStub([response actionIdentifier]).andReturn(params.action_identifier);
    OCMStub([response notification]).andReturn(notification);

    if (params.reply) {
      OCMStub([response userText])
          .andReturn(base::SysUTF16ToNSString(*params.reply));
    }

    [notification_center_delegate_
                userNotificationCenter:mock_notification_center_
        didReceiveNotificationResponse:response
                 withCompletionHandler:^() {
                   inner_quit_closure.Run();
                 }];
    inner_run_loop.Run();
    run_loop.Run();
  }
}

TEST_F(MacNotificationServiceUNTest, DidRecentlyHandledClickAction) {
  EXPECT_FALSE(service_->DidRecentlyHandleClickAction());

  // Simulate a notification click.
  FakeUNNotification* notification =
      CreateNotification("notificationId", "profileId",
                         /*incognito=*/false);
  id response = [OCMockObject mockForClass:[UNNotificationResponse class]];
  OCMStub([response actionIdentifier])
      .andReturn(UNNotificationDefaultActionIdentifier);
  OCMStub([response notification]).andReturn(notification);

  base::RunLoop run_loop;
  EXPECT_CALL(mock_handler_, OnNotificationAction)
      .WillOnce([&](mojom::NotificationActionInfoPtr action_info) {
        run_loop.Quit();
      });

  [notification_center_delegate_
              userNotificationCenter:mock_notification_center_
      didReceiveNotificationResponse:response
               withCompletionHandler:^(){
               }];

  EXPECT_TRUE(service_->DidRecentlyHandleClickAction());
  run_loop.Run();
  EXPECT_TRUE(service_->DidRecentlyHandleClickAction());
  task_environment_.FastForwardBy(base::Milliseconds(250));
  EXPECT_FALSE(service_->DidRecentlyHandleClickAction());
}

TEST_F(MacNotificationServiceUNTest,
       PermissionStateChangedCallback_RequestPermission) {
  id mock_notification_center = CreateMockNotificationCenter(
      CreateMockNotificationSettings(UNAuthorizationStatusNotDetermined));

  MockNotificationActionHandler mock_handler;
  mojo::Receiver<mojom::MacNotificationActionHandler> handler_receiver{
      &mock_handler};

  base::test::TestFuture<mojom::PermissionStatus> status;
  auto service = std::make_unique<MacNotificationServiceUN>(
      handler_receiver.BindNewPipeAndPassRemote(),
      status.GetRepeatingCallback(), mock_notification_center);
  EXPECT_EQ(status.Take(), mojom::PermissionStatus::kNotDetermined);

  bool granted = true;
  OCMExpect(
      [mock_notification_center
          requestAuthorizationWithOptions:0
                        completionHandler:([OCMArg
                                              invokeBlockWithArgs:@(granted),
                                                                  NSNull.null,
                                                                  nil])])
      .ignoringNonObjectArgs();
  base::test::TestFuture<mojom::RequestPermissionResult> permission_result;
  service->RequestPermission(permission_result.GetCallback());
  EXPECT_EQ(status.Take(), mojom::PermissionStatus::kPromptPending);
  EXPECT_EQ(permission_result.Get(),
            mojom::RequestPermissionResult::kPermissionGranted);
  EXPECT_EQ(status.Take(), mojom::PermissionStatus::kGranted);
}

TEST_F(MacNotificationServiceUNTest,
       PermissionStateChangedCallback_RequestPermissionPreviouslyDenied) {
  id mock_notification_center = CreateMockNotificationCenter(
      CreateMockNotificationSettings(UNAuthorizationStatusDenied));

  MockNotificationActionHandler mock_handler;
  mojo::Receiver<mojom::MacNotificationActionHandler> handler_receiver{
      &mock_handler};

  mojom::PermissionStatus status = mojom::PermissionStatus::kNotDetermined;
  base::RunLoop loop;
  auto service = std::make_unique<MacNotificationServiceUN>(
      handler_receiver.BindNewPipeAndPassRemote(),
      base::BindLambdaForTesting([&](mojom::PermissionStatus new_status) {
        status = new_status;
        loop.Quit();
      }),
      mock_notification_center);
  loop.Run();
  EXPECT_EQ(status, mojom::PermissionStatus::kDenied);

  base::test::TestFuture<mojom::RequestPermissionResult> permission_result;
  service->RequestPermission(permission_result.GetCallback());
  EXPECT_EQ(permission_result.Get(),
            mojom::RequestPermissionResult::kPermissionPreviouslyDenied);
  EXPECT_EQ(status, mojom::PermissionStatus::kDenied);
}

TEST_F(MacNotificationServiceUNTest,
       PermissionStateChangedCallback_Synchronization) {
  // Reset the service created by the test harness so it doesn't interfere with
  // the one created in this test.
  ResetService();

  UNAuthorizationStatus current_status = UNAuthorizationStatusAuthorized;
  auto& status_ref = current_status;
  id settings = OCMClassMock([UNNotificationSettings class]);
  OCMStub([settings authorizationStatus]).andDo(^(NSInvocation* invocation) {
    [invocation setReturnValue:(void*)(&status_ref)];
  });
  id mock_notification_center = CreateMockNotificationCenter(settings);

  MockNotificationActionHandler mock_handler;
  mojo::Receiver<mojom::MacNotificationActionHandler> handler_receiver{
      &mock_handler};

  base::test::TestFuture<mojom::PermissionStatus> status;
  auto service = std::make_unique<MacNotificationServiceUN>(
      handler_receiver.BindNewPipeAndPassRemote(),
      status.GetRepeatingCallback(), mock_notification_center);
  EXPECT_EQ(status.Take(), mojom::PermissionStatus::kGranted);

  current_status = UNAuthorizationStatusDenied;
  EXPECT_EQ(status.Take(), mojom::PermissionStatus::kDenied);

  current_status = UNAuthorizationStatusNotDetermined;
  EXPECT_EQ(status.Take(), mojom::PermissionStatus::kNotDetermined);

  current_status = UNAuthorizationStatusAuthorized;
  EXPECT_EQ(status.Take(), mojom::PermissionStatus::kGranted);
}

}  // namespace mac_notifications