// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/safety_check_notifications/model/safety_check_notification_client.h"
#import <Foundation/Foundation.h>
#import <UserNotifications/UserNotifications.h>
#import <memory>
#import "base/functional/bind.h"
#import "base/memory/raw_ptr.h"
#import "base/run_loop.h"
#import "base/task/sequenced_task_runner.h"
#import "base/test/task_environment.h"
#import "components/password_manager/core/browser/password_manager_test_utils.h"
#import "components/password_manager/core/browser/password_store/test_password_store.h"
#import "components/prefs/pref_service.h"
#import "components/prefs/scoped_user_pref_update.h"
#import "components/safe_browsing/core/common/safe_browsing_prefs.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_profile_password_store_factory.h"
#import "ios/chrome/browser/passwords/model/password_checkup_utils.h"
#import "ios/chrome/browser/push_notification/model/constants.h"
#import "ios/chrome/browser/safety_check/model/ios_chrome_safety_check_manager.h"
#import "ios/chrome/browser/safety_check/model/ios_chrome_safety_check_manager_factory.h"
#import "ios/chrome/browser/safety_check_notifications/utils/constants.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_manager_ios.h"
#import "ios/chrome/browser/upgrade/model/upgrade_recommended_details.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/chrome/test/testing_application_context.h"
#import "ios/testing/scoped_block_swizzler.h"
#import "ios/web/public/browser_state.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
namespace {
// Returns app upgrade details for an outdated application.
UpgradeRecommendedDetails OutdatedAppDetails() {
UpgradeRecommendedDetails details;
details.is_up_to_date = false;
details.next_version = "9999.9999.9999.9999";
details.upgrade_url = GURL("http://orgForName.org");
return details;
}
} // namespace
class SafetyCheckNotificationClientTest : public PlatformTest {
public:
void SetUp() override {
SetupMockNotificationCenter();
ScopedDictPrefUpdate update(GetApplicationContext()->GetLocalState(),
prefs::kAppLevelPushNotificationPermissions);
update->Set(kSafetyCheckNotificationKey, true);
TestChromeBrowserState::Builder builder;
builder.AddTestingFactory(
IOSChromeProfilePasswordStoreFactory::GetInstance(),
base::BindRepeating(
&password_manager::BuildPasswordStore<
web::BrowserState, password_manager::TestPasswordStore>));
ChromeBrowserState* browser_state =
profile_manager_.AddProfileWithBuilder(std::move(builder));
BrowserList* list = BrowserListFactory::GetForBrowserState(browser_state);
mock_scene_state_ = OCMClassMock([SceneState class]);
OCMStub([mock_scene_state_ activationLevel])
.andReturn(SceneActivationLevelForegroundActive);
browser_ = std::make_unique<TestBrowser>(browser_state, mock_scene_state_);
list->AddBrowser(browser_.get());
pref_service_ = browser_state->GetPrefs();
local_pref_service_ =
TestingApplicationContext::GetGlobal()->GetLocalState();
safety_check_manager_ =
IOSChromeSafetyCheckManagerFactory::GetForBrowserState(browser_state);
notification_client_ = std::make_unique<SafetyCheckNotificationClient>(
base::SequencedTaskRunner::GetCurrentDefault());
}
void TearDown() override {
safety_check_manager_->StopSafetyCheck();
}
// Sets up a mock notification center, so notification requests can be
// tested.
void SetupMockNotificationCenter() {
mock_notification_center_ = OCMClassMock([UNUserNotificationCenter class]);
// Swizzle in the mock notification center.
UNUserNotificationCenter* (^swizzle_block)() =
^UNUserNotificationCenter*() {
return mock_notification_center_;
};
notification_center_swizzler_ = std::make_unique<ScopedBlockSwizzler>(
[UNUserNotificationCenter class], @selector(currentNotificationCenter),
swizzle_block);
}
// Stubs the notification center's completion callback for
// getPendingNotificationRequestsWithCompletionHandler.
void StubGetPendingRequests(NSArray<UNNotificationRequest*>* requests) {
auto completionCaller =
^BOOL(void (^completion)(NSArray<UNNotificationRequest*>* requests)) {
completion(requests);
return YES;
};
OCMStub([mock_notification_center_
getPendingNotificationRequestsWithCompletionHandler:
[OCMArg checkWithBlock:completionCaller]]);
}
// Returns an `OCMArg` that verifies a `UNNotificationRequest` was passed
// matching `notification_id`.
id NotificationRequestArg(NSString* notification_id) {
return [OCMArg checkWithBlock:^BOOL(UNNotificationRequest* request) {
EXPECT_TRUE([request.identifier isEqualToString:notification_id]);
return YES;
}];
}
// Sets up an OCMock expectation that a notification matching
// `notification_id` is requested.
void ExpectNotificationRequest(NSString* notification_id) {
ExpectNotificationRequest(NotificationRequestArg(notification_id));
}
void ExpectNotificationRequest(id request) {
auto completionCaller = ^BOOL(void (^completion)(NSError* error)) {
if (completion) {
completion(nil);
}
return YES;
};
OCMExpect([mock_notification_center_
addNotificationRequest:request
withCompletionHandler:[OCMArg checkWithBlock:completionCaller]]);
}
protected:
web::WebTaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
std::unique_ptr<SafetyCheckNotificationClient> notification_client_;
IOSChromeScopedTestingLocalState scoped_testing_local_state_;
TestProfileManagerIOS profile_manager_;
raw_ptr<IOSChromeSafetyCheckManager> safety_check_manager_;
id mock_notification_center_;
std::unique_ptr<ScopedBlockSwizzler> notification_center_swizzler_;
id mock_scene_state_;
std::unique_ptr<TestBrowser> browser_;
raw_ptr<PrefService> pref_service_;
raw_ptr<PrefService> local_pref_service_;
};
#pragma mark - Test cases
// Tests that HandleNotificationReception does nothing and returns "NoData".
TEST_F(SafetyCheckNotificationClientTest,
HandleNotificationReceptionReturnsNoData) {
EXPECT_EQ(notification_client_->HandleNotificationReception(nil),
UIBackgroundFetchResultNoData);
}
// Tests that RegisterActionalableNotifications returns an empty array.
TEST_F(SafetyCheckNotificationClientTest,
RegisterActionableNotificationsReturnsEmptyArray) {
EXPECT_EQ(notification_client_->RegisterActionableNotifications().count, 0u);
}
// Tests that a Safe Browsing notification is correctly scheduled when the user
// turns off Safe Browsing.
TEST_F(SafetyCheckNotificationClientTest, SchedulesSafeBrowsingNotification) {
pref_service_->SetBoolean(prefs::kSafeBrowsingEnhanced, false);
pref_service_->SetBoolean(prefs::kSafeBrowsingEnabled, false);
StubGetPendingRequests(nil);
ExpectNotificationRequest(kSafetyCheckSafeBrowsingNotificationID);
base::RunLoop run_loop;
notification_client_->OnSceneActiveForegroundBrowserReady(
run_loop.QuitClosure());
run_loop.Run();
EXPECT_OCMOCK_VERIFY(mock_notification_center_);
}
// Tests that a Update Chrome notification is correctly scheduled when the user
// has an available app update.
TEST_F(SafetyCheckNotificationClientTest, SchedulesUpdateChromeNotification) {
StubGetPendingRequests(nil);
ExpectNotificationRequest(kSafetyCheckUpdateChromeNotificationID);
// Simulate an available app update.
//
// Note: We need `task_environment_.RunUntilIdle()` because
// `IOSChromeSafetyCheckManager`'s internals aren't exposed, and we must
// ensure all pending tasks complete before proceeding.
safety_check_manager_->StartOmahaCheckForTesting();
task_environment_.FastForwardBy(kOmahaNetworkWaitTime / 2);
safety_check_manager_->HandleOmahaResponse(OutdatedAppDetails());
task_environment_.RunUntilIdle();
base::RunLoop run_loop;
notification_client_->OnSceneActiveForegroundBrowserReady(
run_loop.QuitClosure());
run_loop.Run();
EXPECT_OCMOCK_VERIFY(mock_notification_center_);
}
// Tests that a Password notification is correctly scheduled when the user
// has a compromised credential.
TEST_F(SafetyCheckNotificationClientTest, SchedulesPasswordNotification) {
StubGetPendingRequests(nil);
ExpectNotificationRequest(kSafetyCheckPasswordNotificationID);
password_manager::InsecurePasswordCounts counts = {
/* compromised */ 1, /* dismissed */ 0, /* reused */ 0,
/* weak */ 0};
safety_check_manager_->SetInsecurePasswordCountsForTesting(counts);
safety_check_manager_->SetPasswordCheckStateForTesting(
PasswordSafetyCheckState::kUnmutedCompromisedPasswords);
base::RunLoop run_loop;
notification_client_->OnSceneActiveForegroundBrowserReady(
run_loop.QuitClosure());
run_loop.Run();
EXPECT_OCMOCK_VERIFY(mock_notification_center_);
}