chromium/chrome/services/mac_notifications/mac_notification_service_ns_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 <Foundation/NSUserNotification.h>

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

#include "base/apple/bundle_locations.h"
#include "base/barrier_closure.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_ns.h"
#import "chrome/services/mac_notifications/mac_notification_service_utils.h"
#include "chrome/services/mac_notifications/public/mojom/mac_notifications.mojom.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"
#include "ui/gfx/image/image_skia.h"
#include "url/gurl.h"

// This class implements the Chromium interface to a deprecated API. It is in
// the process of being replaced, and warnings about its deprecation are not
// helpful. https://crbug.com/1127306
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"

// Make dynamic properties accessible for OCMock.
@implementation NSUserNotificationCenter (Testing)
- (id<NSUserNotificationCenterDelegate>)delegate {
  return nil;
}
- (void)setDelegate:(id<NSUserNotificationCenterDelegate>)delegate {
}
- (NSArray*)deliveredNotifications {
  return nil;
}
@end

@implementation NSUserNotification (Testing)
- (NSDictionary*)userInfo {
  return nil;
}
- (NSUserNotificationActivationType)activationType {
  return NSUserNotificationActivationTypeNone;
}
- (NSArray*)_alternateActionButtonTitles {
  return nil;
}
- (NSNumber*)_alternateActionIndex {
  return nil;
}
@end

namespace mac_notifications {

namespace {

struct NotificationActionParams {
  NSUserNotificationActivationType activation_type;
  NSNumber* has_settings_button;
  NSArray* action_button_titles;
  NSNumber* alternate_action_index;
  NotificationOperation operation;
  int button_index;
};

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

}  // namespace

class MacNotificationServiceNSTest : public testing::Test {
 public:
  MacNotificationServiceNSTest() {
    mock_notification_center_ =
        [OCMockObject mockForClass:[NSUserNotificationCenter class]];
    // Expect the MacNotificationServiceNS ctor to register a delegate with the
    // NSUserNotificationCenter.
    ExpectAndUpdateNSUserNotificationCenterDelegate(/*expect_not_nil=*/true);
    service_ = std::make_unique<MacNotificationServiceNS>(
        service_remote_.BindNewPipeAndPassReceiver(),
        handler_receiver_.BindNewPipeAndPassRemote(),
        mock_notification_center_);
    [mock_notification_center_ verify];
  }

  ~MacNotificationServiceNSTest() override {
    // Expect the MacNotificationServiceUN dtor to clear the delegate from the
    // UNNotificationCenter.
    ExpectAndUpdateNSUserNotificationCenterDelegate(/*expect_not_nil=*/false);
    service_.reset();
    [mock_notification_center_ verify];
  }

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

  NSUserNotification* CreateNotification(const std::string& notification_id,
                                         const std::string& profile_id,
                                         bool incognito,
                                         const GURL& origin) {
    NSUserNotification* toast = [[NSUserNotification alloc] init];
    toast.userInfo = @{
      kNotificationId : base::SysUTF8ToNSString(notification_id),
      kNotificationProfileId : base::SysUTF8ToNSString(profile_id),
      kNotificationIncognito : [NSNumber numberWithBool:incognito],
      kNotificationOrigin : base::SysUTF8ToNSString(origin.spec()),
    };
    return toast;
  }

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

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

    [[[mock_notification_center_ expect] andReturn:notifications_ns]
        deliveredNotifications];

    return notifications;
  }

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

