blob: 9611336daef199f899a7d41992dce4948bdbec05 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/permissions/contextual_notification_permission_ui_selector.h"
#include <memory>
#include <string>
#include <utility>
#include "base/macros.h"
#include "base/memory/scoped_refptr.h"
#include "base/optional.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/permissions/crowd_deny.pb.h"
#include "chrome/browser/permissions/crowd_deny_fake_safe_browsing_database_manager.h"
#include "chrome/browser/permissions/crowd_deny_preload_data.h"
#include "chrome/browser/permissions/quiet_notification_permission_ui_config.h"
#include "chrome/browser/safe_browsing/test_safe_browsing_service.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "components/permissions/test/mock_permission_request.h"
#include "components/prefs/testing_pref_service.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace {
using QuietUiReason = ContextualNotificationPermissionUiSelector::QuietUiReason;
using WarningReason = ContextualNotificationPermissionUiSelector::WarningReason;
using Decision = ContextualNotificationPermissionUiSelector::Decision;
using SiteReputation = chrome_browser_crowd_deny::SiteReputation;
constexpr char kTestDomainUnknown[] = "unknown.com";
constexpr char kTestDomainAcceptable[] = "acceptable.com";
constexpr char kTestDomainSpammy[] = "spammy.com";
constexpr char kTestDomainSpammyWarn[] = "warn-spammy.com";
constexpr char kTestDomainAbusivePrompts[] = "abusive_prompts.com";
constexpr char kTestDomainAbusivePromptsWarn[] = "warn_prompts.com";
constexpr char kTestOriginNoData[] = "https://nodata.com/";
constexpr char kTestOriginUnknown[] = "https://unknown.com/";
constexpr char kTestOriginAcceptable[] = "https://acceptable.com/";
constexpr char kTestOriginSpammy[] = "https://spammy.com/";
constexpr char kTestOriginSpammyWarn[] = "https://warn-spammy.com/";
constexpr char kTestOriginAbusivePrompts[] = "https://abusive-prompts.com/";
constexpr char kTestOriginSubDomainOfAbusivePrompts[] =
"https://b.abusive-prompts.com/";
constexpr char kTestOriginAbusivePromptsWarn[] =
"https://warn-abusive-prompts.com/";
constexpr const char* kAllTestingOrigins[] = {
kTestOriginNoData,
kTestOriginUnknown,
kTestOriginAcceptable,
kTestOriginSpammy,
kTestOriginSpammyWarn,
kTestOriginAbusivePrompts,
kTestOriginSubDomainOfAbusivePrompts,
kTestOriginAbusivePromptsWarn,
};
} // namespace
class ContextualNotificationPermissionUiSelectorTest : public testing::Test {
public:
ContextualNotificationPermissionUiSelectorTest()
: testing_profile_(std::make_unique<TestingProfile>()),
contextual_selector_(testing_profile_.get()) {}
~ContextualNotificationPermissionUiSelectorTest() override = default;
protected:
void SetUp() override {
testing::Test::SetUp();
fake_database_manager_ =
base::MakeRefCounted<CrowdDenyFakeSafeBrowsingDatabaseManager>();
safe_browsing_factory_ =
std::make_unique<safe_browsing::TestSafeBrowsingServiceFactory>();
safe_browsing_factory_->SetTestDatabaseManager(
fake_database_manager_.get());
TestingBrowserProcess::GetGlobal()->SetSafeBrowsingService(
safe_browsing_factory_->CreateSafeBrowsingService());
}
void TearDown() override {
TestingBrowserProcess::GetGlobal()->SetSafeBrowsingService(nullptr);
testing::Test::TearDown();
}
void SetQuietUiEnabledInPrefs(bool enabled) {
testing_profile_->GetPrefs()->SetBoolean(
prefs::kEnableQuietNotificationPermissionUi, enabled);
}
void LoadTestPreloadData() {
using SiteReputation = CrowdDenyPreloadData::SiteReputation;
SiteReputation reputation_unknown;
reputation_unknown.set_domain(kTestDomainUnknown);
reputation_unknown.set_notification_ux_quality(SiteReputation::UNKNOWN);
testing_preload_data_.SetOriginReputation(
url::Origin::Create(GURL(kTestOriginUnknown)),
std::move(reputation_unknown));
SiteReputation reputation_acceptable;
reputation_acceptable.set_domain(kTestDomainAcceptable);
reputation_acceptable.set_notification_ux_quality(
SiteReputation::ACCEPTABLE);
testing_preload_data_.SetOriginReputation(
url::Origin::Create(GURL(kTestOriginAcceptable)),
std::move(reputation_acceptable));
SiteReputation reputation_spammy;
reputation_spammy.set_domain(kTestDomainSpammy);
reputation_spammy.set_notification_ux_quality(
SiteReputation::UNSOLICITED_PROMPTS);
testing_preload_data_.SetOriginReputation(
url::Origin::Create(GURL(kTestOriginSpammy)),
std::move(reputation_spammy));
SiteReputation reputation_spammy_warn;
reputation_spammy_warn.set_domain(kTestDomainSpammyWarn);
reputation_spammy_warn.set_notification_ux_quality(
SiteReputation::UNSOLICITED_PROMPTS);
reputation_spammy_warn.set_warning_only(true);
testing_preload_data_.SetOriginReputation(
url::Origin::Create(GURL(kTestOriginSpammyWarn)),
std::move(reputation_spammy_warn));
SiteReputation reputation_abusive;
reputation_abusive.set_domain(kTestDomainAbusivePrompts);
reputation_abusive.set_notification_ux_quality(
SiteReputation::ABUSIVE_PROMPTS);
reputation_abusive.set_include_subdomains(true);
testing_preload_data_.SetOriginReputation(
url::Origin::Create(GURL(kTestOriginAbusivePrompts)),
std::move(reputation_abusive));
SiteReputation reputation_abusive_warn;
reputation_abusive_warn.set_domain(kTestDomainAbusivePromptsWarn);
reputation_abusive_warn.set_notification_ux_quality(
SiteReputation::ABUSIVE_PROMPTS);
reputation_abusive_warn.set_warning_only(true);
reputation_abusive_warn.set_include_subdomains(true);
testing_preload_data_.SetOriginReputation(
url::Origin::Create(GURL(kTestOriginAbusivePromptsWarn)),
std::move(reputation_abusive_warn));
}
void AddUrlToFakeApiAbuseBlocklist(const GURL& url) {
safe_browsing::ThreatMetadata test_metadata;
test_metadata.api_permissions.emplace("NOTIFICATIONS");
fake_database_manager_->SetSimulatedMetadataForUrl(url, test_metadata);
}
void LoadTestSafeBrowsingBlocklist() {
// For simplicity, Safe Browsing will simulate a match for all testing
// origins, tests can clear the fake results to simulate a miss.
for (const char* origin : kAllTestingOrigins) {
AddUrlToFakeApiAbuseBlocklist(GURL(origin));
}
}
void ClearSafeBrowsingBlocklist() {
fake_database_manager_->RemoveAllBlacklistedUrls();
}
void QueryAndExpectDecisionForUrl(
const GURL& origin,
base::Optional<QuietUiReason> quiet_ui_reason,
base::Optional<WarningReason> warning_reason) {
permissions::MockPermissionRequest mock_request(
std::string(),
permissions::PermissionRequestType::PERMISSION_NOTIFICATIONS, origin);
base::MockCallback<
ContextualNotificationPermissionUiSelector::DecisionMadeCallback>
mock_callback;
Decision actual_decison(base::nullopt, base::nullopt);
EXPECT_CALL(mock_callback, Run)
.WillRepeatedly(testing::SaveArg<0>(&actual_decison));
contextual_selector_.SelectUiToUse(&mock_request, mock_callback.Get());
task_environment_.RunUntilIdle();
testing::Mock::VerifyAndClearExpectations(&mock_callback);
EXPECT_EQ(quiet_ui_reason, actual_decison.quiet_ui_reason);
EXPECT_EQ(warning_reason, actual_decison.warning_reason);
}
private:
content::BrowserTaskEnvironment task_environment_;
std::unique_ptr<TestingProfile> testing_profile_;
testing::ScopedCrowdDenyPreloadDataOverride testing_preload_data_;
scoped_refptr<CrowdDenyFakeSafeBrowsingDatabaseManager>
fake_database_manager_;
std::unique_ptr<safe_browsing::TestSafeBrowsingServiceFactory>
safe_browsing_factory_;
ContextualNotificationPermissionUiSelector contextual_selector_;
DISALLOW_COPY_AND_ASSIGN(ContextualNotificationPermissionUiSelectorTest);
};
// With all the field trials enabled, test all combinations of:
// (a) quiet UI being enabled/disabled in prefs, and
// (b) positive/negative Safe Browsing verdicts.
TEST_F(ContextualNotificationPermissionUiSelectorTest,
PrefAndSafeBrowsingCombinations) {
using Config = QuietNotificationPermissionUiConfig;
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeatureWithParameters(
features::kQuietNotificationPrompts,
{{Config::kEnableAdaptiveActivation, "true"},
{Config::kEnableCrowdDenyTriggering, "true"},
{Config::kEnableAbusiveRequestBlocking, "true"},
{Config::kEnableAbusiveRequestWarning, "true"}});
LoadTestPreloadData();
{
SetQuietUiEnabledInPrefs(false);
ClearSafeBrowsingBlocklist();
SCOPED_TRACE("Quiet UI disabled in prefs, Safe Browsing verdicts negative");
for (const auto* origin_string : kAllTestingOrigins) {
SCOPED_TRACE(origin_string);
QueryAndExpectDecisionForUrl(GURL(origin_string), Decision::UseNormalUi(),
Decision::ShowNoWarning());
}
}
{
SetQuietUiEnabledInPrefs(true);
ClearSafeBrowsingBlocklist();
SCOPED_TRACE("Quiet UI enabled in prefs, Safe Browsing verdicts negative");
for (const auto* origin_string : kAllTestingOrigins) {
SCOPED_TRACE(origin_string);
QueryAndExpectDecisionForUrl(GURL(origin_string),
QuietUiReason::kEnabledInPrefs,
Decision::ShowNoWarning());
}
}
{
SetQuietUiEnabledInPrefs(false);
LoadTestSafeBrowsingBlocklist();
const struct {
const char* origin_string;
base::Optional<QuietUiReason> expected_ui_reason =
Decision::UseNormalUi();
base::Optional<WarningReason> expected_warning_reason =
Decision::ShowNoWarning();
} kTestCases[] = {
{kTestOriginNoData},
{kTestOriginUnknown},
{kTestOriginAcceptable},
{kTestOriginSpammy, QuietUiReason::kTriggeredByCrowdDeny},
{kTestOriginSpammyWarn},
{kTestOriginAbusivePrompts,
QuietUiReason::kTriggeredDueToAbusiveRequests},
{kTestOriginSubDomainOfAbusivePrompts,
QuietUiReason::kTriggeredDueToAbusiveRequests},
{kTestOriginAbusivePromptsWarn, Decision::UseNormalUi(),
WarningReason::kAbusiveRequests},
};
SCOPED_TRACE("Quiet UI disabled in prefs, Safe Browsing verdicts positive");
for (const auto& test : kTestCases) {
SCOPED_TRACE(test.origin_string);
QueryAndExpectDecisionForUrl(GURL(test.origin_string),
test.expected_ui_reason,
test.expected_warning_reason);
}
}
{
SetQuietUiEnabledInPrefs(true);
LoadTestSafeBrowsingBlocklist();
const struct {
const char* origin_string;
base::Optional<QuietUiReason> expected_ui_reason =
Decision::UseNormalUi();
base::Optional<WarningReason> expected_warning_reason =
Decision::ShowNoWarning();
} kTestCases[] = {
{kTestOriginNoData, QuietUiReason::kEnabledInPrefs},
{kTestOriginUnknown, QuietUiReason::kEnabledInPrefs},
{kTestOriginAcceptable, QuietUiReason::kEnabledInPrefs},
{kTestOriginSpammy, QuietUiReason::kTriggeredByCrowdDeny},
{kTestOriginSpammyWarn, QuietUiReason::kEnabledInPrefs},
{kTestOriginAbusivePrompts,
QuietUiReason::kTriggeredDueToAbusiveRequests},
{kTestOriginSubDomainOfAbusivePrompts,
QuietUiReason::kTriggeredDueToAbusiveRequests},
{kTestOriginAbusivePromptsWarn, QuietUiReason::kEnabledInPrefs,
WarningReason::kAbusiveRequests},
};
SCOPED_TRACE("Quiet UI enabled in prefs, Safe Browsing verdicts positive");
for (const auto& test : kTestCases) {
SCOPED_TRACE(test.origin_string);
QueryAndExpectDecisionForUrl(GURL(test.origin_string),
test.expected_ui_reason,
test.expected_warning_reason);
}
}
}
TEST_F(ContextualNotificationPermissionUiSelectorTest, FeatureDisabled) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(features::kQuietNotificationPrompts);
SetQuietUiEnabledInPrefs(true);
LoadTestPreloadData();
LoadTestSafeBrowsingBlocklist();
for (const auto* origin_string : kAllTestingOrigins) {
SCOPED_TRACE(origin_string);
QueryAndExpectDecisionForUrl(GURL(origin_string), Decision::UseNormalUi(),
Decision::ShowNoWarning());
}
}
// The feature is enabled but no adaptive triggers are enabled.
TEST_F(ContextualNotificationPermissionUiSelectorTest, AllTriggersDisabled) {
using Config = QuietNotificationPermissionUiConfig;
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeatureWithParameters(
features::kQuietNotificationPrompts,
{{Config::kEnableAdaptiveActivation, "true"}});
SetQuietUiEnabledInPrefs(true);
LoadTestPreloadData();
LoadTestSafeBrowsingBlocklist();
for (const auto* origin_string : kAllTestingOrigins) {
SCOPED_TRACE(origin_string);
QueryAndExpectDecisionForUrl(GURL(origin_string),
QuietUiReason::kEnabledInPrefs,
Decision::ShowNoWarning());
}
}
// The feature is enabled but only the `crowd deny` trigger is enabled.
TEST_F(ContextualNotificationPermissionUiSelectorTest, OnlyCrowdDenyEnabled) {
using Config = QuietNotificationPermissionUiConfig;
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeatureWithParameters(
features::kQuietNotificationPrompts,
{{Config::kEnableAdaptiveActivation, "true"},
{Config::kEnableCrowdDenyTriggering, "true"}});
SetQuietUiEnabledInPrefs(false);
LoadTestPreloadData();
LoadTestSafeBrowsingBlocklist();
const struct {
const char* origin_string;
base::Optional<QuietUiReason> expected_ui_reason = Decision::UseNormalUi();
base::Optional<WarningReason> expected_warning_reason =
Decision::ShowNoWarning();
} kTestCases[] = {
{kTestOriginSpammy, QuietUiReason::kTriggeredByCrowdDeny},
{kTestOriginSpammyWarn},
{kTestOriginAbusivePrompts},
{kTestOriginSubDomainOfAbusivePrompts},
{kTestOriginAbusivePromptsWarn},
};
for (const auto& test : kTestCases) {
SCOPED_TRACE(test.origin_string);
QueryAndExpectDecisionForUrl(GURL(test.origin_string),
test.expected_ui_reason,
test.expected_warning_reason);
}
}
// The feature is enabled but only the `abusive prompts` trigger is enabled.
TEST_F(ContextualNotificationPermissionUiSelectorTest,
OnlyAbusivePromptBlockingEnabled) {
using Config = QuietNotificationPermissionUiConfig;
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeatureWithParameters(
features::kQuietNotificationPrompts,
{{Config::kEnableAdaptiveActivation, "true"},
{Config::kEnableAbusiveRequestBlocking, "true"}});
SetQuietUiEnabledInPrefs(false);
LoadTestPreloadData();
LoadTestSafeBrowsingBlocklist();
const struct {
const char* origin_string;
base::Optional<QuietUiReason> expected_ui_reason = Decision::UseNormalUi();
base::Optional<WarningReason> expected_warning_reason =
Decision::ShowNoWarning();
} kTestCases[] = {
{kTestOriginSpammy},
{kTestOriginSpammyWarn},
{kTestOriginAbusivePrompts,
QuietUiReason::kTriggeredDueToAbusiveRequests},
{kTestOriginSubDomainOfAbusivePrompts,
QuietUiReason::kTriggeredDueToAbusiveRequests},
{kTestOriginAbusivePromptsWarn},
};
for (const auto& test : kTestCases) {
SCOPED_TRACE(test.origin_string);
QueryAndExpectDecisionForUrl(GURL(test.origin_string),
test.expected_ui_reason,
test.expected_warning_reason);
}
}
// The feature is enabled but only the `abusive prompts` warning is enabled.
TEST_F(ContextualNotificationPermissionUiSelectorTest,
OnlyAbusivePromptWarningsEnabled) {
using Config = QuietNotificationPermissionUiConfig;
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeatureWithParameters(
features::kQuietNotificationPrompts,
{{Config::kEnableAdaptiveActivation, "true"},
{Config::kEnableAbusiveRequestWarning, "true"}});
SetQuietUiEnabledInPrefs(false);
LoadTestPreloadData();
LoadTestSafeBrowsingBlocklist();
const struct {
const char* origin_string;
base::Optional<QuietUiReason> expected_ui_reason = Decision::UseNormalUi();
base::Optional<WarningReason> expected_warning_reason =
Decision::ShowNoWarning();
} kTestCases[] = {
{kTestOriginSpammy},
{kTestOriginSpammyWarn},
{kTestOriginAbusivePrompts},
{kTestOriginSubDomainOfAbusivePrompts},
{kTestOriginAbusivePromptsWarn, Decision::UseNormalUi(),
WarningReason::kAbusiveRequests},
};
for (const auto& test : kTestCases) {
SCOPED_TRACE(test.origin_string);
QueryAndExpectDecisionForUrl(GURL(test.origin_string),
test.expected_ui_reason,
test.expected_warning_reason);
}
}
TEST_F(ContextualNotificationPermissionUiSelectorTest,
CrowdDenyHoldbackChance) {
const struct {
std::string holdback_chance;
bool enabled_in_prefs;
base::Optional<QuietUiReason> expected_ui_reason;
bool expected_histogram_bucket;
} kTestCases[] = {
// 100% chance to holdback, the UI used should be the normal UI.
{"1.0", false, Decision::UseNormalUi(), true},
// 0% chance to holdback, the UI used should be the quiet UI.
{"0.0", false, QuietUiReason::kTriggeredByCrowdDeny},
// 100% chance to holdback but the quiet UI is enabled by the user in
// prefs, the UI used should be the quiet UI.
{"1.0", true, QuietUiReason::kEnabledInPrefs, true},
};
LoadTestPreloadData();
LoadTestSafeBrowsingBlocklist();
for (const auto& test : kTestCases) {
SCOPED_TRACE(test.holdback_chance);
SCOPED_TRACE(test.enabled_in_prefs);
using Config = QuietNotificationPermissionUiConfig;
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeatureWithParameters(
features::kQuietNotificationPrompts,
{{Config::kEnableAdaptiveActivation, "true"},
{Config::kEnableAbusiveRequestBlocking, "true"},
{Config::kEnableAbusiveRequestWarning, "true"},
{Config::kEnableCrowdDenyTriggering, "true"},
{Config::kCrowdDenyHoldBackChance, test.holdback_chance}});
SetQuietUiEnabledInPrefs(test.enabled_in_prefs);
base::HistogramTester histograms;
QueryAndExpectDecisionForUrl(GURL(kTestOriginSpammy),
test.expected_ui_reason,
Decision::ShowNoWarning());
// The hold-back should not apply to other per-site triggers.
QueryAndExpectDecisionForUrl(GURL(kTestOriginAbusivePrompts),
QuietUiReason::kTriggeredDueToAbusiveRequests,
Decision::ShowNoWarning());
QueryAndExpectDecisionForUrl(GURL(kTestOriginAbusivePromptsWarn),
test.enabled_in_prefs
? QuietUiReason::kEnabledInPrefs
: Decision::UseNormalUi(),
WarningReason::kAbusiveRequests);
auto expected_bucket = static_cast<base::HistogramBase::Sample>(
test.expected_histogram_bucket);
histograms.ExpectBucketCount("Permissions.CrowdDeny.DidHoldbackQuietUi",
expected_bucket, 1);
}
}