blob: 6ee02833deb9751d59653be97d5fbaa328a12566 [file] [log] [blame]
// Copyright 2025 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/win/installer_downloader/installer_downloader_controller.h"
#include <memory>
#include <optional>
#include <utility>
#include "base/base_paths.h"
#include "base/feature_list.h"
#include "base/files/file_path.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_path_override.h"
#include "chrome/browser/win/installer_downloader/installer_downloader_feature.h"
#include "chrome/browser/win/installer_downloader/installer_downloader_model.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/mock_download_manager.h"
#include "content/public/test/test_web_contents_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
using base::test::RunOnceCallback;
using ::testing::_;
using ::testing::AllOf;
using ::testing::ContainsRegex;
using ::testing::HasSubstr;
using ::testing::MatchesRegex;
using ::testing::Not;
using ::testing::Property;
using ::testing::Return;
using ::testing::SaveArg;
using ::testing::StrictMock;
namespace installer_downloader {
namespace {
// A simple, valid template: IIDGUID and STATS are placeholders that the
// production code will substitute.
constexpr char kUrlTemplate[] =
"https://example.com/installer.exe?iid=IIDGUID&stats=STATS&lang=LANGUAGE";
class MockInstallerDownloaderModel : public InstallerDownloaderModel {
public:
MOCK_METHOD(void,
StartDownload,
(const GURL&,
const base::FilePath&,
content::DownloadManager&,
CompletionCallback),
(override));
MOCK_METHOD(void, CheckEligibility, (EligibilityCheckCallback), (override));
MOCK_METHOD(bool, CanShowInfobar, (), (const, override));
MOCK_METHOD(void, IncrementShowCount, (), (override));
MOCK_METHOD(void, PreventFutureDisplay, (), (override));
MOCK_METHOD(bool, ShouldByPassEligibilityCheck, (), (const, override));
};
class InstallerDownloaderControllerTest : public testing::Test {
protected:
InstallerDownloaderControllerTest() {
feature_list_.InitAndEnableFeatureWithParameters(
kInstallerDownloader,
{{kInstallerUrlTemplateParam.name, kUrlTemplate}});
auto download_manager = std::make_unique<content::MockDownloadManager>();
mock_download_manager_ = download_manager.get();
profile_.SetDownloadManagerForTesting(std::move(download_manager));
web_contents_ = web_contents_factory_.CreateWebContents(&profile_);
auto model = std::make_unique<StrictMock<MockInstallerDownloaderModel>>();
mock_model_ = model.get();
controller_ = std::make_unique<InstallerDownloaderController>(
show_infobar_callback_.Get(), is_metric_enabled_mock_callback_.Get(),
std::move(model));
controller_->SetActiveWebContentsCallbackForTesting(
base::BindLambdaForTesting(
[&]() -> content::WebContents* { return web_contents_; }));
controller_->SetShouldShowInfobarForProfileCallbackForTesting(
should_show_infobar_for_profile_mock_callback_.Get());
}
base::test::ScopedFeatureList feature_list_;
content::BrowserTaskEnvironment task_environment_;
TestingProfile profile_;
StrictMock<
base::MockCallback<InstallerDownloaderController::ShowInfobarCallback>>
show_infobar_callback_;
content::TestWebContentsFactory web_contents_factory_;
// Owned by `web_contents_factory_`.
raw_ptr<content::WebContents> web_contents_;
std::unique_ptr<InstallerDownloaderController> controller_;
raw_ptr<MockInstallerDownloaderModel> mock_model_;
raw_ptr<content::MockDownloadManager> mock_download_manager_;
StrictMock<base::MockCallback<base::RepeatingCallback<bool()>>>
is_metric_enabled_mock_callback_;
StrictMock<base::MockCallback<base::RepeatingCallback<bool()>>>
should_show_infobar_for_profile_mock_callback_;
};
TEST_F(InstallerDownloaderControllerTest, BailsWhenInfobarCannotShow) {
EXPECT_CALL(*mock_model_, CanShowInfobar()).WillOnce(Return(false));
controller_->MaybeShowInfoBar();
}
TEST_F(InstallerDownloaderControllerTest, CallsEligibilityWhenInfobarCanShow) {
EXPECT_CALL(*mock_model_, CanShowInfobar()).WillOnce(Return(true));
EXPECT_CALL(should_show_infobar_for_profile_mock_callback_, Run())
.WillOnce(Return(true));
EXPECT_CALL(*mock_model_, ShouldByPassEligibilityCheck())
.WillOnce(Return(false));
EXPECT_CALL(*mock_model_, CheckEligibility(_))
.WillOnce(RunOnceCallback<0>(std::nullopt));
controller_->MaybeShowInfoBar();
}
// All conditions satisfied → coordinator::Show should run exactly once.
TEST_F(InstallerDownloaderControllerTest, ShowsInfobarWhenEligible) {
EXPECT_CALL(*mock_model_, CanShowInfobar()).WillOnce(Return(true));
EXPECT_CALL(should_show_infobar_for_profile_mock_callback_, Run())
.WillOnce(Return(true));
EXPECT_CALL(*mock_model_, CheckEligibility(_))
.WillOnce(base::test::RunOnceCallback<0>(
std::optional<base::FilePath>(FILE_PATH_LITERAL("C:\\foo"))));
EXPECT_CALL(show_infobar_callback_, Run(_, _, _))
.WillOnce(Return(reinterpret_cast<infobars::InfoBar*>(0x1)));
EXPECT_CALL(*mock_model_, IncrementShowCount()).Times(1);
controller_->MaybeShowInfoBar();
}
// If there is no active WebContents, Show() must *not* be called.
TEST_F(InstallerDownloaderControllerTest, SkipsWhenNoActiveContents) {
controller_->SetActiveWebContentsCallbackForTesting(
base::BindLambdaForTesting(
[&]() -> content::WebContents* { return nullptr; }));
EXPECT_CALL(*mock_model_, CanShowInfobar()).WillOnce(Return(true));
EXPECT_CALL(should_show_infobar_for_profile_mock_callback_, Run())
.WillOnce(Return(true));
EXPECT_CALL(*mock_model_, CheckEligibility(_)).Times(1);
controller_->MaybeShowInfoBar();
}
// If the eligibility callback returns `std::nullopt`, no infobar is shown.
TEST_F(InstallerDownloaderControllerTest, SkipsWhenNotEligible) {
EXPECT_CALL(*mock_model_, CanShowInfobar()).WillOnce(Return(true));
EXPECT_CALL(should_show_infobar_for_profile_mock_callback_, Run())
.WillOnce(Return(true));
EXPECT_CALL(*mock_model_, ShouldByPassEligibilityCheck())
.WillOnce(Return(false));
EXPECT_CALL(*mock_model_, CheckEligibility(_))
.WillOnce(base::test::RunOnceCallback<0>(std::nullopt));
controller_->MaybeShowInfoBar();
}
TEST_F(InstallerDownloaderControllerTest,
DownloadUrlHasValidGuidAndNoPlaceholders) {
EXPECT_CALL(is_metric_enabled_mock_callback_, Run()).WillOnce(Return(true));
const base::FilePath destination(FILE_PATH_LITERAL("C:\\tmp"));
EXPECT_CALL(
*mock_model_,
StartDownload(
Property(
&GURL::spec,
AllOf(
// GUID in the iid= query param.
ContainsRegex(
"iid=[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-"
"[0-9a-f]{12}"),
// Metrics flag.
HasSubstr("&stats=1"),
// Language substitution.
HasSubstr("&lang=en"),
// No leftover placeholders.
Not(HasSubstr("IIDGUID")), Not(HasSubstr("STATS")),
Not(HasSubstr("LANGUAGE")))),
destination.AppendASCII(kDownloadedInstallerFileName.Get()), _, _));
controller_->OnDownloadRequestAccepted(destination);
}
TEST_F(InstallerDownloaderControllerTest, DownloadUrlStatsEnabled) {
EXPECT_CALL(is_metric_enabled_mock_callback_, Run()).WillOnce(Return(true));
const base::FilePath destination(FILE_PATH_LITERAL("C:\\tmp"));
EXPECT_CALL(
*mock_model_,
StartDownload(Property(&GURL::spec, HasSubstr("&stats=1")),
destination.AppendASCII(kDownloadedInstallerFileName.Get()),
_, _));
controller_->OnDownloadRequestAccepted(destination);
}
TEST_F(InstallerDownloaderControllerTest, DownloadUrlStatsDisabled) {
EXPECT_CALL(is_metric_enabled_mock_callback_, Run()).WillOnce(Return(false));
const base::FilePath destination(FILE_PATH_LITERAL("C:\\tmp"));
EXPECT_CALL(
*mock_model_,
StartDownload(Property(&GURL::spec, HasSubstr("&stats=0")),
destination.AppendASCII(kDownloadedInstallerFileName.Get()),
_, _));
controller_->OnDownloadRequestAccepted(destination);
}
TEST_F(InstallerDownloaderControllerTest, DownloadUrlLanguageSubstitution) {
EXPECT_CALL(is_metric_enabled_mock_callback_, Run()).WillOnce(Return(true));
const base::FilePath destination(FILE_PATH_LITERAL("C:\\tmp"));
EXPECT_CALL(
*mock_model_,
StartDownload(
Property(&GURL::spec,
AllOf(HasSubstr("&lang=en"), Not(HasSubstr("LANGUAGE")))),
destination.AppendASCII(kDownloadedInstallerFileName.Get()), _, _));
controller_->OnDownloadRequestAccepted(destination);
}
TEST_F(InstallerDownloaderControllerTest,
DownloadGeneratesDifferentGuidEachTime) {
EXPECT_CALL(is_metric_enabled_mock_callback_, Run())
.WillRepeatedly(Return(true));
const base::FilePath destination(FILE_PATH_LITERAL("C:\\tmp"));
const base::FilePath full_destination =
destination.AppendASCII(kDownloadedInstallerFileName.Get());
GURL first_url;
GURL second_url;
{
::testing::Sequence s;
EXPECT_CALL(*mock_model_, StartDownload(_, full_destination, _, _))
.InSequence(s)
.WillOnce(SaveArg<0>(&first_url));
EXPECT_CALL(*mock_model_, StartDownload(_, full_destination, _, _))
.InSequence(s)
.WillOnce(SaveArg<0>(&second_url));
}
controller_->OnDownloadRequestAccepted(destination);
controller_->OnDownloadRequestAccepted(destination);
EXPECT_NE(first_url, second_url);
}
// Bypass = true, eligibility callback returns std::nullopt → infobar shown.
TEST_F(InstallerDownloaderControllerTest,
ShowsInfobarWhenBypassEnabledAndNoDestination) {
base::ScopedTempDir temp_dir;
ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
// Point DIR_USER_DESKTOP to a real, absolute temp location so that the
// fallback path logic succeeds.
base::ScopedPathOverride desktop_override(base::DIR_USER_DESKTOP,
temp_dir.GetPath(),
/*is_absolute=*/true,
/*create=*/true);
EXPECT_CALL(*mock_model_, CanShowInfobar()).WillOnce(Return(true));
EXPECT_CALL(should_show_infobar_for_profile_mock_callback_, Run())
.WillOnce(Return(true));
EXPECT_CALL(*mock_model_, ShouldByPassEligibilityCheck())
.WillOnce(Return(true));
EXPECT_CALL(*mock_model_, IncrementShowCount()).Times(1);
// Eligibility returns std::nullopt → controller must rely on the bypass path.
EXPECT_CALL(*mock_model_, CheckEligibility(_))
.WillOnce(RunOnceCallback<0>(std::nullopt));
// When bypass is enabled we still expect the infobar to be shown.
EXPECT_CALL(show_infobar_callback_, Run(_, _, _))
.WillOnce(Return(reinterpret_cast<infobars::InfoBar*>(0x1)));
controller_->MaybeShowInfoBar();
}
TEST_F(InstallerDownloaderControllerTest, IncrementOnlyOncePerShow) {
EXPECT_CALL(*mock_model_, CanShowInfobar()).WillOnce(Return(true));
EXPECT_CALL(should_show_infobar_for_profile_mock_callback_, Run())
.WillOnce(Return(true));
EXPECT_CALL(*mock_model_, CheckEligibility(_))
.WillOnce(base::test::RunOnceCallback<0>(
std::optional<base::FilePath>(base::FilePath(L"C:\\foo"))));
EXPECT_CALL(*mock_model_, IncrementShowCount()).Times(1);
EXPECT_CALL(show_infobar_callback_, Run(_, _, _))
.WillOnce(Return(reinterpret_cast<infobars::InfoBar*>(0x1)));
controller_->MaybeShowInfoBar();
}
TEST_F(InstallerDownloaderControllerTest, InfobarShownLoggedOncePerSession) {
base::HistogramTester histograms;
EXPECT_CALL(*mock_model_, CanShowInfobar()).WillRepeatedly(Return(true));
EXPECT_CALL(should_show_infobar_for_profile_mock_callback_, Run())
.WillRepeatedly(Return(true));
EXPECT_CALL(*mock_model_, CheckEligibility(_))
.WillRepeatedly(base::test::RunOnceCallbackRepeatedly<0>(
std::optional<base::FilePath>(base::FilePath(L"C:\\foo"))));
EXPECT_CALL(*mock_model_, IncrementShowCount()).Times(1);
// Stub Show() so the controller thinks the infobar has been created.
EXPECT_CALL(show_infobar_callback_, Run(_, _, _))
.Times(1)
.WillOnce(Return(reinterpret_cast<infobars::InfoBar*>(0x1)));
// First display: should log.
controller_->MaybeShowInfoBar();
// Second display in the same session: should NOT log again.
controller_->MaybeShowInfoBar();
histograms.ExpectUniqueSample("Windows.InstallerDownloader.InfobarShown",
/*true=*/1, /*expected_count=*/1);
}
TEST_F(InstallerDownloaderControllerTest, RequestAcceptedTrueMetric) {
base::HistogramTester histograms;
EXPECT_CALL(is_metric_enabled_mock_callback_, Run()).WillOnce(Return(true));
EXPECT_CALL(*mock_model_, StartDownload(_, _, _, _)).Times(1);
controller_->OnDownloadRequestAccepted(
base::FilePath(FILE_PATH_LITERAL("C:\\tmp"))
.AppendASCII(kDownloadedInstallerFileName.Get()));
histograms.ExpectUniqueSample("Windows.InstallerDownloader.RequestAccepted",
/*true=*/1, /*expected_count=*/1);
}
TEST_F(InstallerDownloaderControllerTest, RequestAcceptedFalseMetric) {
base::HistogramTester histograms;
EXPECT_CALL(*mock_model_, PreventFutureDisplay()).Times(1);
controller_->OnInfoBarDismissed();
histograms.ExpectUniqueSample("Windows.InstallerDownloader.RequestAccepted",
/*false=*/0, /*expected_count=*/1);
}
TEST_F(InstallerDownloaderControllerTest, LogsDownloadResultMetric) {
base::HistogramTester histograms;
EXPECT_CALL(is_metric_enabled_mock_callback_, Run()).WillOnce(Return(true));
CompletionCallback download_completion_callback;
EXPECT_CALL(*mock_model_, StartDownload(_, _, _, _))
.WillOnce([&](const GURL&, const base::FilePath&,
content::DownloadManager&, CompletionCallback callback) {
download_completion_callback = std::move(callback);
});
EXPECT_CALL(*mock_model_, PreventFutureDisplay()).Times(1);
controller_->OnDownloadRequestAccepted(
base::FilePath(FILE_PATH_LITERAL("C:\\tmp"))
.AppendASCII(kDownloadedInstallerFileName.Get()));
ASSERT_TRUE(download_completion_callback);
std::move(download_completion_callback).Run(/*success=*/true);
histograms.ExpectUniqueSample("Windows.InstallerDownloader.DownloadSucceed",
/*success=*/1, /*expected_count=*/1);
}
TEST_F(InstallerDownloaderControllerTest,
PreventFutureDisplayCalledOnInfoBarDismissed) {
EXPECT_CALL(*mock_model_, PreventFutureDisplay()).Times(1);
controller_->OnInfoBarDismissed();
}
TEST_F(InstallerDownloaderControllerTest,
PreventFutureDisplayCalledOnDownloadCompleted) {
EXPECT_CALL(is_metric_enabled_mock_callback_, Run()).WillOnce(Return(true));
CompletionCallback completion_callback;
EXPECT_CALL(*mock_model_, StartDownload(_, _, _, _))
.WillOnce(
[&](const GURL&, const base::FilePath&, content::DownloadManager&,
CompletionCallback cb) { completion_callback = std::move(cb); });
controller_->OnDownloadRequestAccepted(
base::FilePath(FILE_PATH_LITERAL("C:\\tmp"))
.AppendASCII(kDownloadedInstallerFileName.Get()));
ASSERT_TRUE(completion_callback);
EXPECT_CALL(*mock_model_, PreventFutureDisplay()).Times(1);
std::move(completion_callback).Run(/*success=*/true);
}
TEST_F(InstallerDownloaderControllerTest, NoInfobarOnGuestProfile) {
EXPECT_CALL(*mock_model_, CanShowInfobar()).WillOnce(Return(true));
// Since this is a guest profile, the eligibility check should not run.
EXPECT_CALL(should_show_infobar_for_profile_mock_callback_, Run())
.WillOnce(Return(false));
EXPECT_CALL(*mock_model_, CheckEligibility(_)).Times(0);
controller_->MaybeShowInfoBar();
}
TEST_F(InstallerDownloaderControllerTest, SkipsWhenActiveBrowserHasNoTabs) {
controller_->SetActiveWebContentsCallbackForTesting(
base::BindLambdaForTesting(
[&]() -> content::WebContents* { return nullptr; }));
EXPECT_CALL(*mock_model_, CanShowInfobar()).WillOnce(Return(true));
EXPECT_CALL(should_show_infobar_for_profile_mock_callback_, Run())
.WillOnce(Return(false));
EXPECT_CALL(*mock_model_, CheckEligibility(_)).Times(0);
controller_->MaybeShowInfoBar();
}
} // namespace
} // namespace installer_downloader