  mojom::NotificationPtr CreateMojoNotification() {
    auto profile_identifier =
        mojom::ProfileIdentifier::New("profileId", /*incognito=*/true);
    auto notification_identifier = mojom::NotificationIdentifier::New(
        "notificationId", std::move(profile_identifier));
    auto meta = mojom::NotificationMetadata::New(
        std::move(notification_identifier), /*type=*/0, /*origin_url=*/GURL(),
        /*user_data_dir=*/"");

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

  void DisplayNotificationSync() {
    base::RunLoop run_loop;
    base::RepeatingClosure quit_closure = run_loop.QuitClosure();
    [[[mock_notification_center_ expect] andDo:^(NSInvocation*) {
      quit_closure.Run();
    }] deliverNotification:[OCMArg any]];

    service_remote_->DisplayNotification(CreateMojoNotification());
    run_loop.Run();
    [mock_notification_center_ verify];
  }

  base::test::TaskEnvironment task_environment_;
  MockNotificationActionHandler mock_handler_;
  mojo::Receiver<mojom::MacNotificationActionHandler> handler_receiver_{
      &mock_handler_};
  mojo::Remote<mojom::MacNotificationService> service_remote_;
  id mock_notification_center_ = nil;
  id<NSUserNotificationCenterDelegate> notification_center_delegate_ = nullptr;
  std::unique_ptr<MacNotificationServiceNS> service_;
};

TEST_F(MacNotificationServiceNSTest, DisplayNotification) {
  base::RunLoop run_loop;
  base::RepeatingClosure quit_closure = run_loop.QuitClosure();

  // Verify notification content.
  [[mock_notification_center_ expect]
      deliverNotification:[OCMArg checkWithBlock:^BOOL(
                                      NSUserNotification* notification) {
        EXPECT_NSEQ(@"i|profileId|notificationId", [notification identifier]);
        NSDictionary* user_info = [notification 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", [notification title]);
        EXPECT_NSEQ(@"subtitle", [notification subtitle]);
        EXPECT_NSEQ(@"body", [notification informativeText]);
        quit_closure.Run();
        return YES;
      }]];

  // Create and display a new notification.
  service_remote_->DisplayNotification(CreateMojoNotification());

  run_loop.Run();
  [mock_notification_center_ verify];
}

TEST_F(MacNotificationServiceNSTest, 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(MacNotificationServiceNSTest,
       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(MacNotificationServiceNSTest, GetAllDisplayedNotifications) {
  auto notifications = SetupNotifications();
  auto displayed = GetDisplayedNotificationsSync(/*profile=*/nullptr);
  EXPECT_EQ(notifications.size(), displayed.size());
}

TEST_F(MacNotificationServiceNSTest, CloseNotification) {
  auto notifications = SetupNotifications();
  NSUserNotification* expected = notifications.back();

  // Expect to close the expected notification.
  base::RunLoop run_loop;
  base::RepeatingClosure quit_closure = run_loop.QuitClosure();
  [[[mock_notification_center_ expect] andDo:^(NSInvocation*) {
    quit_closure.Run();
  }] removeDeliveredNotification:expected];

  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();
  [mock_notification_center_ verify];
}

TEST_F(MacNotificationServiceNSTest, CloseProfileNotifications) {
  auto notifications = SetupNotifications();

  // Expect to close the expected notifications.
  base::RunLoop run_loop;
  base::RepeatingClosure barrier =
      base::BarrierClosure(/*num_closures=*/2, run_loop.QuitClosure());
  [[[mock_notification_center_ expect] andDo:^(NSInvocation*) {
    barrier.Run();
  }] removeDeliveredNotification:notifications[2]];
  [[[mock_notification_center_ expect] andDo:^(NSInvocation*) {
    barrier.Run();
  }] removeDeliveredNotification:notifications[3]];

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

  run_loop.Run();
  [mock_notification_center_ verify];
}

TEST_F(MacNotificationServiceNSTest, CloseAllNotifications) {
  base::RunLoop run_loop;
  base::RepeatingClosure quit_closure = run_loop.QuitClosure();
  [[[mock_notification_center_ expect] andDo:^(NSInvocation*) {
    quit_closure.Run();
  }] removeAllDeliveredNotifications];
  service_remote_->CloseAllNotifications();
  run_loop.Run();
  [mock_notification_center_ verify];
}

const NotificationActionParams kNotificationActionParams[] = {
    {NSUserNotificationActivationTypeNone,
     /*has_settings_button=*/@NO, @[ @"A", @"B" ],
     /*alternate_action_index=*/@0, NotificationOperation::kClose,
     kNotificationInvalidButtonIndex},
    {NSUserNotificationActivationTypeContentsClicked,
     /*has_settings_button=*/@NO, @[ @"A", @"B" ],
     /*alternate_action_index=*/@0, NotificationOperation::kClick,
     kNotificationInvalidButtonIndex},
    {NSUserNotificationActivationTypeActionButtonClicked,
     /*has_settings_button=*/@NO, @[ @"A", @"B" ],
     /*alternate_action_index=*/@0, NotificationOperation::kClick,
     /*button_index=*/0},
    {NSUserNotificationActivationTypeActionButtonClicked,
     /*has_settings_button=*/@YES, @[ @"A", @"B", @"Settings" ],
     /*alternate_action_index=*/@1, NotificationOperation::kClick,
     /*button_index=*/1},
    {NSUserNotificationActivationTypeActionButtonClicked,
     /*has_settings_button=*/@YES, @[ @"A", @"B", @"Settings" ],
     /*alternate_action_index=*/@2, NotificationOperation::kSettings,
     kNotificationInvalidButtonIndex},
};

class MacNotificationServiceNSTestNotificationAction
    : public MacNotificationServiceNSTest,
      public testing::WithParamInterface<NotificationActionParams> {
 public:
  MacNotificationServiceNSTestNotificationAction() = default;
  ~MacNotificationServiceNSTestNotificationAction() override = default;
};

TEST_P(MacNotificationServiceNSTestNotificationAction, OnNotificationAction) {
  const NotificationActionParams& params = GetParam();
  base::RunLoop run_loop;
  EXPECT_CALL(mock_handler_, OnNotificationAction)
      .WillOnce([&](mojom::NotificationActionInfoPtr action_info) {
        EXPECT_EQ(params.operation, action_info->operation);
        EXPECT_EQ(params.button_index, action_info->button_index);
        run_loop.Quit();
      });

  // Simulate a notification action and wait until we acknowledge it.
  id notification = [OCMockObject mockForClass:[NSUserNotification class]];
  [[[notification stub] andReturn:@{
    kNotificationHasSettingsButton : params.has_settings_button,
  }] userInfo];
  [[[notification stub] andReturnValue:OCMOCK_VALUE(params.activation_type)]
      activationType];
  [[[notification stub] andReturn:params.action_button_titles]
      valueForKey:@"_alternateActionButtonTitles"];
  [[[notification stub] andReturn:params.alternate_action_index]
      valueForKey:@"_alternateActionIndex"];

  [notification_center_delegate_
       userNotificationCenter:mock_notification_center_
      didActivateNotification:notification];
  run_loop.Run();
}

INSTANTIATE_TEST_SUITE_P(All,
                         MacNotificationServiceNSTestNotificationAction,
                         testing::ValuesIn(kNotificationActionParams));

}  // namespace mac_notifications

#pragma clang diagnostic pop