blob: 22918c26ed31a73cd98adf0e67e8650fc2159f77 [file] [log] [blame]
// 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.
#include "chrome/browser/download/download_warning_desktop_hats_utils.h"
#include <memory>
#include "base/files/file_path.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/download/download_item_warning_data.h"
#include "chrome/browser/ui/hats/hats_service.h"
#include "chrome/browser/ui/hats/hats_service_factory.h"
#include "chrome/browser/ui/hats/mock_hats_service.h"
#include "chrome/browser/ui/hats/survey_config.h"
#include "chrome/test/base/testing_profile.h"
#include "components/download/public/common/download_danger_type.h"
#include "components/download/public/common/download_item.h"
#include "components/download/public/common/mock_download_item.h"
#include "components/safe_browsing/buildflags.h"
#include "components/safe_browsing/core/common/features.h"
#include "components/safe_browsing/core/common/proto/csd.pb.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "content/public/browser/download_item_utils.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(SAFE_BROWSING_DOWNLOAD_PROTECTION)
#include "chrome/browser/safe_browsing/download_protection/download_protection_service.h"
#endif
namespace {
using download::DownloadItem;
using download::MockDownloadItem;
using Fields = DownloadWarningHatsProductSpecificData::Fields;
using ::testing::_;
using ::testing::Eq;
using ::testing::HasSubstr;
using ::testing::Key;
using ::testing::NiceMock;
using ::testing::Not;
using ::testing::Return;
using ::testing::ReturnRefOfCopy;
using ::testing::UnorderedElementsAreArray;
constexpr char kUrl[] = "https://www.site.example/file.pdf";
constexpr char kReferrerUrl[] = "https://www.site.example/referrer";
constexpr char kPlaceholderPrefix[] = "Not logged";
const base::FilePath::CharType kFilename[] = FILE_PATH_LITERAL("my_file.pdf");
constexpr base::TimeDelta kIgnoreDelay = base::Seconds(10);
// Matcher that checks for the presence of a particular bits data field and
// checks that the field value matches the given matcher.
MATCHER_P2(BitsDataMatches, field, matcher, "") {
const auto it = arg.bits_data().find(field);
if (it == arg.bits_data().end()) {
return false;
}
return ExplainMatchResult(matcher, it->second, result_listener);
}
// As above, but for string data.
MATCHER_P2(StringDataMatches, field, matcher, "") {
const auto it = arg.string_data().find(field);
if (it == arg.string_data().end()) {
return false;
}
return ExplainMatchResult(matcher, it->second, result_listener);
}
// Checks that the `fields` (vector of strings) exactly matches the keys in the
// `arg` (map of string->T).
MATCHER_P(UnorderedKeysAre, fields, "") {
std::vector<std::string> keys;
for (const auto& [key, val] : arg) {
keys.push_back(key);
}
return ExplainMatchResult(UnorderedElementsAreArray(fields), keys,
result_listener);
}
class DownloadWarningDesktopHatsUtilsTest : public ::testing::Test {
public:
DownloadWarningDesktopHatsUtilsTest() = default;
~DownloadWarningDesktopHatsUtilsTest() override = default;
void SetUp() override {
profile_ = TestingProfile::Builder().Build();
item_ = SetUpMockDownloadItem();
mock_hats_service_ = static_cast<MockHatsService*>(
HatsServiceFactory::GetInstance()->SetTestingFactoryAndUse(
profile_.get(), base::BindRepeating(&BuildMockHatsService)));
}
void TearDown() override { mock_hats_service_ = nullptr; }
std::unique_ptr<NiceMock<MockDownloadItem>> SetUpMockDownloadItem() {
auto item = std::make_unique<NiceMock<MockDownloadItem>>();
content::DownloadItemUtils::AttachInfoForTesting(item.get(), profile_.get(),
nullptr);
SetUpDefaultsForItem(item.get());
return item;
}
// Sets up defaults for the mock download item_. These are not necessarily
// valid/consistent with the Safe Browsing state, but we just want to test
// that the values are reflected in the PSD and don't necessarily care what
// they are.
// Advances the time by 7 seconds.
void SetUpDefaultsForItem(MockDownloadItem* item) {
ON_CALL(*item, GetURL()).WillByDefault(ReturnRefOfCopy(GURL(kUrl)));
ON_CALL(*item, GetReferrerUrl())
.WillByDefault(ReturnRefOfCopy(GURL(kReferrerUrl)));
ON_CALL(*item, GetFileNameToReportUser())
.WillByDefault(Return(base::FilePath(kFilename)));
ON_CALL(*item, IsDangerous()).WillByDefault(Return(true));
ON_CALL(*item, GetDangerType())
.WillByDefault(
Return(download::DownloadDangerType::
DOWNLOAD_DANGER_TYPE_DANGEROUS_ACCOUNT_COMPROMISE));
// Set up the time since download started.
base::Time start_time = base::Time::Now();
ON_CALL(*item, GetStartTime()).WillByDefault(Return(start_time));
// Add some warning action events.
task_environment_.FastForwardBy(base::Seconds(5));
DownloadItemWarningData::AddWarningActionEvent(
item, DownloadItemWarningData::WarningSurface::BUBBLE_MAINPAGE,
DownloadItemWarningData::WarningAction::SHOWN);
task_environment_.FastForwardBy(base::Seconds(1));
DownloadItemWarningData::AddWarningActionEvent(
item, DownloadItemWarningData::WarningSurface::BUBBLE_MAINPAGE,
DownloadItemWarningData::WarningAction::OPEN_SUBPAGE);
task_environment_.FastForwardBy(base::Seconds(1));
DownloadItemWarningData::AddWarningActionEvent(
item, DownloadItemWarningData::WarningSurface::BUBBLE_SUBPAGE,
DownloadItemWarningData::WarningAction::CLOSE);
ON_CALL(*item, IsDone()).WillByDefault(Return(false));
#if BUILDFLAG(SAFE_BROWSING_DOWNLOAD_PROTECTION)
// Set tailored verdict for cookie theft.
safe_browsing::ClientDownloadResponse::TailoredVerdict tailored_verdict;
tailored_verdict.set_tailored_verdict_type(
safe_browsing::ClientDownloadResponse::TailoredVerdict::COOKIE_THEFT);
safe_browsing::DownloadProtectionService::SetDownloadProtectionData(
item, "token",
safe_browsing::ClientDownloadResponse::DANGEROUS_ACCOUNT_COMPROMISE,
std::move(tailored_verdict));
#endif // BUILDFLAG(SAFE_BROWSING_DOWNLOAD_PROTECTION)
ON_CALL(*item, HasUserGesture()).WillByDefault(Return(true));
}
void ExpectDefaultPsd(const DownloadWarningHatsProductSpecificData& psd) {
EXPECT_THAT(psd,
StringDataMatches(Fields::kSecondsSinceDownloadStarted, "7"));
EXPECT_THAT(psd, StringDataMatches(Fields::kSecondsSinceWarningShown, "2"));
EXPECT_THAT(psd, StringDataMatches(Fields::kDangerType,
HasSubstr("AccountCompromise")));
#if BUILDFLAG(FULL_SAFE_BROWSING)
EXPECT_THAT(
psd, StringDataMatches(Fields::kDangerType, HasSubstr("Cookie theft")));
#endif
EXPECT_THAT(psd, StringDataMatches(Fields::kWarningType, "Dangerous"));
EXPECT_THAT(psd, BitsDataMatches(Fields::kUserGesture, true));
EXPECT_THAT(psd, BitsDataMatches(Fields::kPartialViewEnabled, true));
}
void ExpectDefaultPsdForSafeBrowsing(
const DownloadWarningHatsProductSpecificData& psd) {
EXPECT_THAT(psd, StringDataMatches(Fields::kUrlDownload, kUrl));
EXPECT_THAT(psd, StringDataMatches(Fields::kUrlReferrer, kReferrerUrl));
EXPECT_THAT(psd, StringDataMatches(Fields::kFilename, "my_file.pdf"));
}
void ExpectPlaceholderForSafeBrowsing(
const DownloadWarningHatsProductSpecificData& psd) {
EXPECT_THAT(psd, StringDataMatches(Fields::kUrlDownload,
HasSubstr(kPlaceholderPrefix)));
EXPECT_THAT(psd, StringDataMatches(Fields::kUrlReferrer,
HasSubstr(kPlaceholderPrefix)));
EXPECT_THAT(psd, StringDataMatches(Fields::kFilename,
HasSubstr(kPlaceholderPrefix)));
}
void ExpectDefaultPsdForEnhancedSafeBrowsing(
const DownloadWarningHatsProductSpecificData& psd) {
EXPECT_THAT(
psd, StringDataMatches(Fields::kWarningInteractions,
"BUBBLE_MAINPAGE:SHOWN:0,BUBBLE_MAINPAGE:OPEN_"
"SUBPAGE:1000,BUBBLE_SUBPAGE:CLOSE:2000"));
}
void ExpectPlaceholderForEnhancedSafeBrowsing(
const DownloadWarningHatsProductSpecificData& psd) {
EXPECT_THAT(psd, StringDataMatches(Fields::kWarningInteractions,
HasSubstr(kPlaceholderPrefix)));
}
protected:
content::BrowserTaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
base::test::ScopedFeatureList features_;
std::unique_ptr<TestingProfile> profile_;
std::unique_ptr<NiceMock<MockDownloadItem>> item_;
raw_ptr<MockHatsService> mock_hats_service_ = nullptr;
};
TEST_F(DownloadWarningDesktopHatsUtilsTest,
ProductSpecificData_NoSafeBrowsing) {
safe_browsing::SetSafeBrowsingState(
profile_->GetPrefs(), safe_browsing::SafeBrowsingState::NO_SAFE_BROWSING);
auto psd = DownloadWarningHatsProductSpecificData::Create(
DownloadWarningHatsType::kDownloadBubbleBypass, item_.get());
// Test the PSD fields added afterwards.
// This shouldn't do anything because this is a download bubble trigger.
psd.AddNumPageWarnings(10);
psd.AddPartialViewInteraction(true);
// All fields for download bubble are included.
EXPECT_THAT(psd.bits_data(),
UnorderedKeysAre(
DownloadWarningHatsProductSpecificData::GetBitsDataFields(
DownloadWarningHatsType::kDownloadBubbleBypass)));
EXPECT_THAT(psd.string_data(),
UnorderedKeysAre(
DownloadWarningHatsProductSpecificData::GetStringDataFields(
DownloadWarningHatsType::kDownloadBubbleBypass)));
ExpectDefaultPsd(psd);
ExpectPlaceholderForSafeBrowsing(psd);
ExpectPlaceholderForEnhancedSafeBrowsing(psd);
EXPECT_THAT(
psd, StringDataMatches(Fields::kSafeBrowsingState, "No Safe Browsing"));
EXPECT_THAT(psd, StringDataMatches(Fields::kOutcome, HasSubstr("Bypass")));
EXPECT_THAT(psd, StringDataMatches(Fields::kSurface, HasSubstr("bubble")));
EXPECT_THAT(psd, BitsDataMatches(Fields::kPartialViewInteraction, true));
EXPECT_THAT(psd, Not(StringDataMatches(Fields::kNumPageWarnings, _)));
}
TEST_F(DownloadWarningDesktopHatsUtilsTest,
ProductSpecificData_StandardSafeBrowsing) {
safe_browsing::SetSafeBrowsingState(
profile_->GetPrefs(),
safe_browsing::SafeBrowsingState::STANDARD_PROTECTION);
auto psd = DownloadWarningHatsProductSpecificData::Create(
DownloadWarningHatsType::kDownloadsPageHeed, item_.get());
// Test the PSD fields added afterwards.
psd.AddNumPageWarnings(10);
// This shouldn't do anything because this is a download page trigger.
psd.AddPartialViewInteraction(true);
// All fields for downloads page are included.
EXPECT_THAT(psd.bits_data(),
UnorderedKeysAre(
DownloadWarningHatsProductSpecificData::GetBitsDataFields(
DownloadWarningHatsType::kDownloadsPageHeed)));
EXPECT_THAT(psd.string_data(),
UnorderedKeysAre(
DownloadWarningHatsProductSpecificData::GetStringDataFields(
DownloadWarningHatsType::kDownloadsPageHeed)));
ExpectDefaultPsd(psd);
ExpectDefaultPsdForSafeBrowsing(psd);
ExpectPlaceholderForEnhancedSafeBrowsing(psd);
EXPECT_THAT(psd, StringDataMatches(Fields::kSafeBrowsingState,
"Standard Protection"));
EXPECT_THAT(psd, StringDataMatches(Fields::kOutcome, HasSubstr("Heed")));
EXPECT_THAT(psd, StringDataMatches(Fields::kSurface, HasSubstr("page")));
EXPECT_THAT(psd, StringDataMatches(Fields::kNumPageWarnings, "10"));
EXPECT_THAT(psd, Not(BitsDataMatches(Fields::kPartialViewInteraction, _)));
}
TEST_F(DownloadWarningDesktopHatsUtilsTest,
ProductSpecificData_EnhancedSafeBrowsing) {
safe_browsing::SetSafeBrowsingState(
profile_->GetPrefs(),
safe_browsing::SafeBrowsingState::ENHANCED_PROTECTION);
auto psd = DownloadWarningHatsProductSpecificData::Create(
DownloadWarningHatsType::kDownloadBubbleIgnore, item_.get());
// Test the PSD fields added afterwards.
// This shouldn't do anything because this is a download bubble trigger.
psd.AddNumPageWarnings(10);
psd.AddPartialViewInteraction(true);
// All fields for download bubble are included.
EXPECT_THAT(psd.bits_data(),
UnorderedKeysAre(
DownloadWarningHatsProductSpecificData::GetBitsDataFields(
DownloadWarningHatsType::kDownloadBubbleIgnore)));
EXPECT_THAT(psd.string_data(),
UnorderedKeysAre(
DownloadWarningHatsProductSpecificData::GetStringDataFields(
DownloadWarningHatsType::kDownloadBubbleIgnore)));
ExpectDefaultPsd(psd);
ExpectDefaultPsdForSafeBrowsing(psd);
ExpectDefaultPsdForEnhancedSafeBrowsing(psd);
EXPECT_THAT(psd, StringDataMatches(Fields::kSafeBrowsingState,
"Enhanced Protection"));
EXPECT_THAT(psd, StringDataMatches(Fields::kOutcome, HasSubstr("Ignore")));
EXPECT_THAT(psd, StringDataMatches(Fields::kSurface, HasSubstr("bubble")));
EXPECT_THAT(psd, BitsDataMatches(Fields::kPartialViewInteraction, true));
EXPECT_THAT(psd, Not(StringDataMatches(Fields::kNumPageWarnings, _)));
}
TEST_F(DownloadWarningDesktopHatsUtilsTest,
DelayedDownloadWarningHatsLauncher_LaunchesSurvey) {
base::test::ScopedFeatureList features;
features.InitAndEnableFeatureWithParameters(
safe_browsing::kDownloadWarningSurvey,
{{safe_browsing::kDownloadWarningSurveyType.name,
"2" /*kDownloadBubbleIgnore*/}});
DelayedDownloadWarningHatsLauncher launcher{profile_.get(), kIgnoreDelay};
EXPECT_TRUE(launcher.TryScheduleTask(
DownloadWarningHatsType::kDownloadBubbleIgnore, item_.get()));
launcher.RecordBrowserActivity();
EXPECT_CALL(*mock_hats_service_,
LaunchSurvey(kHatsSurveyTriggerDownloadWarningBubbleIgnore, _, _,
_, _, _, _));
task_environment_.FastForwardBy(kIgnoreDelay);
}
TEST_F(DownloadWarningDesktopHatsUtilsTest,
DelayedDownloadWarningHatsLauncher_DoesntScheduleDuplicateSurvey) {
base::test::ScopedFeatureList features;
features.InitAndEnableFeatureWithParameters(
safe_browsing::kDownloadWarningSurvey,
{{safe_browsing::kDownloadWarningSurveyType.name,
"2" /*kDownloadBubbleIgnore*/}});
DelayedDownloadWarningHatsLauncher launcher{profile_.get(), kIgnoreDelay};
EXPECT_TRUE(launcher.TryScheduleTask(
DownloadWarningHatsType::kDownloadBubbleIgnore, item_.get()));
EXPECT_FALSE(launcher.TryScheduleTask(
DownloadWarningHatsType::kDownloadBubbleIgnore, item_.get()));
}
TEST_F(DownloadWarningDesktopHatsUtilsTest,
DelayedDownloadWarningHatsLauncher_MultipleSurveys) {
safe_browsing::SetSafeBrowsingState(
profile_->GetPrefs(),
safe_browsing::SafeBrowsingState::STANDARD_PROTECTION);
base::test::ScopedFeatureList features;
features.InitAndEnableFeatureWithParameters(
safe_browsing::kDownloadWarningSurvey,
{{safe_browsing::kDownloadWarningSurveyType.name,
"2" /*kDownloadBubbleIgnore*/}});
std::unique_ptr<NiceMock<MockDownloadItem>> other_item =
SetUpMockDownloadItem();
ON_CALL(*other_item, GetFileNameToReportUser())
.WillByDefault(
Return(base::FilePath(FILE_PATH_LITERAL("other_file.pdf"))));
DelayedDownloadWarningHatsLauncher launcher{profile_.get(), kIgnoreDelay};
EXPECT_TRUE(launcher.TryScheduleTask(
DownloadWarningHatsType::kDownloadBubbleIgnore, item_.get()));
launcher.RecordBrowserActivity();
task_environment_.FastForwardBy(kIgnoreDelay / 2);
EXPECT_TRUE(launcher.TryScheduleTask(
DownloadWarningHatsType::kDownloadBubbleIgnore, other_item.get()));
{
EXPECT_CALL(
*mock_hats_service_,
LaunchSurvey(
kHatsSurveyTriggerDownloadWarningBubbleIgnore, _, _, _,
Contains(Pair(Fields::kFilename, HasSubstr("my_file.pdf"))), _, _));
task_environment_.FastForwardBy(kIgnoreDelay / 2);
}
launcher.RecordBrowserActivity();
{
EXPECT_CALL(
*mock_hats_service_,
LaunchSurvey(
kHatsSurveyTriggerDownloadWarningBubbleIgnore, _, _, _,
Contains(Pair(Fields::kFilename, HasSubstr("other_file.pdf"))), _,
_));
task_environment_.FastForwardBy(kIgnoreDelay / 2);
}
}
TEST_F(DownloadWarningDesktopHatsUtilsTest,
DelayedDownloadWarningHatsLauncher_WithholdsSurveyIfNoUserActivity) {
base::test::ScopedFeatureList features;
features.InitAndEnableFeatureWithParameters(
safe_browsing::kDownloadWarningSurvey,
{{safe_browsing::kDownloadWarningSurveyType.name,
"2" /*kDownloadBubbleIgnore*/}});
DelayedDownloadWarningHatsLauncher launcher{profile_.get(), kIgnoreDelay};
launcher.TryScheduleTask(DownloadWarningHatsType::kDownloadBubbleIgnore,
item_.get());
EXPECT_CALL(*mock_hats_service_,
LaunchSurvey(kHatsSurveyTriggerDownloadWarningBubbleIgnore, _, _,
_, _, _, _))
.Times(0);
task_environment_.FastForwardBy(2 * kIgnoreDelay);
}
TEST_F(DownloadWarningDesktopHatsUtilsTest,
DelayedDownloadWarningHatsLauncher_DeletesTaskWhenItemDeleted) {
base::test::ScopedFeatureList features;
features.InitAndEnableFeatureWithParameters(
safe_browsing::kDownloadWarningSurvey,
{{safe_browsing::kDownloadWarningSurveyType.name,
"2" /*kDownloadBubbleIgnore*/}});
DelayedDownloadWarningHatsLauncher launcher{profile_.get(), kIgnoreDelay};
EXPECT_TRUE(launcher.TryScheduleTask(
DownloadWarningHatsType::kDownloadBubbleIgnore, item_.get()));
item_.reset();
launcher.RecordBrowserActivity();
EXPECT_CALL(*mock_hats_service_,
LaunchSurvey(kHatsSurveyTriggerDownloadWarningBubbleIgnore, _, _,
_, _, _, _))
.Times(0);
task_environment_.FastForwardBy(kIgnoreDelay);
item_ = SetUpMockDownloadItem();
// Trying again works because the older task was deleted.
EXPECT_TRUE(launcher.TryScheduleTask(
DownloadWarningHatsType::kDownloadBubbleIgnore, item_.get()));
}
TEST_F(DownloadWarningDesktopHatsUtilsTest,
MaybeGetDownloadWarningHatsTrigger_FeatureDisabled) {
base::test::ScopedFeatureList features;
features.InitAndDisableFeature(safe_browsing::kDownloadWarningSurvey);
for (DownloadWarningHatsType type :
{DownloadWarningHatsType::kDownloadBubbleBypass,
DownloadWarningHatsType::kDownloadBubbleHeed,
DownloadWarningHatsType::kDownloadBubbleIgnore,
DownloadWarningHatsType::kDownloadsPageBypass,
DownloadWarningHatsType::kDownloadsPageHeed,
DownloadWarningHatsType::kDownloadsPageIgnore}) {
EXPECT_FALSE(MaybeGetDownloadWarningHatsTrigger(type));
}
}
TEST_F(DownloadWarningDesktopHatsUtilsTest,
MaybeGetDownloadWarningHatsTrigger_ParamOutOfRange) {
for (DownloadWarningHatsType type :
{DownloadWarningHatsType::kDownloadBubbleBypass,
DownloadWarningHatsType::kDownloadBubbleHeed,
DownloadWarningHatsType::kDownloadBubbleIgnore,
DownloadWarningHatsType::kDownloadsPageBypass,
DownloadWarningHatsType::kDownloadsPageHeed,
DownloadWarningHatsType::kDownloadsPageIgnore}) {
for (const std::string& param_value : {"", "-1", "6"}) {
base::test::ScopedFeatureList features;
features.InitAndEnableFeatureWithParameters(
safe_browsing::kDownloadWarningSurvey,
{{safe_browsing::kDownloadWarningSurveyType.name, param_value}});
EXPECT_FALSE(MaybeGetDownloadWarningHatsTrigger(type));
}
}
}
TEST_F(DownloadWarningDesktopHatsUtilsTest,
MaybeGetDownloadWarningHatsTrigger_OnlyReturnsTriggerIfEligible) {
for (DownloadWarningHatsType type :
{DownloadWarningHatsType::kDownloadBubbleBypass,
DownloadWarningHatsType::kDownloadBubbleHeed,
DownloadWarningHatsType::kDownloadBubbleIgnore,
DownloadWarningHatsType::kDownloadsPageBypass,
DownloadWarningHatsType::kDownloadsPageHeed,
DownloadWarningHatsType::kDownloadsPageIgnore}) {
for (int param_value = 0; param_value < 6; ++param_value) {
std::string param_value_string = base::NumberToString(param_value);
base::test::ScopedFeatureList features;
features.InitAndEnableFeatureWithParameters(
safe_browsing::kDownloadWarningSurvey,
{{safe_browsing::kDownloadWarningSurveyType.name,
param_value_string}});
bool eligible = param_value == static_cast<int>(type);
EXPECT_EQ(MaybeGetDownloadWarningHatsTrigger(type).has_value(), eligible);
}
}
}
} // namespace