blob: 0ea19162feeccb1ee554f7c0151cd9aa83c7ebdd [file] [log] [blame]
// Copyright 2022 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/ui/safety_hub/revoked_permissions_service.h"
#include "base/json/values_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/time/default_clock.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/engagement/site_engagement_service_factory.h"
#include "chrome/browser/notifications/notification_display_service_tester.h"
#include "chrome/browser/notifications/platform_notification_service_factory.h"
#include "chrome/browser/notifications/platform_notification_service_impl.h"
#include "chrome/browser/permissions/notifications_engagement_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/safe_browsing/test_safe_browsing_service.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/safety_hub/disruptive_notification_permissions_manager.h"
#include "chrome/browser/ui/safety_hub/mock_safe_browsing_database_manager.h"
#include "chrome/browser/ui/safety_hub/revoked_permissions_service_factory.h"
#include "chrome/browser/ui/safety_hub/safety_hub_result.h"
#include "chrome/browser/ui/safety_hub/safety_hub_test_util.h"
#include "chrome/browser/ui/safety_hub/safety_hub_util.h"
#include "chrome/browser/ui/safety_hub/unused_site_permissions_manager.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/content_settings/core/browser/content_settings_registry.h"
#include "components/content_settings/core/browser/content_settings_uma_util.h"
#include "components/content_settings/core/browser/content_settings_utils.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings.h"
#include "components/content_settings/core/common/content_settings_constraints.h"
#include "components/content_settings/core/common/features.h"
#include "components/permissions/constants.h"
#include "components/permissions/features.h"
#include "components/permissions/notifications_engagement_service.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/notifications/platform_notification_data.h"
namespace {
using testing::Eq;
using testing::Field;
using testing::Ge;
using testing::Not;
using testing::Optional;
using testing::Pointee;
const char histogram_name[] =
"Settings.SafetyHub.UnusedSitePermissionsModule.AutoRevoked2";
} // namespace
class RevokedPermissionsServiceBrowserTest : public InProcessBrowserTest {
public:
using DisruptiveNotificationRevocationEntry =
DisruptiveNotificationPermissionsManager::RevocationEntry;
using DisruptiveNotificationContentSettingHelper =
DisruptiveNotificationPermissionsManager::ContentSettingHelper;
using DisruptiveNotificationRevocationState =
DisruptiveNotificationPermissionsManager::RevocationState;
RevokedPermissionsServiceBrowserTest() {
feature_list.InitWithFeatures(
/*enabled_features=*/
{content_settings::features::kSafetyCheckUnusedSitePermissions},
/*disabled_features=*/{});
}
void SetUpOnMainThread() override {
InProcessBrowserTest::SetUpOnMainThread();
ASSERT_TRUE(embedded_test_server()->Start());
}
ContentSettingsForOneType GetRevokedUnusedPermissions(
HostContentSettingsMap* hcsm) {
return hcsm->GetSettingsForOneType(
ContentSettingsType::REVOKED_UNUSED_SITE_PERMISSIONS);
}
private:
base::test::ScopedFeatureList feature_list;
};
IN_PROC_BROWSER_TEST_F(RevokedPermissionsServiceBrowserTest,
TestNavigationUpdatesLastUsedDate) {
auto* map =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
auto* service =
RevokedPermissionsServiceFactory::GetForProfile(browser()->profile());
GURL url = embedded_test_server()->GetURL("/title1.html");
// Create content setting 20 days in the past.
// TODO(crbug.com/40250875): Move code below to a helper method.
base::Time now(base::Time::Now());
base::Time past(now - base::Days(20));
base::SimpleTestClock clock;
clock.SetNow(past);
map->SetClockForTesting(&clock);
service->SetClockForTesting(&clock);
content_settings::ContentSettingConstraints constraints;
constraints.set_track_last_visit_for_autoexpiration(true);
map->SetContentSettingDefaultScope(url, url, ContentSettingsType::GEOLOCATION,
CONTENT_SETTING_ALLOW, constraints);
clock.SetNow(now);
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
ASSERT_EQ(service->GetTrackedUnusedPermissionsForTesting().size(), 1u);
ASSERT_EQ(GetRevokedUnusedPermissions(map).size(), 0u);
// Check that the timestamp is initially in the past.
content_settings::SettingInfo info;
map->GetWebsiteSetting(url, url, ContentSettingsType::GEOLOCATION, &info);
ASSERT_FALSE(info.metadata.last_visited().is_null());
EXPECT_GE(info.metadata.last_visited(),
past - content_settings::GetCoarseVisitedTimePrecision());
EXPECT_LE(info.metadata.last_visited(), past);
// Navigate to |url|.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
// Check that the timestamp is updated after a navigation.
map->GetWebsiteSetting(url, url, ContentSettingsType::GEOLOCATION, &info);
EXPECT_GE(info.metadata.last_visited(),
now - content_settings::GetCoarseVisitedTimePrecision());
EXPECT_LE(info.metadata.last_visited(), now);
map->SetClockForTesting(base::DefaultClock::GetInstance());
}
// Test that navigations work fine in incognito mode.
IN_PROC_BROWSER_TEST_F(RevokedPermissionsServiceBrowserTest,
TestIncognitoProfile) {
GURL url = embedded_test_server()->GetURL("/title1.html");
auto* otr_browser = OpenURLOffTheRecord(browser()->profile(), url);
ASSERT_FALSE(
RevokedPermissionsServiceFactory::GetForProfile(otr_browser->profile()));
}
// Test that revocation is happen correctly when auto-revoke is on.
IN_PROC_BROWSER_TEST_F(RevokedPermissionsServiceBrowserTest,
TestRevokeUnusedPermissions) {
auto* map =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
auto* service =
RevokedPermissionsServiceFactory::GetForProfile(browser()->profile());
GURL url = embedded_test_server()->GetURL("/title1.html");
// Create content setting 20 days in the past.
base::Time now(base::Time::Now());
base::Time past(now - base::Days(20));
base::SimpleTestClock clock;
clock.SetNow(past);
map->SetClockForTesting(&clock);
service->SetClockForTesting(&clock);
content_settings::ContentSettingConstraints constraints;
constraints.set_track_last_visit_for_autoexpiration(true);
map->SetContentSettingDefaultScope(url, url, ContentSettingsType::GEOLOCATION,
CONTENT_SETTING_ALLOW, constraints);
clock.SetNow(now);
// Check if the content setting is still ALLOW, before auto-revocation.
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
ASSERT_EQ(service->GetTrackedUnusedPermissionsForTesting().size(), 1u);
ASSERT_EQ(GetRevokedUnusedPermissions(map).size(), 0u);
EXPECT_EQ(CONTENT_SETTING_ALLOW,
map->GetContentSetting(url, url, ContentSettingsType::GEOLOCATION));
// Travel through time for 40 days to make permissions be revoked.
clock.Advance(base::Days(40));
// Check if the content setting turn to ASK, when auto-revocation happens.
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
ASSERT_EQ(service->GetTrackedUnusedPermissionsForTesting().size(), 0u);
ASSERT_EQ(GetRevokedUnusedPermissions(map).size(), 1u);
EXPECT_EQ(CONTENT_SETTING_ASK,
map->GetContentSetting(url, url, ContentSettingsType::GEOLOCATION));
}
// Test that revocation happens correctly for all content setting types.
IN_PROC_BROWSER_TEST_F(RevokedPermissionsServiceBrowserTest,
RevokeAllContentSettingTypes) {
auto* map =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
auto* service =
RevokedPermissionsServiceFactory::GetForProfile(browser()->profile());
base::Time time;
ASSERT_TRUE(base::Time::FromString("2022-09-07 13:00", &time));
base::SimpleTestClock clock;
clock.SetNow(time);
map->SetClockForTesting(&clock);
service->SetClockForTesting(&clock);
GURL url = embedded_test_server()->GetURL("/title1.html");
base::HistogramTester histogram_tester;
// TODO(b/338365161): Remove the skip list, once the bug is fixed. Currently,
// when CAMERA_PAN_TILT_ZOOM is allowed, MEDIASTREAM_CAMERA is also allowed
// under the hood without passing predefined constraints. Since the
// auto-revokation relies on the constraint, the MEDIASTREAM_CAMERA ends up
// not being revoked - although it is should be.
const std::vector<ContentSettingsType> skip_list = {
ContentSettingsType::CAMERA_PAN_TILT_ZOOM};
// Allow all content settings in the content setting registry.
std::vector<ContentSettingsType> allowed_permission_types;
auto* content_settings_registry =
content_settings::ContentSettingsRegistry::GetInstance();
for (const content_settings::ContentSettingsInfo* info :
*content_settings_registry) {
ContentSettingsType type = info->website_settings_info()->type();
// Skip if the setting's last visit can not be tracked.
content_settings::ContentSettingConstraints constraint;
if (!content_settings::CanTrackLastVisit(type)) {
continue;
}
// Add last visited timestamp if the setting can be tracked.
constraint.set_track_last_visit_for_autoexpiration(true);
// Skip if the setting can not be set to ALLOW.
if (!content_settings_registry->Get(type)->IsSettingValid(
ContentSetting::CONTENT_SETTING_ALLOW)) {
continue;
}
// Skip if the setting produces patterns that do not get revoked.
content_settings::PatternPair patterns =
map->GetPatternsForContentSettingsType(url, url, type);
if (!patterns.first.MatchesSingleOrigin() ||
patterns.second != ContentSettingsPattern::Wildcard()) {
continue;
}
// Skip if the setting in the skip list.
if (base::Contains(skip_list, type)) {
continue;
}
map->SetContentSettingDefaultScope(GURL(url), GURL(url), type,
ContentSetting::CONTENT_SETTING_ALLOW,
constraint);
allowed_permission_types.push_back(type);
}
// Travel through time for 70 days to make permissions be revoked.
clock.Advance(base::Days(70));
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
// Assert there are revoked permission only for one origin.
ContentSettingsForOneType revoked_permissions =
GetRevokedUnusedPermissions(map);
EXPECT_EQ(revoked_permissions.size(), 1u);
// Get the revoked permission types for the origin.
const base::Value::Dict& permission_types_by_values =
revoked_permissions[0].setting_value.GetDict();
const base::Value::List revoked_permission_types =
permission_types_by_values.FindList(permissions::kRevokedKey)->Clone();
// Assert all the allowed permissions are revoked.
EXPECT_EQ(allowed_permission_types.size(), revoked_permission_types.size());
for (int i = 0; i < (int)revoked_permission_types.size(); i++) {
ContentSettingsType revoked_permission_type =
UnusedSitePermissionsManager::ConvertKeyToContentSettingsType(
revoked_permission_types[i].GetString());
EXPECT_EQ(allowed_permission_types[i], revoked_permission_type);
}
// Assert all auto-revocations are recorded in UMA metrics.
EXPECT_EQ(allowed_permission_types.size(),
histogram_tester.GetAllSamples(histogram_name).size());
for (const ContentSettingsType type : allowed_permission_types) {
histogram_tester.ExpectBucketCount(
histogram_name,
content_settings_uma_util::ContentSettingTypeToHistogramValue(type), 1);
}
// Navigate to content settings page.
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), GURL(chrome::kChromeUIContentSettingsURL)));
}
class AbusiveNotificationPermissionsRevocationBrowserTest
: public RevokedPermissionsServiceBrowserTest {
public:
AbusiveNotificationPermissionsRevocationBrowserTest() {
safe_browsing_factory_ =
std::make_unique<safe_browsing::TestSafeBrowsingServiceFactory>();
feature_list_.InitWithFeatures(
/*enabled_features=*/
{content_settings::features::kSafetyCheckUnusedSitePermissions},
/*disabled_features=*/{});
}
void CreatedBrowserMainParts(
content::BrowserMainParts* browser_main_parts) override {
fake_database_manager_ =
base::MakeRefCounted<MockSafeBrowsingDatabaseManager>();
safe_browsing_factory_->SetTestDatabaseManager(
fake_database_manager_.get());
safe_browsing::SafeBrowsingServiceInterface::RegisterFactory(
safe_browsing_factory_.get());
}
void AddDangerousUrl(const GURL& url) {
mock_database_manager()->SetThreatTypeForUrl(
url, safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_PHISHING);
}
MockSafeBrowsingDatabaseManager* mock_database_manager() {
return fake_database_manager_.get();
}
private:
scoped_refptr<MockSafeBrowsingDatabaseManager> fake_database_manager_;
std::unique_ptr<safe_browsing::TestSafeBrowsingServiceFactory>
safe_browsing_factory_;
base::test::ScopedFeatureList feature_list_;
};
// TODO(crbug.com/400648091): Re-enable the test when it's fixed.
// Test that revocation is happen correctly when auto-revoke is on.
IN_PROC_BROWSER_TEST_F(AbusiveNotificationPermissionsRevocationBrowserTest,
DISABLED_TestRevokeAbusiveNotificationPermissions) {
auto* map =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
auto* service =
RevokedPermissionsServiceFactory::GetForProfile(browser()->profile());
const GURL url("https://example1.com");
AddDangerousUrl(url);
base::HistogramTester histogram_tester;
// Create granted abusive notification permission.
map->SetContentSettingDefaultScope(
url, url, ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW);
ASSERT_EQ(
safety_hub_util::GetRevokedAbusiveNotificationPermissions(map).size(),
0u);
// Check if the content setting turn to ASK, when auto-revocation happens.
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
ASSERT_EQ(
safety_hub_util::GetRevokedAbusiveNotificationPermissions(map).size(),
1u);
EXPECT_EQ(
CONTENT_SETTING_ASK,
map->GetContentSetting(url, url, ContentSettingsType::NOTIFICATIONS));
// Assert notification auto-revocation is recorded in UMA metrics.
EXPECT_EQ(1u, histogram_tester.GetAllSamples(histogram_name).size());
histogram_tester.ExpectBucketCount(
histogram_name,
content_settings_uma_util::ContentSettingTypeToHistogramValue(
ContentSettingsType::NOTIFICATIONS),
1);
}
// TODO(crbug.com/400645286): Re-enable the test when it's fixed.
// Test that revocation is happen correctly when auto-revoke is on for a site
// that is unused then abusive.
IN_PROC_BROWSER_TEST_F(AbusiveNotificationPermissionsRevocationBrowserTest,
DISABLED_TestSiteWithFirstUnusedThenAbusivePermissions) {
auto* map =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
auto* service =
RevokedPermissionsServiceFactory::GetForProfile(browser()->profile());
// Test cases where there is a single URL which is both abusive and unused,
// then when there are separate abusive and unused URLs.
for (const auto& [abusive_url, unused_url] :
std::vector<std::pair<GURL, GURL>>{
{GURL("https://example1.com"), GURL("https://example1.com")},
{GURL("https://example2.com"), GURL("https://example3.com")}}) {
AddDangerousUrl(abusive_url);
// Create content setting 20 days in the past.
base::Time now(base::Time::Now());
base::Time past(now - base::Days(20));
base::SimpleTestClock clock;
clock.SetNow(past);
map->SetClockForTesting(&clock);
service->SetClockForTesting(&clock);
content_settings::ContentSettingConstraints constraints;
constraints.set_track_last_visit_for_autoexpiration(true);
map->SetContentSettingDefaultScope(unused_url, unused_url,
ContentSettingsType::GEOLOCATION,
CONTENT_SETTING_ALLOW, constraints);
clock.SetNow(now);
// Check if the content setting is still ALLOW, before auto-revocation.
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
ASSERT_EQ(GetRevokedUnusedPermissions(map).size(), 0u);
EXPECT_EQ(CONTENT_SETTING_ALLOW,
map->GetContentSetting(unused_url, unused_url,
ContentSettingsType::GEOLOCATION));
// Travel through time for 40 days to make permissions be revoked.
clock.Advance(base::Days(40));
// Check if the content setting turn to ASK, when auto-revocation happens.
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
ASSERT_EQ(GetRevokedUnusedPermissions(map).size(), 1u);
EXPECT_EQ(CONTENT_SETTING_ASK,
map->GetContentSetting(unused_url, unused_url,
ContentSettingsType::GEOLOCATION));
// Travel through time for 20 more days, then revoke abusive notifications
// immediately.
clock.Advance(base::Days(20));
map->SetContentSettingDefaultScope(abusive_url, abusive_url,
ContentSettingsType::NOTIFICATIONS,
CONTENT_SETTING_ALLOW);
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
ASSERT_GT(
safety_hub_util::GetRevokedAbusiveNotificationPermissions(map).size(),
0u);
EXPECT_TRUE(safety_hub_test_util::IsUrlInSettingsList(
safety_hub_util::GetRevokedAbusiveNotificationPermissions(map),
abusive_url));
EXPECT_EQ(CONTENT_SETTING_ASK,
map->GetContentSetting(unused_url, unused_url,
ContentSettingsType::GEOLOCATION));
EXPECT_EQ(CONTENT_SETTING_ASK,
map->GetContentSetting(abusive_url, abusive_url,
ContentSettingsType::NOTIFICATIONS));
}
}
// TODO(crbug.com/400648762): Re-enable the test when it's fixed.
// Test that revocation is happen correctly when auto-revoke is on for a site
// that is abusive then unused.
IN_PROC_BROWSER_TEST_F(AbusiveNotificationPermissionsRevocationBrowserTest,
DISABLED_TestSiteWithFirstAbusiveThenUnusedPermissions) {
auto* map =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
auto* service =
RevokedPermissionsServiceFactory::GetForProfile(browser()->profile());
// Test cases where there is a single URL which is both abusive and unused,
// then when there are separate abusive and unused URLs.
for (const auto& [abusive_url, unused_url] :
std::vector<std::pair<GURL, GURL>>{
{GURL("https://example1.com"), GURL("https://example1.com")},
{GURL("https://example2.com"), GURL("https://example3.com")}}) {
AddDangerousUrl(abusive_url);
// Create content setting 20 days in the past.
base::Time now(base::Time::Now());
base::Time past(now - base::Days(20));
base::SimpleTestClock clock;
clock.SetNow(past);
map->SetClockForTesting(&clock);
service->SetClockForTesting(&clock);
content_settings::ContentSettingConstraints constraints;
constraints.set_track_last_visit_for_autoexpiration(true);
map->SetContentSettingDefaultScope(unused_url, unused_url,
ContentSettingsType::GEOLOCATION,
CONTENT_SETTING_ALLOW, constraints);
clock.SetNow(now);
// Check if the content setting is still ALLOW, before auto-revocation.
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
ASSERT_LT(GetRevokedUnusedPermissions(map).size(), 2u);
EXPECT_FALSE(safety_hub_test_util::IsUrlInSettingsList(
GetRevokedUnusedPermissions(map), unused_url));
EXPECT_EQ(CONTENT_SETTING_ALLOW,
map->GetContentSetting(unused_url, unused_url,
ContentSettingsType::GEOLOCATION));
// Travel through time for 20 more days, then revoke abusive notifications
// immediately. Note the unused permission is not yet revoked.
clock.Advance(base::Days(20));
map->SetContentSettingDefaultScope(abusive_url, abusive_url,
ContentSettingsType::NOTIFICATIONS,
CONTENT_SETTING_ALLOW);
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
ASSERT_LT(GetRevokedUnusedPermissions(map).size(), 2u);
EXPECT_FALSE(safety_hub_test_util::IsUrlInSettingsList(
GetRevokedUnusedPermissions(map), unused_url));
ASSERT_GT(
safety_hub_util::GetRevokedAbusiveNotificationPermissions(map).size(),
0u);
EXPECT_TRUE(safety_hub_test_util::IsUrlInSettingsList(
safety_hub_util::GetRevokedAbusiveNotificationPermissions(map),
abusive_url));
EXPECT_EQ(CONTENT_SETTING_ALLOW,
map->GetContentSetting(unused_url, unused_url,
ContentSettingsType::GEOLOCATION));
EXPECT_EQ(CONTENT_SETTING_ASK,
map->GetContentSetting(abusive_url, abusive_url,
ContentSettingsType::NOTIFICATIONS));
// Travel through time for 20 days to make unused permissions revoked.
// Abusive permission is still revoked.
clock.Advance(base::Days(20));
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
ASSERT_GT(GetRevokedUnusedPermissions(map).size(), 0u);
EXPECT_TRUE(safety_hub_test_util::IsUrlInSettingsList(
GetRevokedUnusedPermissions(map), unused_url));
ASSERT_GT(
safety_hub_util::GetRevokedAbusiveNotificationPermissions(map).size(),
0u);
EXPECT_TRUE(safety_hub_test_util::IsUrlInSettingsList(
safety_hub_util::GetRevokedAbusiveNotificationPermissions(map),
abusive_url));
EXPECT_EQ(CONTENT_SETTING_ASK,
map->GetContentSetting(unused_url, unused_url,
ContentSettingsType::GEOLOCATION));
EXPECT_EQ(CONTENT_SETTING_ASK,
map->GetContentSetting(abusive_url, abusive_url,
ContentSettingsType::NOTIFICATIONS));
}
}
class DisruptiveNotificationPermissionsRevocationShadowRunBrowserTest
: public RevokedPermissionsServiceBrowserTest {
public:
DisruptiveNotificationPermissionsRevocationShadowRunBrowserTest() {
feature_list_.InitAndEnableFeatureWithParameters(
features::kSafetyHubDisruptiveNotificationRevocation,
{
{features::kSafetyHubDisruptiveNotificationRevocationShadowRun.name,
"true"},
});
}
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(
DisruptiveNotificationPermissionsRevocationShadowRunBrowserTest,
TestProposeRevokeDisruptiveNotificationPermissions) {
auto* hcsm =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
auto* service =
RevokedPermissionsServiceFactory::GetForProfile(browser()->profile());
GURL url = embedded_test_server()->GetURL("/title1.html");
// Set up a disruptive notification permission.
hcsm->SetContentSettingDefaultScope(
url, GURL(), ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW);
auto* notifications_engagement_service =
NotificationsEngagementServiceFactory::GetForProfile(
browser()->profile());
notifications_engagement_service->RecordNotificationDisplayed(url, 50);
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
// The url was stored in the disruptive notification content setting.
EXPECT_THAT(
DisruptiveNotificationContentSettingHelper(*hcsm).GetRevocationEntry(url),
Not(Eq(std::nullopt)));
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
std::optional<DisruptiveNotificationRevocationEntry> revocation_entry =
DisruptiveNotificationContentSettingHelper(*hcsm).GetRevocationEntry(url);
EXPECT_THAT(
revocation_entry,
Optional(Field(&DisruptiveNotificationRevocationEntry::revocation_state,
DisruptiveNotificationRevocationState::kProposed)));
ASSERT_EQ(GetRevokedUnusedPermissions(hcsm).size(), 0u);
std::optional<std::unique_ptr<SafetyHubResult>> opt_result =
service->GetCachedResult();
ASSERT_TRUE(opt_result.has_value());
auto* result =
static_cast<RevokedPermissionsResult*>(opt_result.value().get());
EXPECT_EQ(result->GetRevokedPermissions().size(), 0u);
EXPECT_EQ(
CONTENT_SETTING_ALLOW,
hcsm->GetContentSetting(url, url, ContentSettingsType::NOTIFICATIONS));
}
class DisruptiveNotificationPermissionsRevocationBrowserTest
: public RevokedPermissionsServiceBrowserTest {
public:
DisruptiveNotificationPermissionsRevocationBrowserTest() {
#if BUILDFLAG(IS_ANDROID)
feature_list_.InitAndEnableFeatureWithParameters(
features::kSafetyHubDisruptiveNotificationRevocation,
{{features::kSafetyHubDisruptiveNotificationRevocationShadowRun.name,
"false"},
{features::
kSafetyHubDisruptiveNotificationRevocationWaitingForMetricsDays
.name,
"7"}});
#else
feature_list_.InitWithFeaturesAndParameters(
/*enabled_features=*/
{{features::kSafetyHubDisruptiveNotificationRevocation,
{{features::kSafetyHubDisruptiveNotificationRevocationShadowRun.name,
"false"},
{features::
kSafetyHubDisruptiveNotificationRevocationWaitingForMetricsDays
.name,
"7"}}}},
/*disabled_features=*/{
safe_browsing::kShowWarningsForSuspiciousNotifications});
#endif
}
void SetUpOnMainThread() override {
RevokedPermissionsServiceBrowserTest::SetUpOnMainThread();
recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
}
site_engagement::SiteEngagementService* site_engagement_service() {
return site_engagement::SiteEngagementServiceFactory::GetForProfile(
browser()->profile());
}
std::unique_ptr<ukm::TestAutoSetUkmRecorder> recorder_;
private:
base::test::ScopedFeatureList feature_list_;
};
// Test that revocation is happening correctly when auto-revoke is on.
IN_PROC_BROWSER_TEST_F(DisruptiveNotificationPermissionsRevocationBrowserTest,
TestRevokeDisruptiveNotificationPermissions) {
auto* hcsm =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
auto* service =
RevokedPermissionsServiceFactory::GetForProfile(browser()->profile());
GURL url = embedded_test_server()->GetURL("/title1.html");
base::SimpleTestClock clock;
hcsm->SetClockForTesting(&clock);
service->SetClockForTesting(&clock);
clock.SetNow(base::Time::Now());
// Force for the initial safety check to be complete before setting up a
// disruptive notification. Otherwise, sometimes the check is finished before
// and sometimes after the setup which causes flakes.
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
// Set up a disruptive notification permission.
hcsm->SetContentSettingDefaultScope(
url, GURL(), ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW);
auto* notifications_engagement_service =
NotificationsEngagementServiceFactory::GetForProfile(
browser()->profile());
notifications_engagement_service->RecordNotificationDisplayed(url, 50);
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
// The url was stored in the disruptive notification content setting.
std::optional<DisruptiveNotificationRevocationEntry> revocation_entry =
DisruptiveNotificationContentSettingHelper(*hcsm).GetRevocationEntry(url);
EXPECT_THAT(
revocation_entry,
Optional(Field(&DisruptiveNotificationRevocationEntry::revocation_state,
DisruptiveNotificationRevocationState::kProposed)));
// Wait for the disruptive metrics cooldown to expire.
clock.Advance(base::Days(8));
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
revocation_entry =
DisruptiveNotificationContentSettingHelper(*hcsm).GetRevocationEntry(url);
EXPECT_THAT(
revocation_entry,
Optional(Field(&DisruptiveNotificationRevocationEntry::revocation_state,
DisruptiveNotificationRevocationState::kRevoked)));
std::optional<std::unique_ptr<SafetyHubResult>> opt_result =
service->GetCachedResult();
ASSERT_TRUE(opt_result.has_value());
auto* result =
static_cast<RevokedPermissionsResult*>(opt_result.value().get());
EXPECT_EQ(result->GetRevokedPermissions().size(), 1u);
EXPECT_EQ(
CONTENT_SETTING_ASK,
hcsm->GetContentSetting(url, url, ContentSettingsType::NOTIFICATIONS));
}
IN_PROC_BROWSER_TEST_F(
DisruptiveNotificationPermissionsRevocationBrowserTest,
TestRevokeFirstUnusedThenDisruptiveNotificationPermissions) {
auto* hcsm =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
auto* service =
RevokedPermissionsServiceFactory::GetForProfile(browser()->profile());
GURL url = embedded_test_server()->GetURL("/title1.html");
hcsm->SetContentSettingDefaultScope(
url, GURL(), ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW);
// Create content setting 20 days in the past.
base::Time now(base::Time::Now());
base::Time past(now - base::Days(20));
base::SimpleTestClock clock;
clock.SetNow(past);
hcsm->SetClockForTesting(&clock);
service->SetClockForTesting(&clock);
content_settings::ContentSettingConstraints constraints;
constraints.set_track_last_visit_for_autoexpiration(true);
hcsm->SetContentSettingDefaultScope(url, url,
ContentSettingsType::GEOLOCATION,
CONTENT_SETTING_ALLOW, constraints);
clock.SetNow(now);
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
// Travel through time for 40 days.
clock.Advance(base::Days(40));
// Set up a disruptive notification permission.
auto* notifications_engagement_service =
NotificationsEngagementServiceFactory::GetForProfile(
browser()->profile());
notifications_engagement_service->RecordNotificationDisplayed(url, 50);
// Check if the content setting turn to ASK, when auto-revocation happens.
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
ASSERT_EQ(GetRevokedUnusedPermissions(hcsm).size(), 1u);
EXPECT_EQ(
CONTENT_SETTING_ASK,
hcsm->GetContentSetting(url, url, ContentSettingsType::GEOLOCATION));
// The url was stored in the disruptive notification content setting as
// proposed revocation.
std::optional<DisruptiveNotificationRevocationEntry> revocation_entry =
DisruptiveNotificationContentSettingHelper(*hcsm).GetRevocationEntry(url);
EXPECT_THAT(
revocation_entry,
Optional(Field(&DisruptiveNotificationRevocationEntry::revocation_state,
DisruptiveNotificationRevocationState::kProposed)));
// Wait for the disruptive metrics cooldown to expire.
clock.Advance(base::Days(8));
safety_hub_test_util::UpdateRevokedPermissionsServiceAsync(service);
// Both disruptive notifications and unused permissions were revoked for the
// URL.
std::optional<std::unique_ptr<SafetyHubResult>> opt_result =
service->GetCachedResult();
ASSERT_TRUE(opt_result.has_value());
auto* result =
static_cast<RevokedPermissionsResult*>(opt_result.value().get());
EXPECT_EQ(result->GetRevokedPermissions().size(), 1u);
revocation_entry =
DisruptiveNotificationContentSettingHelper(*hcsm).GetRevocationEntry(url);
EXPECT_THAT(
revocation_entry,
Optional(Field(&DisruptiveNotificationRevocationEntry::revocation_state,
DisruptiveNotificationRevocationState::kRevoked)));
EXPECT_EQ(result->GetRevokedPermissions().size(), 1u);
EXPECT_EQ(result->GetRevokedPermissions().front().permission_types.size(),
2u);
EXPECT_TRUE(result->GetRevokedPermissions().front().permission_types.contains(
ContentSettingsType::GEOLOCATION));
EXPECT_TRUE(result->GetRevokedPermissions().front().permission_types.contains(
ContentSettingsType::NOTIFICATIONS));
EXPECT_EQ(
CONTENT_SETTING_ASK,
hcsm->GetContentSetting(url, url, ContentSettingsType::NOTIFICATIONS));
EXPECT_EQ(
CONTENT_SETTING_ASK,
hcsm->GetContentSetting(url, url, ContentSettingsType::GEOLOCATION));
}
IN_PROC_BROWSER_TEST_F(DisruptiveNotificationPermissionsRevocationBrowserTest,
TestProposedFalsePositiveOnPageVisit) {
auto* hcsm =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
GURL url = embedded_test_server()->GetURL("/title1.html");
// Set up a proposed revoked notification.
DisruptiveNotificationRevocationEntry revoked_entry(
/*revocation_state=*/DisruptiveNotificationRevocationState::kProposed,
/*site_engagement=*/0.0,
/*daily_notification_count=*/5,
/*timestamp=*/base::Time::Now() - base::Days(3));
DisruptiveNotificationContentSettingHelper(*hcsm).PersistRevocationEntry(
url, revoked_entry);
// Visit the site.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
auto interaction_entries = recorder_->GetEntriesByName(
"SafetyHub.DisruptiveNotificationRevocations.FalsePositiveInteraction");
ASSERT_EQ(1u, interaction_entries.size());
auto* interaction_entry = interaction_entries[0].get();
recorder_->ExpectEntryMetric(interaction_entry, "DaysSinceRevocation", 3);
recorder_->ExpectEntryMetric(
interaction_entry, "Reason",
static_cast<int>(DisruptiveNotificationPermissionsManager::
FalsePositiveReason::kPageVisit));
// Site engagement hasn't been updated yet.
recorder_->ExpectEntryMetric(interaction_entry, "NewSiteEngagement", 0.0);
recorder_->ExpectEntryMetric(interaction_entry, "OldSiteEngagement", 0.0);
recorder_->ExpectEntryMetric(interaction_entry, "DailyAverageVolume", 5);
}
IN_PROC_BROWSER_TEST_F(DisruptiveNotificationPermissionsRevocationBrowserTest,
TestRevokedFalsePositiveOnPageVisit) {
auto* hcsm =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
GURL url = embedded_test_server()->GetURL("/title1.html");
// Set up a revoked notification.
DisruptiveNotificationRevocationEntry proposed_entry(
/*revocation_state=*/DisruptiveNotificationRevocationState::kRevoked,
/*site_engagement=*/0.0,
/*daily_notification_count=*/5,
/*timestamp=*/base::Time::Now() - base::Days(3));
DisruptiveNotificationContentSettingHelper(*hcsm).PersistRevocationEntry(
url, proposed_entry);
site_engagement_service()->AddPointsForTesting(url, 4.0);
// Visit the page.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
auto interaction_entries = recorder_->GetEntriesByName(
"SafetyHub.DisruptiveNotificationRevocations.FalsePositiveInteraction");
ASSERT_EQ(1u, interaction_entries.size());
auto* interaction_entry = interaction_entries[0].get();
recorder_->ExpectEntryMetric(interaction_entry, "DaysSinceRevocation", 3);
recorder_->ExpectEntryMetric(
interaction_entry, "Reason",
static_cast<int>(DisruptiveNotificationPermissionsManager::
FalsePositiveReason::kPageVisit));
EXPECT_THAT(recorder_->GetEntryMetric(interaction_entry, "NewSiteEngagement"),
Pointee(Ge(4.0)));
recorder_->ExpectEntryMetric(interaction_entry, "OldSiteEngagement", 0.0);
recorder_->ExpectEntryMetric(interaction_entry, "DailyAverageVolume", 5);
auto revocation_entries = recorder_->GetEntriesByName(
"SafetyHub.DisruptiveNotificationRevocations.FalsePositiveRevocation");
ASSERT_EQ(1u, revocation_entries.size());
auto* revocation_entry = revocation_entries[0].get();
recorder_->ExpectEntryMetric(revocation_entry, "DaysSinceRevocation", 3);
recorder_->ExpectEntryMetric(revocation_entry, "PageVisitCount", 1);
recorder_->ExpectEntryMetric(revocation_entry, "NotificationClickCount", 0);
EXPECT_THAT(recorder_->GetEntryMetric(revocation_entry, "NewSiteEngagement"),
Pointee(Ge(4.0)));
recorder_->ExpectEntryMetric(revocation_entry, "OldSiteEngagement", 0.0);
recorder_->ExpectEntryMetric(revocation_entry, "DailyAverageVolume", 5);
// Switch to a different site.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(),
GURL("https://www.another.site")));
// Visit the page again, the revocation has already reported so it won't be
// reported again.
ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
EXPECT_EQ(
2u, recorder_
->GetEntriesByName("SafetyHub.DisruptiveNotificationRevocations."
"FalsePositiveInteraction")
.size());
EXPECT_EQ(
1u, recorder_
->GetEntriesByName("SafetyHub.DisruptiveNotificationRevocations."
"FalsePositiveRevocation")
.size());
}
// TODO(crbug.com/406472515): Add a test for non persistent notification click.
IN_PROC_BROWSER_TEST_F(DisruptiveNotificationPermissionsRevocationBrowserTest,
TestProposedFalsePositiveOnPersistentNotificationClick) {
auto* hcsm =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
auto display_service_tester =
std::make_unique<NotificationDisplayServiceTester>(browser()->profile());
auto* notifications_service =
PlatformNotificationServiceFactory::GetForProfile(browser()->profile());
GURL url = embedded_test_server()->GetURL("/title1.html");
const char kNotificationId[] = "my-notification-id";
// Set up a proposed revoked disruptive notification. The notification are not
// yet revoked.
hcsm->SetContentSettingDefaultScope(
url, GURL(), ContentSettingsType::NOTIFICATIONS, CONTENT_SETTING_ALLOW);
DisruptiveNotificationRevocationEntry proposed_entry(
/*revocation_state=*/DisruptiveNotificationRevocationState::kProposed,
/*site_engagement=*/0.0,
/*daily_notification_count=*/5,
/*timestamp=*/base::Time::Now() - base::Days(3));
DisruptiveNotificationContentSettingHelper(*hcsm).PersistRevocationEntry(
url, proposed_entry);
// Show a notification.
blink::PlatformNotificationData data;
data.title = u"My notification's title";
data.body = u"Hello, world!";
notifications_service->DisplayPersistentNotification(
kNotificationId, url, url, data, blink::NotificationResources());
// Click on the notification.
display_service_tester->SimulateClick(
NotificationHandler::Type::WEB_PERSISTENT, kNotificationId,
0 /*action_index*/, std::nullopt);
auto interaction_entries = recorder_->GetEntriesByName(
"SafetyHub.DisruptiveNotificationRevocations.FalsePositiveInteraction");
ASSERT_EQ(1u, interaction_entries.size());
auto* interaction_entry = interaction_entries[0].get();
recorder_->ExpectEntryMetric(interaction_entry, "DaysSinceRevocation", 3);
recorder_->ExpectEntryMetric(
interaction_entry, "Reason",
static_cast<int>(DisruptiveNotificationPermissionsManager::
FalsePositiveReason::kPersistentNotificationClick));
recorder_->ExpectEntryMetric(interaction_entry, "NewSiteEngagement", 2.0);
recorder_->ExpectEntryMetric(interaction_entry, "OldSiteEngagement", 0.0);
recorder_->ExpectEntryMetric(interaction_entry, "DailyAverageVolume", 5);
auto revocation_entries = recorder_->GetEntriesByName(
"SafetyHub.DisruptiveNotificationRevocations.FalsePositiveRevocation");
ASSERT_EQ(0u, revocation_entries.size());
}