1// Copyright 2020 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#import <UserNotifications/UserNotifications.h>
6
7#include "base/bind.h"
8#include "base/mac/scoped_nsobject.h"
9#include "base/run_loop.h"
10#include "base/strings/utf_string_conversions.h"
11#include "base/test/bind.h"
12#include "chrome/browser/notifications/notification_platform_bridge_mac_unnotification.h"
13#include "chrome/browser/ui/cocoa/notifications/notification_constants_mac.h"
14#include "chrome/browser/ui/cocoa/notifications/unnotification_builder_mac.h"
15#include "chrome/browser/ui/cocoa/notifications/unnotification_response_builder_mac.h"
16#include "chrome/test/base/testing_browser_process.h"
17#include "chrome/test/base/testing_profile.h"
18#include "chrome/test/base/testing_profile_manager.h"
19#include "content/public/test/browser_task_environment.h"
20#include "testing/gtest/include/gtest/gtest.h"
21#include "testing/gtest_mac.h"
22#include "ui/message_center/public/cpp/notification.h"
23#include "url/gurl.h"
24
25// TODO(crbug/1146412): Move the mock classes to a separate file to avoid name
26// clashes.
27API_AVAILABLE(macosx(10.14))
28@interface FakeNotification : NSObject
29@property(nonatomic, retain) UNNotificationRequest* request;
30@end
31
32@implementation FakeNotification
33@synthesize request;
34@end
35
36API_AVAILABLE(macosx(10.14))
37@interface FakeUNUserNotificationCenter : NSObject
38- (instancetype)init;
39// Need to provide a nop implementation of setDelegate as it is
40// used during the setup of the bridge.
41- (void)setDelegate:(id<UNUserNotificationCenterDelegate>)delegate;
42- (void)removeAllDeliveredNotifications;
43- (void)setNotificationCategories:(NSSet<UNNotificationCategory*>*)categories;
44- (void)replaceContentForRequestWithIdentifier:(NSString*)requestIdentifier
45                            replacementContent:
46                                (UNMutableNotificationContent*)content
47                             completionHandler:
48                                 (void (^)(NSError* _Nullable error))
49                                     notificationDelivered;
50- (void)addNotificationRequest:(UNNotificationRequest*)request
51         withCompletionHandler:(void (^)(NSError* error))completionHandler;
52- (void)getDeliveredNotificationsWithCompletionHandler:
53    (void (^)(NSArray<UNNotification*>* notifications))completionHandler;
54- (void)getNotificationCategoriesWithCompletionHandler:
55    (void (^)(NSSet<UNNotificationCategory*>* categories))completionHandler;
56- (void)requestAuthorizationWithOptions:(UNAuthorizationOptions)options
57                      completionHandler:(void (^)(BOOL granted, NSError* error))
58                                            completionHandler;
59- (void)removeDeliveredNotificationsWithIdentifiers:
60    (NSArray<NSString*>*)identifiers;
61@end
62
63@implementation FakeUNUserNotificationCenter {
64  base::scoped_nsobject<NSMutableArray> _banners;
65  base::scoped_nsobject<NSSet> _categories;
66}
67
68- (instancetype)init {
69  if ((self = [super init])) {
70    _banners.reset([[NSMutableArray alloc] init]);
71    _categories.reset([[NSSet alloc] init]);
72  }
73  return self;
74}
75
76- (void)setDelegate:(id<UNUserNotificationCenterDelegate>)delegate {
77}
78
79- (void)removeAllDeliveredNotifications {
80  [_banners removeAllObjects];
81}
82
83- (void)setNotificationCategories:(NSSet<UNNotificationCategory*>*)categories {
84  _categories.reset([categories copy]);
85}
86
87- (void)replaceContentForRequestWithIdentifier:(NSString*)requestIdentifier
88                            replacementContent:
89                                (UNMutableNotificationContent*)content
90                             completionHandler:
91                                 (void (^)(NSError* _Nullable error))
92                                     notificationDelivered {
93  UNNotificationRequest* request =
94      [UNNotificationRequest requestWithIdentifier:requestIdentifier
95                                           content:content
96                                           trigger:nil];
97  base::scoped_nsobject<FakeNotification> notification(
98      [[FakeNotification alloc] init]);
99  [notification setRequest:request];
100  [_banners addObject:notification];
101  notificationDelivered(/*error=*/nil);
102}
103
104- (void)addNotificationRequest:(UNNotificationRequest*)request
105         withCompletionHandler:(void (^)(NSError* error))completionHandler {
106  base::scoped_nsobject<FakeNotification> notification(
107      [[FakeNotification alloc] init]);
108  [notification setRequest:request];
109  [_banners addObject:notification];
110  completionHandler(/*error=*/nil);
111}
112
113- (void)getDeliveredNotificationsWithCompletionHandler:
114    (void (^)(NSArray<UNNotification*>* notifications))completionHandler {
115  completionHandler([[_banners copy] autorelease]);
116}
117
118- (void)getNotificationCategoriesWithCompletionHandler:
119    (void (^)(NSSet<UNNotificationCategory*>* categories))completionHandler {
120  completionHandler([[_categories copy] autorelease]);
121}
122
123- (void)requestAuthorizationWithOptions:(UNAuthorizationOptions)options
124                      completionHandler:(void (^)(BOOL granted, NSError* error))
125                                            completionHandler {
126  completionHandler(/*granted=*/YES, /*error=*/nil);
127}
128
129- (void)removeDeliveredNotificationsWithIdentifiers:
130    (NSArray<NSString*>*)identifiers {
131  [_banners filterUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
132                                                  UNNotification* notification,
133                                                  NSDictionary* bindings) {
134              NSString* toastId = [[[[notification request] content] userInfo]
135                  objectForKey:notification_constants::kNotificationId];
136              return ![identifiers containsObject:toastId];
137            }]];
138}
139@end
140
141using message_center::Notification;
142
143class UNNotificationPlatformBridgeMacTest : public testing::Test {
144 public:
145  UNNotificationPlatformBridgeMacTest()
146      : manager_(TestingBrowserProcess::GetGlobal()) {}
147
148  void SetUp() override {
149    ASSERT_TRUE(manager_.SetUp());
150    profile_ = manager_.CreateTestingProfile("Moe");
151    if (@available(macOS 10.14, *)) {
152      center_.reset([[FakeUNUserNotificationCenter alloc] init]);
153      bridge_ = std::make_unique<NotificationPlatformBridgeMacUNNotification>(
154          static_cast<UNUserNotificationCenter*>(center_.get()));
155    }
156  }
157
158 protected:
159  Notification CreateNotification(const std::string& notificationId = "id1") {
160    GURL url("https://gmail.com");
161
162    Notification notification(
163        message_center::NOTIFICATION_TYPE_SIMPLE, notificationId,
164        base::UTF8ToUTF16("Title"), base::UTF8ToUTF16("Context"), gfx::Image(),
165        base::UTF8ToUTF16("Notifier's Name"), url,
166        message_center::NotifierId(url), message_center::RichNotificationData(),
167        base::MakeRefCounted<message_center::NotificationDelegate>());
168
169    return notification;
170  }
171
172  API_AVAILABLE(macosx(10.14))
173  base::scoped_nsobject<FakeUNUserNotificationCenter> center_;
174  API_AVAILABLE(macosx(10.14))
175  std::unique_ptr<NotificationPlatformBridgeMacUNNotification> bridge_;
176  TestingProfile* profile_ = nullptr;
177
178 private:
179  content::BrowserTaskEnvironment task_environment_;
180  TestingProfileManager manager_;
181};
182
183TEST_F(UNNotificationPlatformBridgeMacTest, TestDisplay) {
184  if (@available(macOS 10.14, *)) {
185    Notification notification = CreateNotification();
186
187    bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_,
188                     notification, nullptr);
189
190    [center_ getDeliveredNotificationsWithCompletionHandler:^(
191                 NSArray<UNNotification*>* _Nonnull notifications) {
192      ASSERT_EQ(1u, [notifications count]);
193      UNNotification* delivered_notification = [notifications objectAtIndex:0];
194      UNNotificationContent* delivered_content =
195          [[delivered_notification request] content];
196      EXPECT_NSEQ(@"Title", [delivered_content title]);
197      EXPECT_NSEQ(@"Context", [delivered_content body]);
198      EXPECT_NSEQ(@"gmail.com", [delivered_content subtitle]);
199    }];
200
201    [center_ getNotificationCategoriesWithCompletionHandler:^(
202                 NSSet<UNNotificationCategory*>* categories) {
203      EXPECT_EQ(1u, [categories count]);
204    }];
205  }
206}
207
208TEST_F(UNNotificationPlatformBridgeMacTest, TestNotificationHasIcon) {
209  if (@available(macOS 10.14, *)) {
210    Notification notification = CreateNotification();
211
212    SkBitmap icon;
213    icon.allocN32Pixels(64, 64);
214    icon.eraseARGB(255, 100, 150, 200);
215    notification.set_icon(gfx::Image::CreateFrom1xBitmap(icon));
216
217    bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_,
218                     notification, nullptr);
219
220    [center_ getDeliveredNotificationsWithCompletionHandler:^(
221                 NSArray<UNNotification*>* _Nonnull notifications) {
222      ASSERT_EQ(1u, [notifications count]);
223      UNNotification* delivered_notification = [notifications objectAtIndex:0];
224      UNNotificationContent* delivered_content =
225          [[delivered_notification request] content];
226      ASSERT_EQ(1u, [[delivered_content attachments] count]);
227      EXPECT_NSEQ(@"id1", [[[delivered_content attachments] objectAtIndex:0]
228                              identifier]);
229    }];
230  }
231}
232
233TEST_F(UNNotificationPlatformBridgeMacTest, TestNotificationNoIcon) {
234  if (@available(macOS 10.14, *)) {
235    Notification notification = CreateNotification();
236
237    bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_,
238                     notification, nullptr);
239
240    [center_ getDeliveredNotificationsWithCompletionHandler:^(
241                 NSArray<UNNotification*>* _Nonnull notifications) {
242      ASSERT_EQ(1u, [notifications count]);
243      UNNotification* delivered_notification = [notifications objectAtIndex:0];
244      UNNotificationContent* delivered_content =
245          [[delivered_notification request] content];
246      EXPECT_EQ(0u, [[delivered_content attachments] count]);
247    }];
248  }
249}
250
251TEST_F(UNNotificationPlatformBridgeMacTest, TestCloseNotification) {
252  if (@available(macOS 10.14, *)) {
253    Notification notification = CreateNotification();
254
255    [center_ getDeliveredNotificationsWithCompletionHandler:^(
256                 NSArray<UNNotification*>* _Nonnull notifications) {
257      EXPECT_EQ(0u, [notifications count]);
258    }];
259
260    bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_,
261                     notification, nullptr);
262
263    [center_ getDeliveredNotificationsWithCompletionHandler:^(
264                 NSArray<UNNotification*>* _Nonnull notifications) {
265      EXPECT_EQ(1u, [notifications count]);
266    }];
267
268    bridge_->Close(profile_, "id1");
269    // RunLoop is used here to ensure that Close has finished executing before
270    // the notification count is checked below. Since Close executes
271    // asynchronous calls the order is not guaranteed by nature.
272    base::RunLoop().RunUntilIdle();
273    [center_ getDeliveredNotificationsWithCompletionHandler:^(
274                 NSArray<UNNotification*>* _Nonnull notifications) {
275      EXPECT_EQ(0u, [notifications count]);
276    }];
277  }
278}
279
280TEST_F(UNNotificationPlatformBridgeMacTest, TestGetDisplayed) {
281  if (@available(macOS 10.14, *)) {
282    Notification notification = CreateNotification();
283
284    [center_ getDeliveredNotificationsWithCompletionHandler:^(
285                 NSArray<UNNotification*>* _Nonnull notifications) {
286      EXPECT_EQ(0u, [notifications count]);
287    }];
288
289    bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_,
290                     notification, nullptr);
291
292    [center_ getDeliveredNotificationsWithCompletionHandler:^(
293                 NSArray<UNNotification*>* _Nonnull notifications) {
294      EXPECT_EQ(1u, [notifications count]);
295    }];
296
297    base::RunLoop run_loop;
298    int notification_count = -1;
299    bridge_->GetDisplayed(
300        profile_,
301        base::BindLambdaForTesting([&](std::set<std::string> notifications,
302                                       bool supports_synchronization) {
303          notification_count = notifications.size();
304          run_loop.Quit();
305        }));
306    run_loop.Run();
307    EXPECT_EQ(1, notification_count);
308  }
309}
310
311TEST_F(UNNotificationPlatformBridgeMacTest,
312       TestGetDisplayedMultipleNotifications) {
313  if (@available(macOS 10.14, *)) {
314    Notification first_notification = CreateNotification("id1");
315
316    bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_,
317                     first_notification, nullptr);
318
319    Notification second_notification = CreateNotification("id2");
320    bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_,
321                     second_notification, nullptr);
322
323    base::RunLoop run_loop;
324    int notification_count = -1;
325    bridge_->GetDisplayed(
326        profile_,
327        base::BindLambdaForTesting([&](std::set<std::string> notifications,
328                                       bool supports_synchronization) {
329          notification_count = notifications.size();
330          run_loop.Quit();
331        }));
332    run_loop.Run();
333    EXPECT_EQ(2, notification_count);
334
335    [center_ getNotificationCategoriesWithCompletionHandler:^(
336                 NSSet<UNNotificationCategory*>* categories) {
337      EXPECT_EQ(2u, [categories count]);
338    }];
339  }
340}
341
342TEST_F(UNNotificationPlatformBridgeMacTest, TestQuitRemovesNotifications) {
343  if (@available(macOS 10.14, *)) {
344    Notification notification = CreateNotification();
345
346    bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_,
347                     notification, nullptr);
348
349    [center_ getDeliveredNotificationsWithCompletionHandler:^(
350                 NSArray<UNNotification*>* _Nonnull notifications) {
351      EXPECT_EQ(1u, [notifications count]);
352    }];
353
354    bridge_.reset();
355
356    // The destructor of the bridge_ will call removeAllDeliveredNotifications.
357    [center_ getDeliveredNotificationsWithCompletionHandler:^(
358                 NSArray<UNNotification*>* _Nonnull notifications) {
359      EXPECT_EQ(0u, [notifications count]);
360    }];
361  }
362}
363
364TEST_F(UNNotificationPlatformBridgeMacTest, TestNotificationNoButtons) {
365  if (@available(macOS 10.14, *)) {
366    Notification notification = CreateNotification();
367
368    notification.set_settings_button_handler(
369        message_center::SettingsButtonHandler::DELEGATE);
370
371    bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_,
372                     notification, nullptr);
373
374    [center_ getNotificationCategoriesWithCompletionHandler:^(
375                 NSSet<UNNotificationCategory*>* categories) {
376      ASSERT_EQ(1u, [categories count]);
377      UNNotificationCategory* category = [categories anyObject];
378      // If this selector from the private API is available the close button
379      // will be set in alernateAction, and the other buttons will be set in
380      // actions. Otherwise, all buttons will be setting in actions which causes
381      // the total count to differ by one.
382      if ([category respondsToSelector:@selector(alternateAction)]) {
383        ASSERT_EQ(1ul, [[category actions] count]);
384        EXPECT_NSEQ(@"Close",
385                    [[category valueForKey:@"_alternateAction"] title]);
386        EXPECT_NSEQ(@"Settings", [[[category actions] lastObject] title]);
387      } else {
388        ASSERT_EQ(2ul, [[category actions] count]);
389        EXPECT_NSEQ(@"Close", [[[category actions] lastObject] title]);
390        EXPECT_NSEQ(@"Settings", [[[category actions] firstObject] title]);
391      }
392    }];
393  }
394}
395
396TEST_F(UNNotificationPlatformBridgeMacTest, TestNotificationNoSettingsButton) {
397  if (@available(macOS 10.14, *)) {
398    Notification notification = CreateNotification();
399
400    bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_,
401                     notification, nullptr);
402
403    [center_ getNotificationCategoriesWithCompletionHandler:^(
404                 NSSet<UNNotificationCategory*>* categories) {
405      ASSERT_EQ(1u, [categories count]);
406      UNNotificationCategory* category = [categories anyObject];
407
408      if ([category respondsToSelector:@selector(alternateAction)])
409        EXPECT_EQ(0ul, [[category actions] count]);
410      else
411        EXPECT_EQ(1ul, [[category actions] count]);
412    }];
413  }
414}
415
416TEST_F(UNNotificationPlatformBridgeMacTest, TestNotificationWithButtons) {
417  if (@available(macOS 10.14, *)) {
418    Notification notification = CreateNotification();
419
420    notification.set_settings_button_handler(
421        message_center::SettingsButtonHandler::DELEGATE);
422    std::vector<message_center::ButtonInfo> buttons = {
423        message_center::ButtonInfo(base::UTF8ToUTF16("Button 1")),
424        message_center::ButtonInfo(base::UTF8ToUTF16("Button 2"))};
425    notification.set_buttons(buttons);
426
427    bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_,
428                     notification, nullptr);
429
430    [center_ getNotificationCategoriesWithCompletionHandler:^(
431                 NSSet<UNNotificationCategory*>* categories) {
432      ASSERT_EQ(1u, [categories count]);
433      UNNotificationCategory* category = [categories anyObject];
434
435      if ([category respondsToSelector:@selector(alternateAction)]) {
436        EXPECT_NSEQ(@"Button 1", [[category actions][0] title]);
437        EXPECT_NSEQ(@"Button 2", [[category actions][1] title]);
438        EXPECT_EQ(3ul, [[category actions] count]);
439      } else {
440        EXPECT_NSEQ(@"Button 1", [[category actions][1] title]);
441        EXPECT_NSEQ(@"Button 2", [[category actions][2] title]);
442        EXPECT_EQ(4ul, [[category actions] count]);
443      }
444    }];
445  }
446}
447
448TEST_F(UNNotificationPlatformBridgeMacTest,
449       TestNotificationCategoryIdentifier) {
450  if (@available(macOS 10.14, *)) {
451    Notification notification = CreateNotification();
452
453    bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_,
454                     notification, nullptr);
455
456    [center_ getNotificationCategoriesWithCompletionHandler:^(
457                 NSSet<UNNotificationCategory*>* categories) {
458      ASSERT_EQ(1u, [categories count]);
459      EXPECT_NSEQ(@"id1", [[categories anyObject] identifier]);
460    }];
461  }
462}
463
464TEST_F(UNNotificationPlatformBridgeMacTest, TestCloseRemovesCategory) {
465  if (@available(macOS 10.14, *)) {
466    Notification first_notification = CreateNotification();
467
468    bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_,
469                     first_notification, nullptr);
470
471    [center_ getNotificationCategoriesWithCompletionHandler:^(
472                 NSSet<UNNotificationCategory*>* categories) {
473      EXPECT_EQ(1u, [categories count]);
474    }];
475
476    bridge_->Close(profile_, "id1");
477    // RunLoop is used here to ensure that Close has finished executing before
478    // the notification count is checked below. Since Close executes
479    // asynchronous calls the order is not guaranteed by nature.
480    base::RunLoop().RunUntilIdle();
481
482    // Categories get updated during the next display call, so we call display
483    // to make sure that the category has been removed.
484    Notification second_notification = CreateNotification("id2");
485    bridge_->Display(NotificationHandler::Type::WEB_PERSISTENT, profile_,
486                     second_notification, nullptr);
487
488    [center_ getNotificationCategoriesWithCompletionHandler:^(
489                 NSSet<UNNotificationCategory*>* categories) {
490      ASSERT_EQ(1u, [categories count]);
491      EXPECT_NSEQ(@"id2", [[categories anyObject] identifier]);
492    }];
493  }
494}
495